应用场景
商品超卖问题
假设我们要做一个秒杀系统,我们通常的做法是,我们应该是提前设置好库存,然后加载到缓存中,对修改缓存中库存的方法加锁使用synchronized或者ReentrantLock或ReentrantReadWriteLocK加锁
这样在单机多线程下,没有问题,因为这个时候上述锁都在一个jvm里面,一个线程获取到锁后,其他线程就会被阻塞。但是在分布式环境下,同一服务有多个节点,假设有两个节点A、B,当并发请求进来,A节点内a1线程先获得锁,A节点内其他线程会被阻塞,但是B节点是一个单独jvm,B节点内的线程还是能有b1线程获取到锁,b2,、b3线程会被阻塞。这时修改库存的方法就被两个节点内的线程分别同时获取到锁,然后修改数据。
这种情况下,我们就需要用到分布式锁,在修改缓存中库存方法内获得分布式锁,出来完成后在释放掉分布式锁
简单来说就是分布式锁是java本地锁的升级版本,如果项目内有用到java锁的场景,那么多节点部署服务的时候,都要考虑使用分布式锁
分布式锁特性
必须支持的特性:
1,互斥性:只能被一个客户端的一个线程持有
2,锁超时:支持超时,防止持有锁的客户端,还没有释放锁时,宕机了,造成死锁
3,同客户端加解锁:加锁和解锁必须是同一个客户端,客户端a不能解客户端b加的锁
非必须特性:
1,可重入
2,可实现公平锁或非公平锁
解决方案
1,数据库加锁
多个节点连同一个数据库,在数据库层加锁。
优点:实现简单,不需要引入额外的jar包或技术
缺点:依赖数据库,高并发场景数据库压力大
2,redis
单节点的redis没有问题,但是存在单点故障风险
redis集群主从节点是异步同步数据,高并发下可能存在多个线程同时加锁成功或者主从节点还没有同步数据的时候主节点宕机,会造成数据不一致
加锁:使用set(final String key, final String value, final SetParams params)
key=锁名
value=加锁线程的唯一标识,主要用于解锁时判断是否加锁的线程
SetParams setParams.nx();//nx() 相当于setnx()方法,setParams.ex(expireTime);//设置过期时间 ex 单位秒 px 单位毫秒 注:上述方法是较新的jedis才有,有的使用老版本的jedis,使用的是setnx+expire实现加锁设置过期时间,这样的话,加锁和设置过期时间就不是原子操作,如果已经加锁了但还没有设置过期时间宕机了,就会造成锁永远存在
2.1 redisson
节点:1,单redis部署认为是一个节点;2,一主N从结构认为是一个节点
redisson操作单节点redis的时候,使用RedissonLock.tryLock()方法加锁,对于一主N从结构的部署是有可能造成同时获取到锁的情况
redisson给redis集群(Cluster)加锁的时候,要先获取到每个节点的锁对象,然后getRedLock(RLock... locks)获取到级联锁,然后使用RedissonRedLock.tryLock()遍历给每个redis节点加锁,加锁成功的节点大于总结点的一半即认为加锁成功
加锁:如果指定了锁失效时间,则锁失效时间过后会自动释放;
如果没有指定锁失效时间,则默认失效时间为30s,但会启动看门狗线程,看门狗线程每10s(默认失效时间的1/3),重新设置当前锁的失效时间为30s,相当于只要当前线程没有释放锁,则看门狗会一直给锁续期
3,zookeeper
zookeeper是强一致性,适合做分布式锁,但是由于是强一致性,所以性能方面会受一定的影响
加锁:主要是利用zk的临时带序号节点,获取锁就是在zk指定目录下创建临时带序号节点,获取锁的线程判断自己创建的节点序号是否是最小的,若是最小的则表示获取到锁,若不是最小的则监听比自己节点序号小一号的节点,然后wait,等待监听的节点发生变化zk通知自己。
优点:不需要设置过期时间,但是客户端要记得解锁,即删除节点,若客户端宕机断开与zk的链接,则zk会自动删除断开链接创建的临时节点
问题:为什么不使用临时无序号节点或者都监听序号最小的节点?因为这样会导致所有等待加锁的线程全部被唤醒,导致无谓的资源浪费