0%

分布式理论

​ 分布式架构,将服务模块部署到不同的机器上,来缓解系统压力。在同一台机器上,不会存在数据的一致性等问题。但是由于分布式中,服务部署在不同机器上,网络原因是不可抗因素。

​ CAP,一致性、可用性、分区容错性。必须要保证由于故障导致某些节点断掉以后,每个网络区域都能够对外提供服务,所以分区容错性是前提。而要保证高可用性,数据的一致性就得不到保证;要保证一致性,那么高可用性必然有所影响。

eureka和zookeeper可以作为分布式系统中的注册中心。注册中心负责服务的注册与查找,服务提供者会在注册中心注册自己的服务,消费者向注册中心订阅自己的服务,而注册中心就是一个服务列表。

​ 两者比较:

​ eureka保证了CP,即可用性,在eureka中,每个节点都是平等的,当一个节点挂掉后,也不会影响正常工作,但是查询到的数据不一定是最新的。并且eureka有自我保护机制,当有85%的节点都没有正常工作时,会判定会注册中心网络故障。

​ zookeeper保证了AP,即一致性,在zookeeper中,节点有三种角色,leader、follower、observer,leader即为领导者,提供读和写的服务,负责投票的发起和决议,更新系统的状态;follower即跟随者,主要负责读服务,如果遇到写服务,则会将请求转发给leader,并且可以参加选举投票;observer和follower一样,但是不参加选举投票。如果leader挂掉,会重新投票选举出新的leader。并且在整个过程中,系统是不可用的。但是保证了数据的一致性。

一致性

​ 如何保证一致性。有几种方案。

2PC
阅读全文 »

​ 最近跟着网上做电商项目,在获取商品信息时,使用线程池开启异步任务,并通过CompletableFuture对异步任务进行编排。所以记录一下。

​ 先记录一下线程池


线程池

​ 创建线程总共有4种方式: 继承Thread类、实现Runnable接口、实现Callable接口(FutureTask)、线程池。CPU执行程序时,会不停的切换线程上下文,线程的创建和销毁会带来过度的开销,使用线程池可以提高整体性能,减少线程创建和销毁的处理时间,并且使用线程池可以很好的管理线程。

##### 创建线程池

​ 创建线程池可以使用api中的Executors来进行创建,不过最好使用自定义线程池,即自己new一个线程池(ThreadPoolExecutor(…))。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}

​ 构造方法中的参数含义

1
2
3
4
5
6
7
corePoolSize #the number of threads to keep in the pool, even if they are idle, unless {@code allowCoreThreadTimeOut} is set //保留在池中的线程数量,即使它们空闲,除非设置了容许核心线程超时。核心线程数,最少的线程数
maximumPoolSize: 即池中允许的最大线程数
keepAliveTime & TimeUnit: 当线程数大于核心线程数时,空闲线程在终止前等待新任务的时间,就是空闲线程的存活时间, 后面是规定的单位
BlockingQueue: 线程的阻塞队列,用来存放那些被方法提交但是还没有执行的任务
ThreadFactory: 执行器创建一个线程使用的线程工厂
RejectedExecutionHandler: 当线程池满了或者阻塞队列满了的时候,有新任务提交过来时,执行的拒绝策略

阅读全文 »

redis的持久化

​ redis相比传统的memcache优势之一,即redis支持持久化操作。这也保证了redis挂掉之后,当重启redis的时候,可以恢复数据。

​ redis的配置文件 redis.conf 中 也已经标明了有两种持久化的机制,RDB和AOF, 这两种机制各有不同,而redis默认开启的是RDB,如果想要开启AOF, 可以在配置文件中,将appendonly 改为yes。

RDB

​ RDB,snapshotting,即快照。在指定的时间间隔内将内存中的数据集快照写入磁盘中。而恢复的时候会将快照文件直接读到内存。

​ 备份是如何进行的? redis会单独fork一个子进程来进行持久化。将数据先写入一个临时文件中,等到持久化过程结束了,在用这个临时文件替换上一次持久化的临时文件。整个过程,redis的主进程都不会IO,所以可以保证性能,可以保证高效。 但是由于每隔一段时间进行持久化,所以如果redis挂掉了,那么最后一次持久化会导致数据丢失。

​ 注: fork的作用是复制一个与当前进程一样的进程,变量、环境变量、程序计数器等都是一致的。fork出来的进程是一个新的进程,来作为原进程的子进程。

​ redis是默认开启的,有如下配置

1
2
3
4
5
save 900 1           #在900秒(15分钟)之后,如果至少有1个key发生变化,Redis就会自动触发BGSAVE命令创建快照。

save 300 10 #在300秒(5分钟)之后,如果至少有10个key发生变化,Redis就会自动触发BGSAVE命令创建快照。

save 60 10000 #在60秒(1分钟)之后,如果至少有10000个key发生变化,Redis就会自动触发BGSAVE命令创建快照。

