Redis分布式锁
目录
前言:
我觉得无论做什么还是要知行合一,至少对于我来说只看理论是记忆不长久的,底层的细节还是无法真正体会的。前些日子也是还在焦虑实习的事情,但是也是看到了一句话:“让他们卷去吧!我躺平了,至少以后是饿不死的。”我在想确实是饿不死的,我卷也是为了以后生活质量的提高,但是无论干什么事情还是自己心里喜欢,自己愿意才能事半功倍,我喜欢坐在电脑桌前写着这些代码,与其焦虑,不如真正的放慢速度,好好的去干一些自己喜欢的事情...
于是我便想着,反正要背八股,不如真正的放慢速度,把每一章的知识重新复习一遍,其中的场景都实现一遍,每一个知识点都去做个笔记,每一个面试题都用自己的话说一遍,真正的去享受知识...
以下笔记都是基于黑马程序员的面试题写的:
Redis中关于缓存三兄弟(穿透、击穿、雪崩)和数据一致性问题的解决方案实例-CSDN博客
Redis 持久化机制_redis save 机制-CSDN博客
Redis数据过期策略_redis 如何解决数据过期-CSDN博客
一、使用场景
Redis分布式锁适用于需要强一致性和互斥访问的场景
-
场景1: 防止重复操作(如提交订单或支付)
-
场景2:资源访问控制(如库存扣减或秒杀商品抢购)
-
场景3: 分布式任务调度(如集群环境防止定时任务重复执行)
二、redis分布式锁
Redis实现分布式锁主要利用Redis的setnx命令。setnx是SET if not exists(如果不存在,则 SET)的简写。
获取锁:
# 添加锁,NX是互斥、EX是设置超时时间
SET lock value NX EX 10
释放锁:
# 释放锁,删除即可
DEL key
1.执行流程
redis分布式锁的执行流程如下:
当一个线程尝试获取锁失败后,就会进入阻塞等待状态;获取成功后就开始执行任务,执行完成后自动释放锁,期间若业务超时或者服务宕机,锁也会在指定的超时时间后自动释放锁,从而避免了死锁的发生。

三、redisson实现的分布式锁
在上述redis的setnx指令中,我们并不好控制其中锁的有效时长,当业务执行过长,超过了锁的持有时间,那么就会对业务的原子性造成影响。
Redisson 提供了更完善的分布式锁实现:
1.看门狗机制
redis中提供了看门狗机制,很好的解决了业务执行时间大于锁的持有时间问题:

当线程1加锁成功之后,会单独开启一个线程用于锁的续期(Watch dog),默认锁的持有时间是30s,也就是每隔10s做一次锁的续期。且redisson提供了锁的重试机制,也就是持锁期间有其他线程来请求,那么其他的线程获取锁失败后就会一直循环获取,当然会有一个循环的阈值,这样的好处就是在高并发情况下,能够很好的提高分布式锁的使用性能。
2.可重入锁
redisson中提供了可重入锁机制,同一线程可多次获取锁,这样避免了死锁的发生。其中底层采用的是如下所示的hash数据结构,记录线程id和重入次数:

3.trylock细节
对于trylock()方法,有以下的关于看门狗机制的细节:
RLock seckillLock = redissonClient.getLock("seckill");
// 等待时间为10s,默认持有时间为30s,启用看门狗机制
seckillLock.tryLock(10, TimeUnit.SECONDS);
// 等待时间为10s,自设置持有时间为30s,不启用看门狗机制
seckillLock.tryLock(10, 30, TimeUnit.SECONDS);
四、redisson分布式锁的使用场景案例
1.秒杀商品抢购
场景说明:对于秒杀商品的抢购实现一人一单且无超卖问题

流程详解:
首先,通过布隆过滤器过滤掉不存在的商品。
// 1.布隆过滤器过滤
RBloomFilter<Long> bloomFilter = redissonClient.getBloomFilter("product");
if (!bloomFilter.contains(productId)) {
throw new RuntimeException("秒杀商品抢购:商品不存在");
}
然后,判断用户是否已经参与抢购。
// 2.查询用户是否已经抢购了该商品
Boolean isSeckill = redisTemplate.opsForSet().isMember("seckill:" + productId, userId);
// 2.1 已经抢购则直接返回
if (isSeckill) {
return "用户已参与该商品的抢购";
}
其次,判断库存是否充裕。
// 3.判断库存是否充足
Product product = productMapper.selectById(productId);
if (product.getStock() <= 0) {
return "商品已售罄";
}
接着,使用分布式锁防止业务并发执行,并进行双重校验用户是否已经参与抢购,若仍未抢购,则扣减库存并创建订单。
// 4.扣减库存并创建订单
RLock seckillLock = redissonClient.getLock("seckill" + productId);
try {
// 4.1 获取锁
Boolean flag = seckillLock.tryLock(10, TimeUnit.SECONDS);
if (!flag) {
return "系统繁忙,请稍后重试...";
}
// 4.2 双重检测,查询用户是否已经抢购了该商品
isSeckill = redisTemplate.opsForSet().isMember("seckill:" + productId, userId);
if (isSeckill) {
return "用户已参与该商品的抢购";
}
// 4.2 扣减库存
LambdaUpdateWrapper<Product> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper
.set(Product::getStock, product.getStock() - 1)
.eq(Product::getProductId, productId)
.gt(Product::getStock, 0);
productMapper.update(updateWrapper);
// 4.3 创建订单
redisTemplate.opsForSet().add("seckill:" + productId, userId);
return "抢购成功";
} catch (InterruptedException e) {
throw new RuntimeException("系统繁忙,请稍后重试...");
} finally {
seckillLock.unlock();
}
运行测试:我们使用jemeter来进行高并发的测试,1s内发送100次请求来测试。

查看数据库,一人只抢购了一单:

查看结果树,只有一个请求抢购成功:

五、相关面试问题
1.redis的分布式锁是如何实现的?
在redis中提供了一个命令setnx,由于redis是单线程的,当一个客户端对某一个key使用了命令后,在相应的key没有过期或删除时,其余的客户端是无法设置该key的。
2.你是如何控制redis分布式锁的有效时长呢?
redis的setnx指令并不好控制锁的有效时长,我最上使用的是redisson框架来实现这一问题的。
redisson框架中提供了一种锁的续期机制——看门狗机制。
首先,当一个线程获取分布式锁成功后,锁的默认持有时间为30秒,看门狗机制会每隔锁的持有时间%3,也就是10s来检测当前业务是否还持有锁,若持有锁则继续增加锁的持有时间,完成业务后会自动释放锁。
然后,若其他线程获取锁失败,则会一直循环获取,直到成功获取锁,当然这种循环是有一个阈值的。这样的好处就是在高并发情况下,能够很好的提高分布式锁的使用性能。
3.redisson实现的分布式锁是可重入的吗?
redisson实现的分布式锁是可重入的,它的底层使用的是hash数据结构,记录的是线程的id和重入的次数。这个重入在内部就是判断是否是当前线程持有的锁,若是则重入次数+1,释放锁则-1,这样做是为了避免死锁的发生。
4.redisson实现的分布式锁能解决主从一致问题吗?
不能解决,因为会有这种问题:线程1加锁成功后master节点宕机,线程2在新的master节点再次加锁,导致两个节点共持一把锁的问题。
但是可以使用redisson提供的红锁来解决,但是这样的话,性能就太低了,如果业务中非要保证数据的强一致性,建议采用zookeeper实现的分布式锁
更多推荐


所有评论(0)