Redis分布式锁

1、由来

  • 分布式系统多线程、多进程分布在不同的机器上,原来的单机部署情况下的并发策略失效,比如synchronized和ReentrantLock。为了解决此问题需要一种跨JVM的互斥机制来控制共享资源的访问->分布式锁,不仅能锁住同一进程下的不同线程,还能锁住不同进程下的不同线程。

2、概念

  • 在分布式架构下,数据只有一份,此时就需要用利用锁的技术控制某个时刻的进程数

  • 用一个状态值表示锁,对锁的占用和释放通过状态值标识

3、特点

  • 互斥性

    • 不仅要在同一JVM进程下的不同线程间互斥,还要在不同JVM进程下的不同线程间互斥

  • 锁超时

    • 支持锁的自动释放,防止死锁

  • 正确、高效、高可用

    • 加锁和解锁必须是同一个线程,加锁和解锁操作一定要高效,提供锁的的服务具备容错性

  • 可重入

    • 若一个线程拿到锁之后继续去获取锁还能获取到,则锁是可重入的(方法的递归调用)

  • 阻塞/非阻塞

    • 若获取不到直接返回视为非阻塞的,若获取不到就一直等待锁的释放或者等待超时的,视为阻塞的

  • 公平/非公平

    • 按照请求的顺序获取锁视为公平的

4、高并发超卖问题

@Autowired
RedisTemplate<String,String> redisTemplate;
​
String maotai = "maotai20221222";
​
@PostConstruct
public void init(){
    redisTemplate.opsForValue().set(maotai,"100");
}
​
@GetMapping("/get/maotai2")
public String sale2(){
    synchronized (this) {
        Integer count = Integer.parseInt(redisTemplate.opsForValue().get(maotai));
        if (count > 0) {
            redisTemplate.opsForValue().set(maotai, String.valueOf(count - 1));
            return "success";
        }
        return "fail";
    }
}
  • synchronized只锁住本地进程下的多个线程

  • 在单机环境中能保证数据安全性,但在分布式环境下不能保证

5、SETNX

  • 基于redis的SETNX实现分布式锁

  • SETNX key value

    • 若key不存在,设置成功

    • 若key存在,设置失败

  • SETNX返回值

    • 设置成功,返回1

    • 设置失败,返回0

  • SETNX实现同步锁的流程

    • 使用SETNX获取锁,若返回0(key已存在,锁存在)则获取失败,反之获取成功

    • 为防止获取锁后程序出现异常,导致其他线程/进程调用SETNX返回0而进入死锁状态,需要为该key设置合理TTL

    • 使用DEL释放锁

String lockey = "maotailock";
​
@GetMapping("/get/maotai3")
public String sale3(){
​
//        Boolean isLock = redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> {
//            redisConnection.setNX(lock.getBytes(),"1".getBytes());
//            return null;
//        });
​
    Boolean isLock = redisTemplate.opsForValue().setIfAbsent(lockey, "1");//底层调用setnx
​
    if (isLock){
        redisTemplate.expire(lockey,5, TimeUnit.SECONDS);
        try {
            Integer count = Integer.parseInt(redisTemplate.opsForValue().get(maotai));
            if (count > 0){
                redisTemplate.opsForValue().set(maotai,String.valueOf(count - 1));
                return "success";
            }
            return "fali";
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            redisTemplate.delete(lockey);
        }
    }
    return "Don't get lock";
}
  • 问题

    • setnx和设置超时时间是非原子性操作,为保证原子性需要使用lua脚本或者设置值的同时设置TTL

    • 错误解锁,加锁和解锁必须是同一个线程

6、使用lua脚本