注: bgsave命令: save命令只管保存,所以在进行save时,是全部阻塞的,而bgsave可以在后台异步操作。快照可以在后台相应客户端的请求。

阅读全文 »

redis的过期删除策略

​ redis是以K-V键值对存放的。如果给key设置了存活时间,假设是2分钟,如果2分钟以后,redis是怎么删除这些key的。redis的过期删除策略有两种。

​ 一种是惰性删除,一种是定期删除。

惰性删除

​ 见名知义,当我们取出这些key的时候再进行删除。当我们使用到这些key时,会对key进行检查,是否已经过期,如果过期了就会进行删除。这种删除策略对CPU是比较友好的,因为不需要专门来处理过期的key。但是这样也会导致由于使用到才会检查并删除,所以会有很多过期的key还存放在内存中,比较耗内存。

定期删除

​ 定期删除是每隔一段时间就会执行一次对过期key的删除。不是对所有的key,是抽取一部分的key进行删除。并且redis底层会限制删除操作的时长,以免对CPU造成太大负担的同时,也可以缓解内存空间的压力。


​ 但是,以上两种方法都有弊端,惰性删除会导致内存空间浪费,当积压到一定量之后,很有可能会内存溢出。而定期删除虽然可以解决内存问题,但是由于定期的抽取也会导致有大量的key遗留在内存空间中,内存溢出也只是时间问题。为了解决这一问题,redis有一个内存淘汰机制。

内存淘汰机制

阅读全文 »

前述

redis作为当下十分热门的数据库缓存中间件,在很多项目中都有使用到。并且redis的性能也十分强大,可以缓解关系型数据库的IO压力。与Memcached相比,redis支持多种数据结构,并且redis可以周期性的将更新后的数据写入到磁盘中,即可以做持久化的操作。本文主要谈谈redis常见的五大数据结构,并且可以应用到哪些场景。


redis的五大数据结构有: string、list、set、hash、zset

1. String

string类型的数据大家应该很熟悉,在java中,String是字符串,在redis中,也是一样的,是redis中最基本的数据结构。redis是一款基于key-value键值对的开源框架。string类型即一个key对应一个value。并且value的最大容量是512M。并且string类型是二进制安全的,所以我们可以使用string类型来存储二进制文件或者序列化后的对象。


1.1 string常用命令

set 命令。通过set命令可以在redis中添加键值对。如果我要添加一个key-value是”k1”-“v1” 即可用如下操作。set k1 v1

可以添加键值对,那么就可以通过key来获取对应的value。使用get命令即可获取到与key对应的value

阅读全文 »

1. 哈希环

一致性哈希采用的是哈希环,在redis集群中,通过crc16算法,行程的哈希槽,有2的14次方个。其实redis所使用的算法与一致性哈希类似,但是一致性哈希会出现数据倾斜问题。通过虚拟节点来让平均,防止某一个节点压力太大,而另外一台机器比较空闲。而哈希槽则避免了这一问题。


阅读全文 »

​ 在网上找了个电商项目,没全部做完,主要是做了订单服务,对于分布式系统来说,最大的问题就是:如何保证数据的一致性,当某一服务挂掉后,如何保证数据不丢失,数据同步等,这些问题的造成,很重要的原因就是网络的不可抗因素,数据在网络中传输,而网络的抖动、网速的延迟都是解决不了的,所以需要分布式事务来解决这一问题。

​ 当然整个系统有几大模块: 商品服务(增删改查)、用户服务、订单服务、库存、支付。

​ 订单服务: 用户点击提交订单,向服务器发送请求之后,订单服务会创建订单,并远程调用库存服务,进行锁库存,远程调用库存服务,如果库存服务挂掉了,订单服务的事务无法让其进行回滚,就会出现,订单创建成功,库存却没有减少。这样就会有问题。

1
2
3
4
5
6
7
8
9
10
@PostMapping("/oreder/createOrder")
public Result createOrder(@RequestBody OrderVo orderVo) {
String orderId = orderService.createOrder(orderVo);
if (orderId != null) {
//调用库存服务进行锁库存
stockService.lockStock(order.getStockNum());
Result.ok().data("orderId",order.);
}
return Result.error();
}

​ 可以作为分布式事务的框架有阿里巴巴的开源框架seata,使用AT模式,seata中有三个角色:

​ TC (Transaction Coordinator) - 事务协调者

​ 维护全局和分支事务的状态,驱动全局事务提交或回滚。

​ TM (Transaction Manager) - 事务管理器

​ 定义全局事务的范围:开始全局事务、提交或回滚全局事务。

​ RM (Resource Manager) - 资源管理器

阅读全文 »

AQS

​ AQS,全程AbstractQueuedSynchronizer,翻译过来就是抽象队列同步器,这个类在JUC包下面。在多线程中是十分重要的,保证有效工作线程对锁的抢占,已经线程的排队工作。这些都由AQS来制订规范的,使用者只需要继承该类并对方法进行重写就可以实现共享资源的获取以及释放。

