模拟一个秒杀接口:
商品表:
单机状况下,用Jmeter发送1000个请求过来:
因为加了sychronized进行方法同步,结果正常。nginx
如今模拟集群环境,仍是用上面的接口,但启动两个服务,分别是8080和8081端口,用nginx负载均衡到两个tomcat,用Jmeter发送1000个请求到nginx:
发现库存并无-1000,而且控制的库存量打印有重复。git
结论:
咱们在系统中修改已有数据时,须要先读取,而后进行修改保存,此时很容易遇到并发问题。因为修改和保存不是原子操做,在并发场景下,部分对数据的操做可能会丢失。在单服务器系统咱们经常使用本地锁来避免并发带来的问题,然而,当服务采用集群方式部署时,本地锁没法在多个服务器之间生效,这时候保证数据的一致性就须要分布式锁来实现。github
基于数据库的分布式锁, 经常使用的一种方式是使用表的惟一约束特性。当往数据库中成功插入一条数据时, 表明只获取到锁。将这条数据从数据库中删除,则释放锁。redis
CREATE TABLE `database_lock` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `resource` varchar(1024) NOT NULL DEFAULT "" COMMENT '资源', `lock_id` varchar(1024) NOT NULL DEFAULT "" COMMENT '惟一锁编码', `count` int(11) NOT NULL DEFAULT '0' COMMENT '锁的次数,可重入锁', PRIMARY KEY (`id`), UNIQUE KEY `uiq_lock_id` (`lock_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据库分布式锁表';
当咱们想要得到锁时,能够插入一条数据INSERT INTO database_lock(resource,lock_id,count) VALUES ("resource","lock_id",1);
数据库
注意:在表database_lock中,lock_id字段作了惟一性约束,能够是机器的mac地址+线程编号,这样若是有多个请求同时提交到数据库的话,数据库能够保证只有一个操做能够成功(其它的会报错:ERROR 1062 (23000): Duplicate entry ‘1’ for key ‘uiq_lock_id’),那么咱们就能够认为操做成功的那个请求得到了锁。tomcat
当须要释放锁的时,能够删除这条数据:DELETE FROM database_lock where method_name ='resource' and cust_id = 'lock_id'
服务器
可重入锁:UPDATE database_lock SET count = count + 1 WHERE method_name ='resource' AND cust_id = 'lock_id'
网络
伪代码:并发
public void test(){ String resource = "resource"; String lock_id = "lock_id"; if(!checkReentrantLock(resource,lock_id)){ lock(resource,lock_id);//加锁 }else{ reentrantLock(resource,lock_id); //可重入锁+1 } //业务处理 unlock(resource,lock_id);//释放锁 }
这种实现方式很是的简单,可是须要注意如下几点:负载均衡
Redis 锁主要利用 Redis 的 setnx 命令。
if (setnx(key, 1) == 1){ expire(key, 30) try { //TODO 业务逻辑 } finally { del(key) } }
使用SpingBoot集成Redis后使用分布式锁:
能够看到打印的日志再也不有重复的库存量,最小的库存量与数据库中的一致。
Redis分布式锁可能存在一些问题:
1.设置过时时间
A客户端获取锁成功,可是在释放锁以前崩溃了,此时该客户端实际上已经失去了对公共资源的操做权,但却没有办法请求解锁(删除 Key-Value 键值对),那么,它就会一直持有这个锁,而其它客户端永远没法得到锁。
在加锁时为锁设置过时时间,当过时时间到达,Redis 会自动删除对应的 Key-Value,从而避免死锁。
2.SETNX 和 EXPIRE 非原子性
若是SETNX成功,在设置锁超时时间以前,服务器挂掉、重启或网络问题等,致使EXPIRE命令没有执行,锁没有设置超时时间变成死锁。Redis 2.8 以后 Redis 支持 nx 和 ex 操做是同一原子操做。
3.锁误解除
若是线程 A 成功获取到了锁,而且设置了过时时间 30 秒,但线程 A 执行时间超过了 30 秒,锁过时自动释放,此时线程 B 获取到了锁;随后 A 执行完成,线程 A 使用 DEL 命令来释放锁,但此时线程 B 加的锁尚未执行完成,线程 A 实际释放的线程 B 加的锁。
经过在 value 中设置当前线程加锁的标识,在删除以前验证 key 对应的 value 判断锁是不是当前线程持有。可生成一个 UUID 标识当前线程
4.超时解锁致使并发
若是线程 A 成功获取锁并设置过时时间 30 秒,但线程 A 执行时间超过了 30 秒,锁过时自动释放,此时线程 B 获取到了锁,线程 A 和线程 B 并发执行。
通常有两种方式解决该问题:
将过时时间设置足够长,确保代码逻辑在锁释放以前可以执行完成。
为获取锁的线程增长守护线程,为将要过时但未释放的锁增长有效时间。
更好的方法是是使用Redission,WatchDog机制会为将要过时但未释放的锁增长有效时间。
5.redis主从复制
A客户端在Redis的master节点上拿到了锁,可是这个加锁的key尚未同步到slave节点,master故障,发生故障转移,一个slave节点升级为master节点,B客户端也能够获取同个key的锁,但客户端A也已经拿到锁了,这就致使多个客户端都拿到锁。
使用RedLock
能够看见RedLock基本原理是利用多个Redis集群,用多数的集群加锁成功,减小Redis某个集群出故障,形成分布式锁出现问题的几率。
1.多个客户端建立一个锁节点下的一个接一个的临时顺序节点
2.若是本身是第一个临时顺序节点,那么这个客户端加锁成功;若是本身不是第一个节点,就对本身上一个节点加监听器
3.当某个客户端监听到上一个节点释放锁,本身就排到前面去了,此时继续执行步骤2,至关因而一个排队机制。
使用Curator框架进行加锁和释放锁
参考:
再有人问你分布式锁,这篇文章扔给他
分布式锁的实现之 redis 篇
七张图完全讲清楚ZooKeeper分布式锁的实现原理