@GetMapping("/get/maotai4")
public String sale4(){
    String lockLua = "" +
        "if redis.call('setnx',KEYS[1],ARGV[1]) == 1 " +
        "then redis.call('expire',KEYS[1],ARGV[2]);" +
        "return true else return false " +
        "end";
​
    redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> redisConnection.eval(
        lockLua.getBytes(), //要执行的lua脚本
        ReturnType.BOOLEAN, //lua脚本返回值类型
        1, //lua脚本中涉及的key的数量
        lockey.getBytes(), //KEYS[1]对应的值
        "1".getBytes(), //ARGV[1]对应的值
        "5".getBytes() //ARGV[2]对应的值
    ));
    if (isLock){
        try {
            Integer count = Integer.parseInt(redisTemplate.opsForValue().get(maotai));
            if (count > 0){
                redisTemplate.opsForValue().set(maotai,String.valueOf(count - 1));
                return "success";
            }
            return "fail";
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            //释放锁
            redisTemplate.delete(maotai);
        }
    }
    return "Don't get lock";
}
  • 问题

    • 若设置的TTL不合理,线程A业务执行时间超过TTL,其他线程会进来操作共享变量,导致数据不安全

    • 若线程A达到TTL时会释放线程B的锁,导致错误释放

7、SETNX的同时设置TTL

@GetMapping("/get/maotai5")
public String sale5(){
    Boolean isLock =redisTemplate.opsForValue().setIfAbsent(lockey,"1",5,TimeUnit.SECONDS);
    if (isLock){
        try {
            Integer count = Integer.parseInt(redisTemplate.opsForValue().get(maotai));
            if (count > 0){
                redisTemplate.opsForValue().set(maotai,String.valueOf(count - 1));
                return "success";
            }
            return "fail";
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            redisTemplate.delete(lockey);
        }
    }
    return "Don't get lock";
}
  • 虽然保证原子性,但设置的TTL不合理也会导致错误解锁

8、为锁加唯一标识

@GetMapping("/get/maotai6")
public String sale6(){
    String requestId = UUID.randomUUID().toString() + Thread.currentThread().getId();
​
    Boolean isLock = redisTemplate.opsForValue().setIfAbsent(lockey,requestId,5,TimeUnit.SECONDS);
​
    if (isLock){
        try {
            Integer count = Integer.parseInt(redisTemplate.opsForValue().get(maotai));
            if (count > 0){
                redisTemplate.opsForValue().set(maotai,String.valueOf(count - 1));
                return "success";
            }
            return "fail";
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            String id = redisTemplate.opsForValue().get(lockey);
            //判断是自己的锁才能去释放
            if (id != null && id.equals(requestId)){
                redisTemplate.delete(lockey);
            }
        }
    }
    return "Don't get lock";
}
  • 若线程A在finally中得到key之后刚好达到TTL,此时线程B进来并获取到锁,然后线程A执行DEL会将线程B的锁释放,错误解锁

9、解锁时使用lua脚本

@GetMapping("/get/maotai7")
public String sale7(){
    String requestId = UUID.randomUUID().toString() + Thread.currentThread().getId();
    Boolean isLock = redisTemplate.opsForValue().setIfAbsent(lockey,requestId,5,TimeUnit.SECONDS);
    if (isLock){
        try {
            Integer count = Integer.parseInt(redisTemplate.opsForValue().get(maotai));
            if (count > 0){
                redisTemplate.opsForValue().set(maotai,String.valueOf(count - 1));
                return "success";
            }
            return "fail";
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            String unlockLua = "" +
                "if redis.call('get',KEY[1]) == ARGV[1]" +
                "then redis.call('del',KEY[1]);" +
                "return true else return false" +
                "end";
            redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> redisConnection.eval(
                unlockLua.getBytes(), //lua脚本
                ReturnType.BOOLEAN, //lua脚本返回值类型
                1, //lua脚本中涉及的key的数量
                lockey.getBytes(), //KEY[1]
                requestId.getBytes() //ARGV[1]
            ));
        }
    }
    return "Don't get lock";
}

10、锁续期

  • 拿到锁之后执行业务,若业务的执行时间超过了锁的过期时间,则给锁续期

    • 给拿到锁的线程创建一个守护线程,守护线程定时判断拿到锁的线程是否还持有锁,若持有锁则为其续期

