目录

基本原理和不同实现方式对比

满足分布式系统或集群模式下多进程可见且互斥的锁。
image.png

分布式锁常见的三种实现:
image.png

Redis分布式锁实现思路

两个基本方法::

  • 获取锁:

    • 互斥:确保只有一个线程获取锁
    • 非阻塞:尝试一次,成功返回true,失败返回false

      添加锁,利用setnx的互斥特性

      SETNX lock thread1

      添加锁过期时间,避免服务宕机引起死锁

      EXPIRE lock 10

      合并成一条语句,保证原子性

      SET lock thread1 EX 10 NX

  • 释放锁:

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

      释放锁,删除即可

      DEL key

image.png

实现Redis分布式锁版本1

实现

  1. package com.hmdp.utils;
  2. public interface ILock {
  3. /**
  4. * 尝试获取锁
  5. * @param timeoutSec 锁持有的超时时间,过期后自动释放
  6. * @return true代表获取锁成功; false代表获取锁失败
  7. */
  8. boolean tryLock(long timeoutSec);
  9. /**
  10. * 释放锁
  11. */
  12. void unlock();
  13. }
  1. @Resource
  2. private StringRedisTemplate stringRedisTemplate;
  3. @Transactional
  4. public Result createVoucherOrder(Long voucherId) {
  5. //一人一单
  6. Long userId = UserHolder.getUser().getId();
  7. //创建锁对象
  8. SimpleRedisLock redisLock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
  9. //尝试获取锁
  10. boolean isLock = redisLock.tryLock(1200);
  11. if(!isLock){
  12. //获取锁失败
  13. return Result.fail("不允许重复下单!");
  14. }
  15. try {
  16. int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
  17. if(count > 0){
  18. //用户已经购买过
  19. return Result.fail("用户已经购买过!");
  20. }
  21. //5.扣减库存
  22. boolean success = seckillVoucherService.update()
  23. .setSql("stock = stock - 1")
  24. .eq("voucher_id", voucherId).gt("stock",0)
  25. .update();
  26. if(!success) {
  27. return Result.fail("库存不足!");
  28. }
  29. //6.创建订单
  30. VoucherOrder voucherOrder = new VoucherOrder();
  31. //6.1订单id
  32. long orderId = redisIdWorker.nextId("order");
  33. voucherOrder.setId(orderId);
  34. //6.2用户id
  35. voucherOrder.setUserId(userId);
  36. //6.3代金券id
  37. voucherOrder.setVoucherId(voucherId);
  38. save(voucherOrder);
  39. //7.返回订单id
  40. return Result.ok(orderId);
  41. } finally {
  42. redisLock.unlock();
  43. }
  44. }

测试

打断点,通过PostMan进行测试两个用户
image.png

image.png
image.png
image.png
image.png
通过JMeter测试高并发

Redis分布式锁误删问题

image.png
极端情况:
业务阻塞导致锁提前释放,线程1醒来后,释放了线程2的锁,然后线程3加入了触发并行。

image.png
解决方法:
在释放锁时判断标识

解决Redis分布式锁误删