​ AQS原理: 有一个state变量来标记当前资源是否空闲,当state=0表明当前空闲,即可抢占成功,线程抢占成功后,会把state的状态置为1,其他线程在进行锁定时,会尝试抢占,抢占失败之后,就会进入一个队列。这个队列叫做CLH队列,是一个虚拟的双向队列。

​ 做了个原理图,有点拉。其实分析源码,在双向队列中有一个哨兵节点的。

AQS

可以看看AQS抽象类的结构是怎样的,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public abstract class AbstractQueuedSynchronizer 
extends AbstractOwnableSynchronizer
implements java.io.Serializable {

//静态内部类,会把线程封装成一个个node节点放入队列
static final class Node {...}

//双向队列的头节点
private transient volatile Node head;

//双向队列的位节点
private transient volatile Node tail;

//标志当前资源的状态
private volatile int state;

..... //还有很多方法
}

Node类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
static final class Node {
/** Marker to indicate a node is waiting in shared mode */
static final Node SHARED = new Node(); //共享
/** Marker to indicate a node is waiting in exclusive mode */
static final Node EXCLUSIVE = null; //独占

//withStatus的4中状态
static final int CANCELLED = 1; //表明节点(线程) 被取消或者中断
static final int SIGNAL = -1; //表明节点被阻塞,需要unpark,即解锁
static final int CONDITION = -2;
static final int PROPAGATE = -3;

volatile int waitStatus;

volatile Node prev;

volatile Node next;

volatile Thread thread;

Node nextWaiter;

final boolean isShared() {
return nextWaiter == SHARED;
}

final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}

Node() { // Used to establish initial head or SHARED marker
}

Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}

Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}

阅读全文 »

​ 以抢购商品为例子,一步步分析redis实现分布式锁的过程。

​ 在redis中设置商品数量 set goods:01 100 。 假设有100件商品。

单机版

​ 假设下面这个简单的程序,就是一个很简单的逻辑。从Redis中获取数量,如果数量大于0,就可以购买,否则购买不了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Autowired
private StringRedisTemplate redisTemplate;

@GetMapping("/buy")
public String buySales() {
//从redis中获取商品的数量
String result = redisTemplate.opsForValue().get("goods:01");
int nums = (result == null) ? 0 : Integer.parseInt(result);

if (nums > 0) {
//如果还有,商品数量-1
int currentNums = nums - 1;
redisTemplate.opsForValue().set("goods:01", String.valueOf(currentNums));
System.out.println("成功买到商品,还剩余 " + currentNums + " 件");
return "成功买到商品, 还剩 " + currentNums + "件";
} else {
System.out.println("购买失败、、、、");
}
return "购买商品失败,因为xxxx原因......";
}

​ 只有一个客户端,来进行抢购商品。在多线程的条件下进行访问,会出现超卖和重复卖等问题。因此,对于多线程的单击模式下,我们可以通过锁来实现。可以使用synchronized,或者reentrantlock,这样可以解决在多个线程访问的情况下,保证同一时刻,只有一个线程拿到锁。其他线程阻塞。代码如下,把代码放在一个同步代码块中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@GetMapping("/buy")
public String buySales() {
synchronized (this) {
//从redis中获取商品的数量
String result = redisTemplate.opsForValue().get("goods:01");
int nums = (result == null) ? 0 : Integer.parseInt(result);

if (nums > 0) {
//如果还有,商品数量-1
int currentNums = nums - 1;
redisTemplate.opsForValue().set("goods:01", String.valueOf(currentNums));
System.out.println("成功买到商品,还剩余 " + currentNums + " 件");
return "成功买到商品, 还剩 " + currentNums + "件";
} else {
System.out.println("购买失败、、、、");
}
return "购买商品失败,因为xxxx原因......";
}
}

​ 在实际中,在高并发的情况下,肯定不止一台机器,那如果是有多个机器在提供这一服务呢?

分布式服务

阅读全文 »

BeanFactory

​ BeanFactory是spring容器的顶级接口,或者就是源码说的root,根接口。

1
2
3
4
5
6
/**
* The root interface for accessing a Spring bean container.
*/
public interface BeanFactory {

}

​ BeanFactory不能够被实例化,而是定义了一套所有IOC容器必须遵守的规范。可以看看BeanFactory的结构,如下

结构

​ 多个重载方法,getBean(),从spring容器中获取bean对象。 containsBean()判断spring容器中是否包含某个bean。 还有判断bean的作用域,即是否为单例bean或者多例bean的方法,isSingleton和isPrototype,还有根据bean的名字获取名字等等…

具体的容器可以对其进行功能的增加,像spring中用到比较多的ApplicationContext,就是BeanFactory的子类。而ApplicationContext接口又衍生了很多的子类,像比较熟悉的ClassPathXmlApplicationContext、 AnnoationConfigApplicationContext。

类继承图


FactoryBean

阅读全文 »