序言
今天一起学习下分布式锁,分布式锁常见于集群环境下,用于做一些单机锁无法解决的问题,比如扣减库存的场景,如果扣减库存的业务机器是多台部署的就会出现超卖现象(JAVA中常见的lock和Synchronized都属于单机锁),此时就需要引入分布式锁了。
分布式锁的实现有很多种,最为常见的是通过redis实现和zookeeper实现,今天我们通过redis来实现一下分布式锁吧。
分布式锁
redis分布式锁相关视频讲解:学习视频
redis实现分布式锁,那么我们不妨思考如何用redis实现呢,条件为:这个操作是互斥的,redis中正好有一条指令可以实现互斥的效果即setnx(k)指令,这条指令的意思就是如果指定的k不存在则执行成功,如果redis中已存在指定的k,则返回false,通过它可以完美实现多台机器间的互斥了(eg:现有5台机器,第一台机器执行了setnx("lock")后,其他机器执行setnx("lock")都是失败的,直到第一台机器将这个key给释放删除掉)。
代码演示
简单模拟一个创建订单扣减库存的场景
@Autowired
private RedisTemplate redisTemplate;
private final String LOCKKEY="DistributedLock";
@PostMapping("/createOrder")
public String createOrder(String goodsId){
//尝试创建锁
Boolean flag = redisTemplate.opsForValue().setIfAbsent(LOCKKEY, "value");
if (flag){
if (!StringUtils.isEmpty(goodsId)){
Integer stock = (Integer)redisTemplate.opsForValue().get(goodsId);
if (stock>0){
stock--;
redisTemplate.opsForValue().set(goodsId,stock);
//......创建订单
//释放锁
redisTemplate.delete(LOCKKEY);
return "创建订单成功";
}else {
redisTemplate.delete(goodsId);
//释放锁
redisTemplate.delete(LOCKKEY);
return "库存不足";
}
}else {
//释放锁
redisTemplate.delete(LOCKKEY);
return "库存不足";
}
}else {
//对于没有获取到锁的用户,给它返回一个提示信息,让他重试即可
return "系统繁忙,请稍后重试";
}
}
现在我们实现了一个最为简单的分布式锁版本,但是其中还是存在着不少问题,下面我们一步步的将这些问题解决,并对其进行优化。
解决问题
问题一:程序异常导致的锁未释放
问题描述:很有可能出现在业务执行过程中出现了异常情况,导致没有执行到锁释放的代码,针对于这种情况我们需对代码进行完善,代码如下:
@SuppressWarnings("all")
@Controller
public class Lock {
@Autowired
private RedisTemplate redisTemplate;
private final String LOCKKEY="DistributedLock";
@PostMapping("/createOrder")
public String createOrder(String goodsId){
//尝试创建锁
Boolean flag = redisTemplate.opsForValue().setIfAbsent(LOCKKEY, "value");
if (flag){
try {
if (!StringUtils.isEmpty(goodsId)){
Integer stock = (Integer)redisTemplate.opsForValue().get(goodsId);
if (stock>0){
stock--;
redisTemplate.opsForValue().set(goodsId,stock);
//......创建订单
return "创建订单成功";
}else {
redisTemplate.delete(goodsId);
return "库存不足";
}
}else {
return "库存不足";
}
}finally {
//释放锁,防止出现异常导致的锁无法释放场景
redisTemplate.delete(LOCKKEY);
}
}else {
//对于没有获取到锁的用户,给它返回一个提示信息,让他重试即可
return "系统繁忙,请稍后重试";
}
}
}
分享更多关于 Linux后端开发网络底层原理知识学习提升 点击 学习资料 获取,完善技术栈,内容知识点包括Linux,Nginx,ZeroMQ,MySQL,Redis,线程池,MongoDB,ZK,Linux内核,CDN,P2P,epoll,Docker,TCP/IP,协程,DPDK等等。
问题二:宕机导致的锁无法释放
当成功添加锁后,出现了服务器宕机,锁此时就无法释放了,因此我们需要为锁添加过期时间,这样即使宕机了锁最终也是会被释放掉的。
- 注意的点:添加锁过期时间这步操作必须得和添加锁成原子性操作,否则也出现添加完锁,正要添加过期时间时,服务器宕机情况。
正好redisTemplate中支持这种原子性操作,示例如下:
Boolean flag = redisTemplate.opsForValue().setIfAbsent(LOCKKEY, "value",20L, TimeUnit.SECONDS);
问题三:A线程释放B线程的锁
这种情况也是可能发生的,当A线程添加好锁之后,由于某些原因业务代码一直没有执行完,此时锁自动释放了,B线程成功获取到锁,在B线程执行完毕之前,A线程执行到了finally代码块,直接将B线程的锁给释放掉了。
- 解决方法:我们可以为每个线程都随机生成不同的value,在释放前比较一下value是否相同即可
@PostMapping("/createOrder")
public String createOrder(String goodsId){
//生成value
String value = UUID.randomUUID().toString().replace("-", "");
//尝试创建锁,并设置过期时间
Boolean flag = redisTemplate.opsForValue().setIfAbsent(LOCKKEY, value,20L, TimeUnit.SECONDS);
if (flag){
try {
if (!StringUtils.isEmpty(goodsId)){
Integer stock = (Integer)redisTemplate.opsForValue().get(goodsId);
if (stock>0){
stock--;
redisTemplate.opsForValue().set(goodsId,stock);
//......创建订单
return "创建订单成功";
}else {
redisTemplate.delete(goodsId);
return "库存不足";
}
}else {
return "库存不足";
}
}finally {
//释放锁,防止出现异常导致的锁无法释放场景
Object o = redisTemplate.opsForValue().get(LOCKKEY);
if (o!=null){
String redisValue=(String)o;
//当且仅当redis中的value和当前线程value相同时,释放锁
if (value.equals(redisValue)){
redisTemplate.delete(LOCKKEY);
}
}
}
}else {
//对于没有获取到锁的用户,给它返回一个提示信息,让他重试即可
return "系统繁忙,请稍后重试";
}
}
问题四:释放锁的操作不是原子性
我们可以看到刚才的代码中,释放锁操作不是原子性的,它是先查询再删除的操作,这其中也会出现问题,即A线程查询并获取到锁的value,此时value也和A线程的value完全相同,正要准备释放锁的时候,锁自动过期了,B线程加锁成功,A又一次释放掉了B的锁。
解决方式:
- 第一种方式:通过lua脚本来让释放锁的操作变成原子性的.
//释放锁,防止出现异常导致的锁无法释放场景
//lua脚本保证原子性
String script="if redis.call(‘get’,KEYS[1]) == ARGV[1]" +
"then" +
" return redis.call(‘del’,KEYS[1])" +
"else" +
" return 0" +
"end";
Object eval = jedis.eval(script, Collections.singletonList(LOCKKEY),
Collections.singletonList(value));
if ("0".equals(eval.toString())){
System.out.println("删除失败");
}else {
System.out.println("删除锁成功");
}
jedis.close();
- 第二种方式:引入redis中的事务,其中有一个watch命令,它可以监听一个key,开启事务,如果被监听的key被其他线程进行了操作,那么提交是不成功的,因为我们对这个key进行了watch操作
//释放锁,防止出现异常导致的锁无法释放场景
//开启手动提交事务
redisTemplate.setEnableTransactionSupport(true);
//监听锁
redisTemplate.watch(LOCKKEY);
//开启事务
redisTemplate.multi();
if (value.equals(redisTemplate.opsForValue().get(LOCKKEY))){
redisTemplate.delete(LOCKKEY);
}
//提交事务(如果在监听到现在提交这段时间,锁被其他线程占用,那么删除锁操作不会执行,否则删除锁成功)
redisTemplate.exec();
//取消监听
redisTemplate.unwatch();
问题五:锁自动失效及主从架构下的问题
- 锁自动失效问题
可以回顾刚才的问题三和问题四,归根结底的原因就是因为锁自动过期,导致其他线程占用到锁。
锁自动过期无非就是因为锁的存活时间小于业务执行时间,如何解决呢?其实我们可以再启动一个线程让它定时为锁增加存活时间,当删除锁时一并将该线程停止了。
- 主从架构下的问题
这个问题主要体现在,假设有主从两个库,向主库添加锁成功,数据还没来得及同步给从库,此时主库宕机了,从库成了新的主库,一个线程来了向新主库中添加锁,添加成功了,此时其实就出现了两个线程同时添加锁成功的问题了,怎么解决呢?可以设置从库的延时重启,当锁过期后再让从库成为新的主库。
总结
可以看出通过redis实现分布式锁,有很多需要注意的地方,还有一些注意的点我也没写全,所以说想要自己动手实现一个分布式锁还是十分有难度的,那么我们其实可以用市面上已经封装好的分布式锁就可以了,比如很有名的Redission框架,它就帮我们实现了redis分布式锁,使用方法也很简单,直接getlock,然后lock,unlock就可以了。