文章目录
- Redis分布式锁的作用?
- Redis分布式锁的底层原理实现?
- Redis分布式锁的应用场景?
- Redis分布式锁遇到相关的场景问题?
- 死锁问题
- 锁超时问题
- 归一问题
- 可重入问题
- 阻塞与非阻塞问题
- 公平锁(Fair Lock)
- 公平锁(Fair Lock)
- 本篇小结
更多相关内容可查看
Redis分布式锁的作用?
Redis分布式锁是一种通过Redis实现的锁机制,用于在分布式系统中控制对共享资源的访问,以确保在同一时间只有一个客户端或进程可以对资源进行操作。以下是Redis分布式锁的主要作用:
- 互斥访问控制: Redis分布式锁可确保在分布式环境下对共享资源的互斥访问。当多个客户端同时尝试获取锁时,只有一个客户端能成功获取,其他客户端将被阻塞或进行重试。
- 资源保护: 分布式锁可用于保护共享资源的一致性和完整性。当多个客户端需要对某一资源执行操作时,通过获取分布式锁,确保每个操作按顺序执行,避免数据损坏或冲突。
- 并发控制: Redis分布式锁允许对某个资源进行并发访问的控制。通过限制同一时间内只有一个客户端可以获取锁,确保对资源的并发访问不会导致竞态条件或不一致的结果。
- 避免重复任务: 分布式锁可用于防止重复执行特定任务,特别是在定时任务或异步处理场景下。通过获取锁后执行任务,其他客户端在获取锁失败时知道任务已经在执行,避免重复执行。
- 防止任务重入: Redis分布式锁可防止同一客户端对同一资源进行重入操作。当一个客户端已经持有锁时,其他请求同一个资源的操作将被阻塞或进行重试,确保资源只被一个客户端处理。
Redis分布式锁的底层原理实现?
Redis分布式锁主要依靠一个SETNX指令实现的 , 这条命令的含义就是“SET if Not Exists”,即不存在的时候才会设置值。只有在key不存在的情况下,将键key的值设置为value。如果key已经存在,则SETNX命令不做任何操作。
这个命令的返回值如下。
● 命令在设置成功时返回1。
● 命令在设置失败时返回0。
例:假设此时有线程A和线程B同时访问临界区代码,假设线程A首先执行了SETNX命令,并返回结果1,继续向下执行。而此时线程B再次执行SETNX命令时,返回的结果为0,则线程B不能继续向下执行。只有当线程A执行DELETE命令将设置的锁状态删除时,线程B才会成功执行SETNX命令设置加锁状态后继续向下执行
Boolean isLocked = stringRedisTemplate.opsForValue().setIfAbsent(PRODUCT_ID, "binghe");
Redis分布式锁的应用场景?
可应用于面试题
- 参考回答一 : 在我最近做的一个项目中 , 我们在任务调度的时候使用了分布式锁 早期我们在进行定时任务的时候我们采用的是SpringTask实现的 , 在集群部署的情况下, 多个节点的定时任务会同时执行 ,造成重复调度
, 影响运算结果, 浪费系统资源 这里为了防止这种情况的发送, 我们使用Redis实现分布式锁对任务进行调度管理
,防止重复任务执行,后期因为我们系统中的任务越来越多 , 执行规则也比较多 , 而且单节点执行效率有一定的限制 ,
所以定时任务就切换成了XXL-JOB ,系统中就没有再使用分布式锁了- 参考回答二 : 我们项目在下单的过程中为了防止订单超卖 , 使用了分布式锁
- 参考回答三 : 我们项目中有一个用户预约充电桩的功能, 为了避免多个用户预约到同一个充电桩, 使用可分布式锁
- 参考回答四 : 我们项目中有一个抢座功能 , 为了避免同一个座位被多个用户购买, 使用了分布式锁
Redis分布式锁遇到相关的场景问题?
死锁问题
在使用分布式锁的时候, 如果因为一些原因导致系统宕机, 锁资源没有被释放, 就会产生死锁
解决的方案 : 上锁的时候设置锁的超时时间
Boolean isLocked = stringRedisTemplate.opsForValue().setIfAbsent(PRODUCT_ID, "binghe", 30, TimeUnit.SECONDS);
锁超时问题
如果业务执行需要的时间, 超过的锁的超时时间 , 这个时候业务还没有执行完成, 锁就已经自动被删除了,其他请求就能获取锁, 操作这个资源 , 这个时候就会出现并发问题 ,
解决的方案 :
- 入Redis的watch dog机制, 自动为锁续期
- 开启子线程 , 每隔20S运行一次, 重新设置锁的超时时间
归一问题
- 如果一个线程获取了分布式锁, 但是这个线程业务没有执行完成之前 , 锁被其他的线程删掉了 , 又会出现线程并发问题 ,
- 这个时候就需要考虑归一化问题 就是一个线程执行了加锁操作后,后续必须由这个线程执行解锁操作,加锁和解锁操作由同一个线程来完成。
解决的方案 : 为了解决只有加锁的线程才能进行相应的解锁操作的问题,那么,我们就需要将加锁和解锁操作绑定到同一个线程中,可以使用ThreadLocal来解决这个问题, 加锁的时候生成唯一标识保存到ThreadLocal , 并且设置到锁的值中 , 释放锁的时候, 判断线程中的唯一标识和锁的唯一标识是否相同, 只有相同才会释放
public class RedisLockImpl implements RedisLock{
@Autowired
private StringRedisTemplate stringRedisTemplate;
private ThreadLocal<String> threadLocal = new ThreadLocal<String>();
@Override
public boolean tryLock(String key, long timeout, TimeUnit unit){
String uuid = UUID.randomUUID().toString();
threadLocal.set(uuid);
return stringRedisTemplate.opsForValue().setIfAbsent(key, uuid, timeout, unit);
}
@Override
public void releaseLock(String key){
//当前线程中绑定的uuid与Redis中的uuid相同时,再执行删除锁的操作
if(threadLocal.get().equals(stringRedisTemplate.opsForValue().get(key))){
stringRedisTemplate.delete(key);
}
}
}
可重入问题
当一个线程成功设置了锁标志位后,其他的线程再设置锁标志位时,就会返回失败。
还有一种场景就是在一个业务中, 有个操作都需要获取到锁, 这个时候第二个操作就无法获取锁了 , 操作会失败
场景示例 :
下单业务中, 扣减商品库存会给商品加锁, 增加商品销量也需要给商品加锁 , 这个时候需要获取二次锁 第二次获取商品锁就会失败 ,这就需要我们的分布式锁能够实现可重入
解决方案 : 实现可重入锁最简单的方式就是使用计数器 , 加锁成功之后计数器 + 1 , 取消锁之后计数器 -1 , 计数器减为0 , 真正从Redis删除锁
public class RedisLockImpl implements RedisLock{
@Autowired
private StringRedisTemplate stringRedisTemplate;
private ThreadLocal<String> threadLocal = new ThreadLocal<String>();
private ThreadLocal<Integer> threadLocalInteger = new ThreadLocal<Integer>();
@Override
public boolean tryLock(String key, long timeout, TimeUnit unit){
Boolean isLocked = false;
if(threadLocal.get() == null){
String uuid = UUID.randomUUID().toString();
threadLocal.set(uuid);
isLocked = stringRedisTemplate.opsForValue().setIfAbsent(key, uuid, timeout, unit);
}else{
isLocked = true;
}
//加锁成功后将计数器加1
if(isLocked){
Integer count = threadLocalInteger.get() == null ? 0 : threadLocalInteger.get();
threadLocalInteger.set(count++);
}
return isLocked;
}
@Override
public void releaseLock(String key){
//当前线程中绑定的uuid与Redis中的uuid相同时,再执行删除锁的操作
if(threadLocal.get().equals(stringRedisTemplate.opsForValue().get(key))){
Integer count = threadLocalInteger.get();
//计数器减为0时释放锁
if(count == null || --count <= 0){
stringRedisTemplate.delete(key);
}
}
}
}
阻塞与非阻塞问题
在使用分布式锁的时候 , 如果当前需要操作的资源已经加了锁, 这个时候会获取锁失败, 直接向用户返回失败信息 , 用户的体验非常不好 , 所以我们在实现分布式锁的时候, 我们可以将后续的请求进行阻塞,直到当前请求释放锁后,再唤醒阻塞的请求获得分布式锁来执行方法。
解决的方案 : 参考自旋锁的思想, 获取锁失败自选获取锁, 直到成功为止 , 当然为了防止多条线程自旋带来的系统资料消耗, 可以设置一个自旋的超时时间 , 超过时间之后, 自动终止线程 , 返回失败信息
@0verride
public boolean tryLock(String key, long timeout, TimeUnit unit)
{
Boolean isLocked = false;
if(threadLocal.get()== null){
String uuid = UUID.randomUUID().tostring();
threadLocal.set(uuid);
isLocked =stringRedisTemplate.opsForValue().setIfAbsent(key, uuid,timeout,unit):/如果获取锁失败则自旋获取锁,自到成工
if(!isLocked)
{
for(;;){
isLocked = stringRedisTemplate.opsForValue().setIfAbsent(key, uuid, timeout, unit)
if(isLocked){
break;
}
}
}else{
isLocked = true;
}
//加锁成功后将计数器加1
if(isLocked){
Integer count = threadLocalInteger.get()== null ?0threadLocalInteger.get();
threadLocalInteger.set(count++);
}
return isLocked:
}
公平锁(Fair Lock)
- 公平锁保证锁的获取按照请求的顺序进行,即先请求锁的线程先获取锁,遵循先来先服务的原则。
- 公平锁的实现会维护一个请求队列,当锁被释放时,队列中的第一个请求线程会被唤醒并获得锁。
公平锁(Fair Lock)
- 非公平锁在锁释放时不会考虑请求锁的顺序,它允许某个后来的请求线程在当前锁被释放时立即尝试获取锁。
- 非公平锁的优势在于它可以减少锁竞争的开销,尤其是在高并发情况下,因为它允许某些线程在等待队列中绕过排队直接获取锁。
本篇小结
其他Redis的相关问题链接如下
Redis数据持久化策略
Redis数据过期策略
Redis数据淘汰策略
Redis集群方案
Redis主从同步
Redis分片集群如何存储及读取数据
Redis跟Mysql如何保证数据一致性
Redis的缓存穿透、缓存击穿、缓存雪崩及解决方案