【超卖的情况】
例如单个服务器的电商系统,一个用户调用下单接口,要先调用检查库存的接口去检查库存。在并发的时候,会预先把商品的库存存在redis中,下单的时候会更新redis的库存。如果有两个请求同时到来,请求1执行到锁定库存,更新数据库的库存为0,但是还没执行到更新redis的库存为0。而请求2刚好执行到检查redis库存,发现redis的库存还是1,这时候请求2就会继续去锁定数据库的库存并且修改数据库库存-1,就会出现超卖的情况。
【解决方案】
加锁,把【检查redis库存】【锁定数据库库存】【更新数据库库存】【更新redis库存】的整个流程都上锁,只有当请求1这一整套流程执行完的时候,另一个线程请求2才能进行执行【检查redis库存】
【实现方式】
使用java提供的synchronized或者ReentrantLock来锁住,然后在【更新数据库和redis库存量】执行完以后再释放锁
【换成分布式系统以后怎么办】
这个时候两个用户的请求落在两个不同的服务器上,那么这两个请求就是可以同时执行的了,还是会出现超卖的问题,因为使用的Java的锁是只对自己JVM里面的线程才是有效的,对于其他的JVM的线程是无效的。也就是说Java提供的原生锁在集群分布的场景下就会失效。
【分布式锁方案】
既然Java的原生锁在分布式场景下会失效,那就尝试保证多个服务器加的锁事同一个锁。分布式锁的思路就是:在整个系统中提供一个全局、唯一的获取锁的“东西”,然后在每个系统要加锁的时候,都去找这个“东西”拿到一把锁,这样不同的系统拿到的就可以认为是同一把锁,这个“东西”可以是Redis、zookeeper或者数据库。
【redis分布式锁实现思路】
在redis中设置一个值表示加了锁,然后释放锁的时候就把这个key删除
获取锁的时候
// 获取锁
// NX是指如果key不存在就成功,key存在返回false,PX可以指定过期时间
// 给key=anyLock设置一个值value=unique_value
SET anyLock unique_value NX PX 30000
释放锁的时候
// 释放锁:通过执行一段lua脚本
// 释放锁涉及到两条指令,这两条指令不是原子性的
// 需要用到redis的lua脚本支持特性,redis执行lua脚本是原子性的
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
【redis分布式锁的问题】
1-如果是使用单机部署模式,会存在单点问题,只要redis故障了,加锁就会失效。
2-使用主从的模式,加锁的时候只对一个节点加锁,如果master节点故障了,发生了主从切换,此时就会有可能出现锁丢失的问题。
3-如果使用redlock的算法,加锁的时候获取当前时间戳,然后轮流在每个master节点上创建锁,客户端计算好创建锁的时间,如果建立锁的时候小于超时时间,就算创建成功。如果超时了创建失败就得依次删除这个锁,需要这样不断的轮询去尝试获取锁。这样无法保证加锁的过程一定正确。
【最终解决方法:Redisson】
使用开源框架Redission
创建redisson的对象,然后调用getLock方法设定一个唯一的值,表示一把锁lock,然后对象lock调用方法lock或者unlock来加锁和释放锁。
RedissonClient redisson = Redisson.create(config);
RLock lock = redisson.getLock("anyLock");
lock.lock();
lock.unlock();
【zookeeper是什么】
zookeeper是一个中提供配置管理、分布式系统以及命名的中心化服务。
【zk的特点】
zk的模型是这样的:zk包含一系列的节点,叫做znode,就好像文件系统一样每个znode表示一个目录,然后znode有一些特性:
(1)有序节点:假如当前有一个父节点为/lock,我们可以在这个父节点下面创建子节点;zookeeper提供了一个可选的有序特性,例如我们可以创建子节点“/lock/node-”并且指明有序,那么zookeeper在生成子节点时会根据当前的子节点数量自动添加整数序号。也就是说,如果是第一个创建的子节点,那么生成的子节点为/lock/node-0000000000,下一个节点则为/lock/node-0000000001,依次类推。
(2)临时节点:客户端可以建立一个临时节点,在会话结束或者会话超时后,zookeeper会自动删除该节点。
(3)事件监听:在读取数据时,我们可以同时对节点设置事件监听,当节点数据或结构变化时,zookeeper会通知客户端。当前zookeeper有如下四种事件:
1-节点创建
2-节点删除
3-节点数据修改
4-子节点变更
【zk实现分布式锁的方案】
(1)ZooKeeper的每一个节点,都是一个天然的顺序发号器
使用zk的临时节点和有序节点,每个线程获取锁就是在zk创建一个临时有序的节点,比如在/lock/目录下。
(2)ZooKeeper节点的递增有序性,可以确保锁的公平
创建节点成功后,获取/lock目录下的所有临时节点,再判断当前线程创建的节点是否是所有的节点的序号最小的节点
(3)如果当前线程创建的节点是所有节点序号最小的节点,则认为获取锁成功。
(4)ZooKeeper的节点监听机制,可以保障占有锁的传递有序而且高效
如果当前线程创建的节点不是所有节点序号最小的节点,则对节点序号的前一个节点添加一个事件监听。比如当前线程获取到的节点序号为/lock/003,然后所有的节点列表为[/lock/001,/lock/002,/lock/003],则对/lock/002这个节点添加一个事件监听器。
(5)如果锁释放了,会唤醒下一个序号的节点,然后重新执行第3步,判断是否自己的节点序号是最小。
比如节点/lock/001释放删除了,/lock/002监听到时间,此时节点集合为[/lock/002,/lock/003],则/lock/002为最小序号节点,获取到锁。
InterProcessMutex interProcessMutex = new InterProcessMutex(client,"/anyLock");
interProcessMutex.acquire();
interProcessMutex.release();
【zk实现分布式锁的逻辑】
(1)lock方法
(2)trylock方法尝试加锁
1-创建临时顺序节点,并且保存自己的节点路径
2-判断是否是第一个,如果是第一个,则加锁成功。如果不是,就找到前一个Znode节点,并且保存其路径到prior_path。
(3)checkLocked()检查是否持有锁
在checkLocked()方法中,判断是否可以持有锁。判断规则很简单:当前创建的节点,是否在上一步获取到的子节点列表的第一个位置:
1-如果是,说明可以持有锁,返回true,表示加锁成功;
2-如果不是,说明有其他线程早已先持有了锁,返回false。
(4)await()监听前一个节点释放锁
【redis实现分布式锁的缺点】
(1)它获取锁的方式简单粗暴,获取不到锁直接不断尝试获取锁,比较消耗性能。
(2)redis的设计定位决定了它的数据并不是强一致性的,在某些极端情况下,可能会出现问题。锁的模型不够健壮
(3)即便使用redlock算法来实现,在某些复杂场景下,也无法保证其实现100%没有问题
(4)redis分布式锁,其实需要自己不断去尝试获取锁,比较消耗性能。
【zk实现分布式锁的优点】
(1)zookeeper天生设计定位就是分布式协调,强一致性。锁的模型健壮、简单易用、适合做分布式锁。
(2)如果获取不到锁,只需要添加一个监听器就可以了,不用一直轮询,性能消耗较小。