ScheduledExecutorService executorService;//创建守护线程
ConcurrentSkipListSet<String> set = new ConcurrentSkipListSet<>();//队列
​
@PostConstruct
public void init2(){
    executorService = Executors.newScheduledThreadPool(1);
    //续期的lua脚本
    String renewalLua = "" +
        "if redis.call('get',KEYS[1] == ARGV[1]" +
        "then redis.call('expire',KEYS[1],ARGV[2]);" +
        "return true else return false" +
        "end)";
    executorService.scheduleAtFixedRate(() -> {
        Iterator<String> iterator = set.iterator();
        while (iterator.hasNext()){
            String requestId = iterator.next();
            redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> {
                Boolean eval = false;
                try {
                    eval = redisConnection.eval(
                        renewalLua.getBytes(),
                        ReturnType.BOOLEAN,
                        1,
                        lockey.getBytes(),
                        requestId.getBytes(),
                        "5".getBytes()
                    );
                }catch (Exception e){
                    log.info("锁续期失败,{}",e.getMessage());
                }
                return eval;
            });
        }
    },0,1,TimeUnit.SECONDS);
}
​
@GetMapping("/get/maotai8")
public String sale8() {
    String requestId = UUID.randomUUID().toString() + Thread.currentThread().getId();
    Boolean isLock = redisTemplate.opsForValue().setIfAbsent(lockey, requestId, 5, TimeUnit.SECONDS);
    if (isLock){
        //若获取成功后让守护线程为其续期
        set.add(requestId);
        try {
            Integer count = Integer.parseInt(redisTemplate.opsForValue().get(maotai));
            if (count > 0){
                redisTemplate.opsForValue().set(maotai,String.valueOf(count - 1));
                sale8();//递归调用,锁可重入
                TimeUnit.SECONDS.sleep(10);
                return "success";
            }
            return "fail";
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            //解锁锁续期
            set.remove(requestId);
            //释放锁
            String unlockLua = "" +
                "if redis.call('get',KEY[1]) == ARGV[1]" +
                "then redis.call('del',KEY[1]);" +
                "return true else return false" +
                "end";
            redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> redisConnection.eval(
                unlockLua.getBytes(), 
                ReturnType.BOOLEAN, 
                1, 
                lockey.getBytes(), 
                requestId.getBytes() 
            ));
        }
    }
    return "Don't get lock";
}

11、锁的可重入/阻塞锁

  • 加锁的次数和解锁的次数要一致,使用hash数据类型,记录重入次数

  • 之前实现的都是非阻塞锁,若获取不到锁就返回了;阻塞锁:获取不到锁就等待锁的释放,知道获取到锁或者等待超时

    1. 基于客户端轮询方案

      • 每隔一定时间就尝试获取锁,浪费资源

    2. 基于redis发布/订阅方案

      • 线程A获取锁并设置TTL后,线程B获取锁失败,然后订阅线程A释放锁的消息,线程B处于阻塞等待状态,知道等到线程A释放锁

    3. 基于Redisson

      • Redisson内置了一系列的分布式对象,分布式集合,分布式锁,分布式服务等诸多功能特性,是一款基

        于Redis实现,拥有一系列分布式系统功能特性的工具包

      @Value("${spring.redis.host}")
      String host;
      @Value("${spring.redis.port}") 
      String port;
      ​
      @Bean
      public RedissonClient redissonClient() {
          Config config = new Config();
          config.useSingleServer().setAddress("redis://"+host+":"+port);
          return Redisson.create(config);
      }
      ​
      @Autowired
      private RedissonClient redissonClient;
      ​
      @GetMapping("/get/maotai9")
      public String sale9(){
          //要去获取锁
          RLock lock = redissonClient.getLock(lockey);
          //获取到锁
          lock.lock();
          try {
              Integer count = Integer.parseInt(redisTemplate.opsForValue().get(maotai));
              if (count > 0) {
                  redisTemplate.opsForValue().set(maotai,String.valueOf(count-1));
                  return "success";
              }
              return "fail";
          }catch (Exception e){
              e.printStackTrace();
          }finally {
              //释放锁
              lock.unlock();
          }
          return "";
      }
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值