1. 为什么需要分布式锁?
我们假设一个场景:比如某电商网站做一个促销活动,例如秒杀、抢优惠券…这个时候并发超级高。
如果你的系统是集中式结构,那么ok,没有问题,但是如果你的系统是分布式架构…那么问题就来了。
在实际中为了高可用,我们通常会把服务做集群,例如秒杀系统有三个服务:A1、A2、A3,Nginx做负载均衡。
假设有三个用户在同一时刻请求nginx,nginx把这两个请求分别发放到A1、A2、A3,这个时候在A1、A2、A3中获取到的库存都是50,紧接着代码往下执行…
最后的结果是:在A1、A2、A3三个服务中的执行的结果都是剩余库存49。
但是不对啊,明明三个用户都秒杀成功了啊,按道理库存应该是47才对!
这不就跟我们初学Java时的线程安全一样的问题吗!超买超卖!
为什么会这样?
我之前说过,如果是集中式架构,那么我们可以用Java的18般武艺去完美的解决它,不出任何bug。
但现在不行,我们是分布式结构,Java的锁只对单个JVM才有用。
所以这个时候我们就要用到分布式锁
分布式锁的实现方式一般有三种:
- 基于数据库实现排他锁
- 基于redis实现
- 基于zookeeper实现(最初zookeeper就是为了分布式锁而来的)
2. Redis实现分布式锁
上面的案例的解决办法:
Redis命令:
SETNX key value:将 key 的值设为 value ,当且仅当 key 不存在。
当代码执行到这里的时候,只有一个请求可以执行成功,即获取到锁,其他的请求都返回失败。
这样就解决了其他请求同时修改数据的问题了,是不是开心了?
不!!!这样还是有问题的:假设有一个请求刚执行完setnx,刚获取到锁,此时报错了…那岂不是其他用户永远都得不到锁了?
解决:将可能报错代码try finally起来,在finally中将锁释放:del(key)。这样即使代码报错,锁也可以得到释放。是不是又开心了?
不!!!这样还有问题:假设有一个请求刚执行完setnx,刚获取到锁,此时服务挂了,finally也执行不了,锁又得不到释放…那岂不是其他用户永远都得不到锁了?
解决:使用Redis的EXPIRE命令给缓存设置过期时间。这样即使服务挂了,锁也可以得到释放。又开心了?
先获取锁,后设置过期时间,两行代码,非原子性。有没有这样一个极端的场景:获取锁之后,服务挂了,还没来的及设置过期时间…
解决:将SETNX、EXPIRE命令合成一条命令,要么一起成功,要么一起失败(StringRedisTemplate有这样的方法,底层是redis将这两个命令成原子性)。又开心了?
假设这样一个场景:我们设置的过期时间是10s,但是请求A执行代码逻辑在10s中内没有执行完,这个时候redis帮我们把锁释放了,此时请求B过来获取到锁。最后请求A在finally的时候del(key)…可想而知,问题大了,因为请求A释放的锁并不是我们自己…
解决:使用UUID或者谷歌的雪花算法生成的ID,在我们获取锁的时候将key的value设置为UUID,这样我们在del(key)的时候首先将值取出来判断一下UUID是否相同,相同才del(key)。又开心了?
缓存击穿 击穿是一个热点key失效,执行redis的setnx命令
uuid是保证给自己的锁唯一标志,防止误删。
上面的问题其实我们还没有得到解决,那就是:代码逻辑没执行完,过期时间到了,redis自动帮我们删除掉了…
解决:强行续命法:在我们获取到锁之后,开启一个子线程定时(定时时间推荐为过期时间的1/3)检查我们的锁是否到了过期时间,如果锁快过期我们的代码逻辑还没有执行完那么重新设置过期时间(例如重新设置为10s);最后我们代码执行的时候将子线程显示停止。又开心了?
介绍个东西:Redission,他也是redis的一个客户端,他的好处是:当我们获取到锁之后会自动为我们开启子线程强行续命,子线程也不用我们自己停止。Redission里面有很多针对分布式的功能值得我们去学去用。
其实还有极端的情况:redis挂了,哈哈哈
为了高可用,可以做Redis主从、哨兵、集群
假设Redis是主从架构。我们获取到锁之后,主节点正要往从节点同步数据,假设此时主节点挂了,从节点顶替上来,此时新的主节点是没有锁的,其他请求过来就又可以获取到锁了,这样就会出现问题…
但是这种情况极少数…一般我们写到这一步就可以了。
如果非要避免这个问题,那么建议使用Zookeeper,Zookeeper设计之初就是为了分布式锁而来的。