redis 分布式锁的核心命令
redis分布式锁的实现主要是依靠setnx
和expire
两个命令完成。
注意:由于setnx
和expire
是两个命令,会存在如果 setnx
是成功的,但是 expire
设置失败,一旦出现了释放锁失败,或 者没有手工释放,那么这个锁永远被占用,其他线程永远也抢不到锁。这种情况有两种解决办法:
-
使用
set
命令时,同时设置过期时间。不再单独是使用expire
命令。set key value [EX seconds] [PX milliseconds] [NX|XX]
EX
seconds:设置失效时长,单位秒 、PX
milliseconds:设置失效时长,单位毫秒NX
:key不存在时设置value,成功返回OK,失败返回(nil)XX
:key存在时设置value,成功返回OK,失败返回(nil) -
使用lua脚本,将加锁的命令放在lua脚本中原子性的执行。
redission 框架
redission 是redis的第三方库,是基于Netty实现的,是高性能的第三方库。其特点:
- 支持 Redis 单节点(single)模式、哨兵(sentinel)模式、主从(Master/Slave)模式以及集群 (Redis Cluster)模式;
- 提供丰富的分布式集合,如:Map,Multimap,Set,SortedSet,List,Deque,Queue 等
redission 中的分布式锁种类
总体的Redisson框架的分布式锁类型,大致如下:
- 可重入锁
- 公平锁
- 联锁
- 红锁
- 读写锁
- 信号量、可过期信号量
- 闭锁(CountDownLatch)
可重入锁(Reentrant Lock)
Redisson的分布式可重入锁RLock Java对象实现了java.util.concurrent.locks.Lock
接口,同时还支持自动过期解锁。
公平锁(Fair Lock)
redission中的公平锁是可重入的,即可重入公平锁。同时有自动过期解锁的功能。
联锁(MultiLock)
Redisson的RedissonMultiLock
对象可以将多个RLock
对象关联为一个联锁,每个RLock
对象实例可以 来自于不同的Redisson实例。
红锁(RedLock)
Redisson的RedissonRedLock
对象实现了Redlock
介绍的加锁算法。该对象也可以用来将多个RLock对 象关联为一个红锁,每个RLock
对象实例可以来自于不同的Redisson
实例。
public void testRedLock(RedissonClient redisson1,RedissonClient redisson2,
RedissonClient redisson3){
RLock lock1 = redisson1.getLock("lock1");
RLock lock2 = redisson2.getLock("lock2");
RLock lock3 = redisson3.getLock("lock3");
RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3);
try {
// 方式1:同时加锁:lock1 lock2 lock3, 红锁在大部分节点上加锁成功就算成功。
lock.lock();
// 方式2:尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
读写锁(ReadWriteLock)
Redisson的分布式可重入读写锁RReadWriteLock
,Java对象实现了java.util.concurrent.locks.ReadWriteLock
接口。同时还支持自动过期解锁。该对象允许同时有多个读取锁,但是最多只能有一个写入锁。
信号量(Semaphore)
Redisson的分布式信号量(Semaphore)Java对象RSemaphore
采用了与 java.util.concurrent.Semaphore
相似的接口和用法。
信号量主要用于限制对有限资源的并发访问。例如,如果有一组共享的数据库连接资源,而你希望最多只有特定数量的线程能够同时使用这些连接,就可以使用信号量来进行控制。
可过期信号量
Redisson的可过期性信号量(PermitExpirableSemaphore
)实在RSemaphore对象的基础上,为每个 信号增加了一个过期时间。每个信号可以通过独立的ID来辨识,释放时只能通过提交这个ID才能释放。
闭锁/倒数闩(CountDownLatch)
Redisson的分布式闭锁(CountDownLatch
)Java对象RCountDownLatch
采用了与 java.util.concurrent.CountDownLatch
相似的接口和用法。
细说 红锁
RedLock算法思想:
- 在分布式环境下,同时向多个redis节点申请锁;
- 只有当得到的锁 大于等于
n/2 + 1
时(得到过半的redis节点的锁),才算是这个整体的RedLock加锁成功。 - 释放锁时,客户端会向所有获取锁时涉及的 Redis 节点发送释放锁的请求。但是,只要大部分节点(通常是超过半数的节点)成功释放了锁,就认为释放锁的操作是成功的。
解决的问题:红锁避免了说仅仅在一个redis实例上 加锁而带来的问题(单一实例出现故障,就会导致加锁出问题)。
红锁的缺点(被弃用):
-
时钟同步的问题:红锁的实现依赖于各个节点上的时钟同步。如果节点之间的时钟存在较大偏差,可能会导致锁的获取和释放出现问题。例如,如果一个节点的时钟比其他节点快,可能会导致锁提前过期,从而影响系统的正确性。
-
网络分区延迟问题:客户端与某一个redis节点通信出现延迟,会导致不能正确感知是否获得锁。这会影响对 “大多数节点成功获取锁” 这一条件的判断,从而导致不确定是否真正获得了锁。
-
节点故障处理:如果故障节点在锁的获取或释放过程中扮演了重要角色,可能会导致锁的状态不一致,从而影响系统的可靠性。(虽然红锁算法通常会尝试在节点故障时进行容错处理,但这种处理可能并不总是完美的,并且可能会引入额外的复杂性和不确定性)
-
官方认为它没有经过充分的检验。
红锁的替代方案
红锁被弃用,那可以用什么替代呢?
答:
- 用单实例的redis锁;
- 用普通的锁,如
setNX
、或者 redission。
redis 分段锁
Redis分段锁(Sharded Lock 或 Partitioned Lock) 是一种针对高并发场景下的锁机制优化方案。其核心思想是将全局锁分解为多个子锁(段锁),从而降低锁的粒度,减少锁的竞争,提升系统的并发性能。
它可以用于秒杀系统中。Redis分段锁可以有效地在这种高并发场景下提高性能,并确保数据一致性。
举例场景:短时间内,每秒商品A的订单有6000个订单。在该场景中使用redis分段锁的核心点:
-
分段:可以基于用户ID或者订单ID来做哈希分段,这样不同的订单就会被分配到不同的锁段中,减少锁的粒度。
-
每个分段使用一个独立的Redis分布式锁,每个线程只对某一个分段加锁,这样多个线程可以并发操作库存。
而业务流程是:
-
订单进入系统:每个订单到达时,首先根据订单ID计算哈希值,确定其属于哪个分段。
-
获取分段锁:通过 Redis 分布式锁机制,获取订单对应分段的锁。
扣减库存:在获取锁成功后,扣减商品库存。
生成订单:生成订单并保存到数据库。
释放锁:操作完成后,释放锁,允许其他请求进入该锁段。
超卖问题
分段锁机制减少了订单之间的锁竞争,但需要确保商品库存的一致性。可以将商品库存管理集中在Redis中,以确保每次扣减操作都是原子操作,避免超卖。
也就是异步库存扣减,库存的扣减先存储在redis中,等待并发量小的时候,才同步到数据库。