image.png
需求:修改之前分布式锁实现,满足:

  1. 在获取锁时存入线程标识(可以用UUID)
  2. 在释放锁时先获取锁线程标识,判断与当前标识是否一致 ```java private static final String ID_PREFIX = UUID.randomUUID().toString(true) + “-“;
    @Override public boolean tryLock(long timeoutSec) { //获取线程标识 String threadId = ID_PREFIX + Thread.currentThread().getId(); //获取锁 Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS); //避免自动拆箱空指针 return Boolean.TRUE.equals(success); }

@Override public void unlock() { //获取线程标识 String threadId = ID_PREFIX + Thread.currentThread().getId(); //获取锁中标识 String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name); if(threadId.equals(id)){ //释放锁 stringRedisTemplate.delete(KEY_PREFIX + name); } }

  1. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/1123881/1650890415061-cc1e4876-7442-402d-a439-bf07ac97cf3b.png#clientId=ub66d34b1-4391-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=134&id=ud66cd396&margin=%5Bobject%20Object%5D&name=image.png&originHeight=268&originWidth=2257&originalType=binary&ratio=1&rotation=0&showTitle=false&size=435244&status=done&style=none&taskId=u2519ef2b-f87e-4e48-a217-21127391ee4&title=&width=1128.5)
  2. <a name="hxGJM"></a>
  3. ## Redis分布式锁原子性问题
  4. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/1123881/1650890734832-d2dabcf5-38c2-4b48-99d6-8b91746cd523.png#clientId=ub66d34b1-4391-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=348&id=u4f68d6a6&margin=%5Bobject%20Object%5D&name=image.png&originHeight=920&originWidth=1607&originalType=binary&ratio=1&rotation=0&showTitle=false&size=269218&status=done&style=none&taskId=u234c07e8-7731-49a3-be94-3610daff598&title=&width=608)<br />判断锁标识和释放是两个动作,这两个动作之间产生了阻塞。<br />解决方法:保证这两个动作的原子性。
  5. <a name="ergjA"></a>
  6. ## Lua脚本解决多条脚本原子性问题
  7. **RedisLua脚本**<br />Redis提供Lua脚本功能,在一个脚本编写多条Redis命令,确保多条命令原子性。<br />Redis提供调用函数<br />语法如下:<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/1123881/1650891254476-9f241987-cb8d-4b9c-be8a-f3fec93d9f83.png#clientId=ub66d34b1-4391-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=33&id=u1c85c8ce&margin=%5Bobject%20Object%5D&name=image.png&originHeight=61&originWidth=569&originalType=binary&ratio=1&rotation=0&showTitle=false&size=33211&status=done&style=none&taskId=u69bd3051-2419-4985-a8d9-3def7900101&title=&width=310.5)<br />例如要执行set name jack,则脚本如下<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/1123881/1650891261826-edb74bc8-faf1-427c-a79f-9ea09cd43aa2.png#clientId=ub66d34b1-4391-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=41&id=ua57d54fa&margin=%5Bobject%20Object%5D&name=image.png&originHeight=59&originWidth=444&originalType=binary&ratio=1&rotation=0&showTitle=false&size=26627&status=done&style=none&taskId=u60dd9ee4-e848-40a8-b96f-c9778801d6a&title=&width=305)<br />例如要先执行set name Rese,再执行get name<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/1123881/1650891318614-371e6e88-0fdc-4ddc-8a4c-65c54a207e5a.png#clientId=ub66d34b1-4391-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=96&id=u5e3dd259&margin=%5Bobject%20Object%5D&name=image.png&originHeight=165&originWidth=521&originalType=binary&ratio=1&rotation=0&showTitle=false&size=64988&status=done&style=none&taskId=u82a41204-068d-4bd0-9aef-90335aa629a&title=&width=303.5)<br />写好脚本后,用Redis命令调用脚本<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/1123881/1650891368687-162ef72f-ab20-4149-9336-cc2110c56b99.png#clientId=ub66d34b1-4391-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=80&id=ubf87fa2e&margin=%5Bobject%20Object%5D&name=image.png&originHeight=160&originWidth=749&originalType=binary&ratio=1&rotation=0&showTitle=false&size=87078&status=done&style=none&taskId=u33d8c993-eb1a-4671-beee-bd1b75cb4f4&title=&width=374.5)<br />例如,要执行redis.call('set','name','jack')脚本<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/1123881/1650891447929-217f8b7c-3eb3-4429-9aca-d8494f9b0f12.png#clientId=ub66d34b1-4391-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=71&id=uda14f896&margin=%5Bobject%20Object%5D&name=image.png&originHeight=141&originWidth=916&originalType=binary&ratio=1&rotation=0&showTitle=false&size=60090&status=done&style=none&taskId=u0f5514f5-8645-433c-aa53-bd6b737d56d&title=&width=458)<br />脚本中key、value可以作为参数传递。key类型参数放入keys数组,其他参数放入argv数组。<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/1123881/1650891790028-ed37fef0-1f77-4efb-8932-efea993808ff.png#clientId=ub66d34b1-4391-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=81&id=u321f2cc3&margin=%5Bobject%20Object%5D&name=image.png&originHeight=161&originWidth=1135&originalType=binary&ratio=1&rotation=0&showTitle=false&size=79811&status=done&style=none&taskId=ud4173f06-9921-4c26-9cc2-f2cabe249ea&title=&width=567.5)<br />释放锁业务流程:
  8. 1. 获取锁线程标识
  9. 1. 判断是否一致
  10. 1. 如果一致释放
  11. 1. 不一致什么都不做
  12. ```lua
  13. -- KEYS[1]是锁的key,ARGV[1]是线程标识
  14. -- 获取锁中线程标识 get key,比较线程标识与锁中标识是否一致
  15. if(redis.call('get', KEYS[1]) == ARGV[1]) then
  16. -- 释放锁 del key
  17. return redis.call('del', KEYS[1])
  18. end
  19. return 0

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

需求:基于Lua脚本实现分布式锁释放锁逻辑
提示:RedisTemplate调用Lua脚本API如下
image.png

  1. private static final DefaultRedisScript<Long> 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. @Override
  8. public void unlock(){
  9. //调用lua脚本
  10. stringRedisTemplate.execute(UNLOCK_SCRIPT,
  11. Collections.singletonList(KEY_PREFIX + name),
  12. ID_PREFIX + Thread.currentThread().getId());
  13. }

Redisson功能介绍

基于setnx实现的分布式锁问题:
image.png
Redisson
一个在Redis基础上实现的Java驻内存数据网格。不仅提供一系列分布式Java对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现。

Redissson快速入门

  1. 引入依赖

    1. <!--redisson-->
    2. <dependency>
    3. <groupId>org.redisson</groupId>
    4. <artifactId>redisson</artifactId>
    5. <version>3.13.6</version>
    6. </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://localhost:6379");
    8. // 创建RedissonClient对象
    9. return Redisson.create(config);
    10. }
    11. }
  3. 使用Redisson分布式锁

    1. @Resource
    2. private RedissonClient redissonClient;
    3. @Transactional
    4. public Result createVoucherOrder(Long voucherId) {
    5. //一人一单
    6. Long userId = UserHolder.getUser().getId();
    7. //创建锁对象
    8. RLock redisLock = redissonClient.getLock("lock:order:" + userId);
    9. //尝试获取锁
    10. boolean isLock = redisLock.tryLock();
    11. if(!isLock){
    12. //获取锁失败
    13. return Result.fail("不允许重复下单!");
    14. }
    15. try {
    16. ...
    17. ...
    18. //7.返回订单id
    19. return Result.ok(orderId);
    20. } finally {
    21. redisLock.unlock();
    22. }
    23. }

    Redisson可重入锁原理

    内部写个计数器
    image.png
    image.png

    Redisson锁重试和WatchDog机制

    反复听->源码课https://www.bilibili.com/video/BV1cr4y1671t?p=67&spm_id_from=pageDriver
    RedissonLock.java
    image.png

  • 可重入:利用hash结构记录线程id和重入次数
  • 可重试:利用信号量和PubSub实现等待、唤醒、获取锁失败的重试机制
  • 超时续约:利用watchDog,每隔一段时间,重置超时时间

    Redisson的multiLock原理

    image.png
    image.png
    原理:多个独立的Redis节点,必须在所有节点都获取重入锁,才算获取锁成功。