• Redis7-分布式锁


    目录

    基本原理

    分布式锁的实现

    基于Redis的分布式锁

    Redis分布式锁误删

    分布式锁的原子性问题

    基于Redis的分布式锁优化

    Redission概述

    Redisson入门

    Redisson可重入锁原理

    Reddisson锁重试和WatchDog机制

    Redisson分布式锁原理

    Redission的MultiLock原理

    分布式锁总结


    基本原理

    分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁

    分布式锁需要满足的条件:

    • 可见性:多个线程都能看到相同的结果,注意:这个地方说的可见性并不是并发编程中指的内存可见性,只是说多个进程之间都能感知到变化
    • 互斥:互斥是分布式锁的最基本的条件,使得程序串行执行
    • 高可用:程序不易崩溃,时时刻刻都保证较高的可用性
    • 高性能:由于加锁本身就让性能降低,对于分布式锁本身需要有较高的加锁性能和释放锁性能
    • 安全性

    分布式锁的实现

    分布式锁的核心是实现多线程之间互斥,常见的三种实现方式:

    基于Redis的分布式锁

    实现分布式锁时需要实现的两个基本方法:

    1.获取锁

    • 互斥:确保只能有一个线程获取锁

    • 非阻塞:尝试一次,成功返回true,失败返回false

    2.释放锁

    • 手动释放

    • 超时释放:获取锁时添加一个超时时间

    流程: 

    代码实现:

    需求:定义一个类,实现下面接口,利用Redis实现分布式锁功能

    1. public class SimpleRedisLock implements ILock{
    2. private static final String KEY_PREFIX="lock:";
    3. @Override
    4. public boolean tryLock(long timeoutSec) {
    5. //获取线程标示
    6. String threadId = Thread.currentThread().getId();
    7. //获取锁
    8. Boolean success = stringRedisTemplate.opsForValue()
    9. .setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);
    10. return Boolean.TRUE.equals(success);
    11. }
    12. @Override
    13. public void unlock() {
    14. //通过del删除锁
    15. stringRedisTemplate.delete(KEY_PREFIX + name);
    16. }
    17. }
    1. @Override
    2. public Result seckillVoucher(Long voucherId) {
    3. // 1.查询优惠券
    4. SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
    5. // 2.判断秒杀是否开始
    6. if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
    7. // 尚未开始
    8. return Result.fail("秒杀尚未开始!");
    9. }
    10. // 3.判断秒杀是否已经结束
    11. if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
    12. // 尚未开始
    13. return Result.fail("秒杀已经结束!");
    14. }
    15. // 4.判断库存是否充足
    16. if (voucher.getStock() < 1) {
    17. // 库存不足
    18. return Result.fail("库存不足!");
    19. }
    20. Long userId = UserHolder.getUser().getId();
    21. //创建锁对象(新增代码)
    22. SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
    23. //获取锁对象
    24. boolean isLock = lock.tryLock(1200);
    25. //加锁失败
    26. if (!isLock) {
    27. return Result.fail("不允许重复下单");
    28. }
    29. try {
    30. //获取代理对象(事务)
    31. IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
    32. return proxy.createVoucherOrder(voucherId);
    33. } finally {
    34. //释放锁
    35. lock.unlock();
    36. }
    37. }

    Redis分布式锁误删

    持有锁的线程在锁的内部出现了阻塞,导致它的锁自动释放,这时线程2来尝试获得锁,就拿到了这把锁,然后线程2在持有锁执行过程中,线程1反应过来,继续执行,而线程1执行过程中,走到了删除锁逻辑,此时就会把本应该属于线程2的锁进行删除,这就是误删别人锁的情况说明

    解决方案:在每个线程释放锁的时候,去判断一下当前这把锁是否属于自己,如果不属于自己,则不进行锁的删除

    需求:

    修改之前的分布式锁实现,满足:

    1.在获取锁时存入线程标示(可以用UUID表示)

    2.在释放锁时先获取锁中的线程标示,判断是否与当前线程标示一致

    • 如果一致则释放锁

    • 如果不一致则不释放锁

    代码实现:

    获取锁

    1. private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
    2. @Override
    3. public boolean tryLock(long timeoutSec) {
    4. // 获取线程标示
    5. String threadId = ID_PREFIX + Thread.currentThread().getId();
    6. // 获取锁
    7. Boolean success = stringRedisTemplate.opsForValue()
    8. .setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
    9. return Boolean.TRUE.equals(success);
    10. }

    释放锁

    1. public void unlock() {
    2. // 获取线程标示
    3. String threadId = ID_PREFIX + Thread.currentThread().getId();
    4. // 获取锁中的标示
    5. String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
    6. // 判断标示是否一致
    7. if(threadId.equals(id)) {
    8. // 释放锁
    9. stringRedisTemplate.delete(KEY_PREFIX + name);
    10. }
    11. }

    分布式锁的原子性问题

    线程1现在持有锁之后,在执行业务逻辑过程中,正准备删除锁,而且已经走到了条件判断的过程中,比如它已经拿到了当前这把锁,确实是属于自己的,正准备删除锁,但是此时它的锁到期了,那么此时线程2进来,但是线程1它会接着往后执行,当它卡顿结束后,它直接就会执行删除锁那行代码,相当于条件判断并没有起到作用,这就是删锁时的原子性问题

    Lua脚本解决多条命令原子性问题

    Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。Lua是一种编程语言,它的基本语法:Lua 教程 | 菜鸟教程

    Redis提供的调用函数:

    redis.call('命令名称', 'key', '其它参数', ...)

    例:执行set name jack

    1. # 执行 set name jack
    2. redis.call('set', 'name', 'jack')

    例:先执行set name Rose,再执行get name

    1. # 先执行 set name jack
    2. redis.call('set', 'name', 'Rose')
    3. # 再执行 get name
    4. local name = redis.call('get', 'name')
    5. # 返回
    6. return name

    写好脚本以后,需要用Redis命令来调用脚本,调用脚本的常见命令如下:

    例:执行 redis.call('set', 'name', 'jack') 这个脚本

    如果脚本中的key、value不想写死,可以作为参数传递。key类型参数会放入KEYS数组,其它参数会放入ARGV数组,在脚本中可以从KEYS和ARGV数组获取这些参数:  

    Lua脚本实现释放锁:

    1. -- 这里的 KEYS[1] 就是锁的key,这里的ARGV[1] 就是当前线程标示
    2. -- 获取锁中的标示,判断是否与当前线程标示一致
    3. if (redis.call('GET', KEYS[1]) == ARGV[1]) then
    4. -- 一致,则删除锁
    5. return redis.call('DEL', KEYS[1])
    6. end
    7. -- 不一致,则直接返回
    8. return 0

    利用Java代码调用Lua脚本改造分布式锁

    1. private static final DefaultRedisScript UNLOCK_SCRIPT;
    2. static {
    3. UNLOCK_SCRIPT = new DefaultRedisScript<>();
    4. UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
    5. UNLOCK_SCRIPT.setResultType(Long.class);
    6. }
    7. public void unlock() {
    8. // 调用lua脚本
    9. stringRedisTemplate.execute(
    10. UNLOCK_SCRIPT,
    11. Collections.singletonList(KEY_PREFIX + name),
    12. ID_PREFIX + Thread.currentThread().getId());
    13. }

    总结

    基于Redis的分布式锁实现思路:

    • 利用set nx ex获取锁,并设置过期时间,保存线程标示

    • 释放锁时先判断线程标示是否与自己一致,一致则删除锁

    特性:

    • 利用set nx满足互斥性

    • 利用set ex保证故障时锁依然能释放,避免死锁,提高安全性

    • 利用Redis集群保证高可用和高并发特性

    基于Redis的分布式锁优化

    基于setnx实现的分布式锁存在下面的问题:

    Redission概述

    Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid),它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现

    Redission提供了分布式锁的多种多样的功能:

    官网:https://redisson.org

    Redisson入门

    1.引入依赖:

    1. <dependency>
    2. <groupId>org.redissongroupId>
    3. <artifactId>redissonartifactId>
    4. <version>3.13.6version>
    5. dependency>

    2.配置Redisson客户端:

    1. @Configuration
    2. public class RedissonConfig {
    3. @Bean
    4. public RedissonClient redissonClient(){
    5. // 配置
    6. Config config = new Config();
    7. config.useSingleServer().setAddress("redis://127.0.0.1:6379");
    8. // 创建RedissonClient对象
    9. return Redisson.create(config);
    10. }
    11. }

    3.使用Redission的分布式锁:

    1. @Resource
    2. private RedissionClient redissonClient;
    3. @Test
    4. void testRedisson() throws Exception{
    5. //获取锁(可重入),指定锁的名称
    6. RLock lock = redissonClient.getLock("anyLock");
    7. //尝试获取锁,参数分别是:获取锁的最大等待时间(期间会重试),锁自动释放时间,时间单位
    8. boolean isLock = lock.tryLock(1,10,TimeUnit.SECONDS);
    9. //判断获取锁成功
    10. if(isLock){
    11. try{
    12. System.out.println("执行业务");
    13. }finally{
    14. //释放锁
    15. lock.unlock();
    16. }
    17. }
    18. }

    4.在 VoucherOrderServiceImpl注入RedissonClient:

    1. @Resource
    2. private RedissonClient redissonClient;
    3. @Override
    4. public Result seckillVoucher(Long voucherId) {
    5. // 1.查询优惠券
    6. SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
    7. // 2.判断秒杀是否开始
    8. if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
    9. // 尚未开始
    10. return Result.fail("秒杀尚未开始!");
    11. }
    12. // 3.判断秒杀是否已经结束
    13. if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
    14. // 尚未开始
    15. return Result.fail("秒杀已经结束!");
    16. }
    17. // 4.判断库存是否充足
    18. if (voucher.getStock() < 1) {
    19. // 库存不足
    20. return Result.fail("库存不足!");
    21. }
    22. Long userId = UserHolder.getUser().getId();
    23. //创建锁对象 使用分布式锁
    24. RLock lock = redissonClient.getLock("lock:order:" + userId);
    25. //获取锁对象
    26. boolean isLock = lock.tryLock();
    27. //加锁失败
    28. if (!isLock) {
    29. return Result.fail("不允许重复下单");
    30. }
    31. try {
    32. //获取代理对象(事务)
    33. IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
    34. return proxy.createVoucherOrder(voucherId);
    35. } finally {
    36. //释放锁
    37. lock.unlock();
    38. }
    39. }

    Redisson可重入锁原理

    Reddisson锁重试和WatchDog机制

    Redisson分布式锁原理

    可重入:利用hash结构记录线程id和重入次数

    可重试:利用信号量和Pubsub功能实现等待、唤醒,获取锁失败的重试机制

    超时续约:利用whtchDog,每隔一段时间(releaseTime / 3),重置超时时间

    Redission的MultiLock原理

    为了提高redis的可用性,会搭建集群或者主从,以主从为例

    此时去写命令,写在主机上, 主机会将数据同步给从机,但是假设主机还没有来得及把数据写入到从机时,此时主机宕机,哨兵会发现主机宕机,并且选举一个slave变成master,而此时新的master中实际上并没有锁信息,锁信息已经丢掉了

    为了解决这个问题,Redission提出来了MultiLock锁,使用这把锁就不再使用主从,每个节点的地位都是一样的, 这把锁加锁的逻辑需要写入到每一个主丛节点上,只有所有的服务器都写入成功,此时才是加锁成功,假设现在某个节点挂了,那么它去获得锁的时候,只要有一个节点拿不到,都不能算是加锁成功,就保证了加锁的可靠性

    MultiLock加锁原理:

    当设置了多个锁时,Redission会将多个锁添加到一个集合中,然后用while循环不停去尝试拿锁,但是会有一个总共的加锁时间,这个时间是用需要加锁的个数 * 1500ms ,假设有3个锁,那么时间就是4500ms,假设在这4500ms内,所有的锁都加锁成功, 那么此时才算是加锁成功,如果在4500ms有线程加锁失败,则会再次去进行重试

    分布式锁总结

    1.不可重入Redis分布式锁

    • 原理:利用setnx的互斥性;利用ex避免死锁;释放锁时判断线程标示
    • 缺陷:不可重入、无法重试、锁超时失效

    2.可重入的Redis分布式锁

    • 原理:利用hash结构,记录线程标示和重入次数;利用watchDog延续锁时间;利用信号量控制锁重试等待
    • 缺陷:Redis宕机引起锁失效问题

    3.Redisson的MultiLock

    • 原理:多个独立的Redis节点,必须在所有节点都获取重入锁,才算获取锁成功
    • 缺陷:运维成本高,实现复杂
  • 相关阅读:
    Hadoop1X,Hadoop2X和hadoop3X有很大的区别么?
    【无标题】
    CSP-J2022普及组题解T1:乘方
    5.26机器人基础-DH参数 正解
    震惊!Cell:人肠道中鉴定出14万种病毒
    iOS 提取图片中的文字
    zabbix中文乱码解决方法
    35岁创业的重要性
    ReactNative学习笔记
    uni-app 原生ios插件开发以及自定义基座
  • 原文地址:https://blog.csdn.net/m0_73902080/article/details/141036355