Spring boot 分布式锁 优化分布式锁

向右看齐 2024-04-07 10:10 122阅读 0赞

Spring boot 分布式锁 优化分布式锁

一:准备工作

1.配置文件

  1. server:
  2. port: 8080
  3. servlet:
  4. session:
  5. timeout: 30m
  6. spring:
  7. application:
  8. name: spring-boot-redis
  9. cache:
  10. # 使用了Spring Cache后,能指定spring.cache.type就手动指定一下,虽然它会自动去适配已有Cache的依赖,但先后顺序会对Redis使用有影响(JCache -> EhCache -> Redis -> Guava)
  11. type: REDIS
  12. redis:
  13. host: 192.168.0.1
  14. port: 6379
  15. password: 123
  16. # 连接超时时间(ms)
  17. timeout: 10000
  18. # Redis默认情况下有16个分片,这里配置具体使用的分片,默认是0
  19. database: 0
  20. lettuce:
  21. pool:
  22. # 连接池最大连接数(使用负值表示没有限制) 默认 8
  23. max-active: 100
  24. # 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1
  25. max-wait: -1
  26. # 连接池中的最大空闲连接 默认 8
  27. max-idle: 8
  28. # 连接池中的最小空闲连接 默认 0
  29. min-idle: 0

2.pox文件

  1. //为了方便测试 使用了web-starter
  2. <dependency>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter-web</artifactId>
  5. </dependency>
  6. //以下三个必须
  7. <dependency>
  8. <groupId>org.springframework.boot</groupId>
  9. <artifactId>spring-boot-starter-data-redis</artifactId>
  10. </dependency>
  11. <dependency>
  12. <groupId>org.apache.commons</groupId>
  13. <artifactId>commons-pool2</artifactId>
  14. </dependency>
  15. <dependency>
  16. <groupId>org.springframework.session</groupId>
  17. <artifactId>spring-session-data-redis</artifactId>
  18. </dependency>

二:测试

1.contoller层代码

分布式锁测试控制层

  1. import org.springframework.beans.factory.annotation.Autowired;
  2. import org.springframework.data.redis.core.RedisTemplate;
  3. import org.springframework.util.StringUtils;
  4. import org.springframework.web.bind.annotation.GetMapping;
  5. import org.springframework.web.bind.annotation.RestController;
  6. /**
  7. * @author WJL
  8. * @version 1.0
  9. * @Date: 2022/8/20 23:18
  10. * @功能描述
  11. **/
  12. @RestController
  13. public class RedisTestController {
  14. @Autowired
  15. private RedisTemplate redisTemplate;
  16. @GetMapping("/testLock")
  17. public void testLock(){
  18. //1获取锁,setne
  19. Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111",3, TimeUnit.SECONDS);
  20. //2获取锁成功、查询num的值
  21. if(lock){
  22. Object value = redisTemplate.opsForValue().get("num");
  23. //2.1判断num为空return
  24. if(StringUtils.isEmpty(value)){
  25. return;
  26. }
  27. //2.2有值就转成成int
  28. int num = Integer.parseInt(value+"");
  29. //2.3把redis的num加1
  30. redisTemplate.opsForValue().set("num", ++num);
  31. //2.4释放锁,del
  32. redisTemplate.delete("lock");
  33. }else{
  34. //3获取锁失败、每隔0.1秒再获取
  35. try {
  36. Thread.sleep(100);
  37. testLock();
  38. } catch (InterruptedException e) {
  39. e.printStackTrace();
  40. }
  41. }
  42. }
  43. }

要先在服务器的redis中 set num “0”

2.服务区测试效果

使用 ab 测试工具:httpd-tools(yum install -y httpd-tools)

ab -n(一次发送的请求数) -c(请求的并发数) 访问路径

测试如下:5000请求,100并发
ab -n 5000 -c 100 http://127.0.0.1:8206/testLock

在这里插入图片描述

在这里插入图片描述

可以看到此时的num变成1000

三:小结 误删

1.代码执行的流程梳理 例如 A先操作 1.上锁 2.具体操作 。假设服务器在A操作的时候卡顿,此时的时间已经过了设置的过期时间 3.自动释放key B抢到锁 1.进行上锁 2.进行具体操作 当A服务器反应过来的时候 继续进行操作,需要手动释放锁,但此时的锁在B当中,此时B操作没有完成就被释放B的锁!

2.解决方案 使用UUID防止误删 ,用uuid来标识不同的操作

在这里插入图片描述

四:优化 uuid来标识不同的操作

主要是在生成锁的时候加上uuid

在释放锁的时候进行一个uuid的校验

  1. String uuid= UUID.randomUUID().toString();
  2. //1获取锁,setne
  3. Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid , 3, TimeUnit.SECONDS);
  4. //2.4释放锁,del
  5. //判断比较uuid值是否一样
  6. if(uuid.equals(redisTemplate.opsForValue().get("lock"))){
  7. redisTemplate.delete("lock");
  8. }

五:使用lua脚本继续优化

删除操作具有原子性

问题:A会释放B的锁

在这里插入图片描述

解决的方案

有一定的原子性,不会被其他命令插队,可以完成一些redis事务性的操作

  1. @GetMapping("testLockLua")
  2. public void testLockLua() {
  3. //1 声明一个uuid ,将做为一个value 放入我们的key所对应的值中
  4. String uuid = UUID.randomUUID().toString();
  5. //2 定义一个锁:lua 脚本可以使用同一把锁,来实现删除!
  6. String skuId = "25"; // 访问skuId 为25号的商品 100008348542
  7. String locKey = "lock:" + skuId; // 锁住的是每个商品的数据
  8. // 3 获取锁
  9. Boolean lock = redisTemplate.opsForValue().setIfAbsent(locKey, uuid, 3, TimeUnit.SECONDS);
  10. // 第一种: lock 与过期时间中间不写任何的代码。
  11. // redisTemplate.expire("lock",10, TimeUnit.SECONDS);//设置过期时间
  12. // 如果true
  13. if (lock) {
  14. // 执行的业务逻辑开始
  15. // 获取缓存中的num 数据
  16. Object value = redisTemplate.opsForValue().get("num");
  17. // 如果是空直接返回
  18. if (StringUtils.isEmpty(value)) {
  19. return;
  20. }
  21. // 不是空 如果说在这出现了异常! 那么delete 就删除失败! 也就是说锁永远存在!
  22. int num = Integer.parseInt(value + "");
  23. // 使num 每次+1 放入缓存
  24. redisTemplate.opsForValue().set("num", String.valueOf(++num));
  25. /*使用lua脚本来锁*/
  26. // 定义lua 脚本
  27. String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
  28. // 使用redis执行lua执行
  29. DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
  30. redisScript.setScriptText(script);
  31. // 设置一下返回值类型 为Long
  32. // 因为删除判断的时候,返回的0,给其封装为数据类型。如果不封装那么默认返回String 类型,
  33. // 那么返回字符串与0 会有发生错误。
  34. redisScript.setResultType(Long.class);
  35. // 第一个要是script 脚本 ,第二个需要判断的key,第三个就是key所对应的值。
  36. redisTemplate.execute(redisScript, Arrays.asList(locKey), uuid);
  37. } else {
  38. // 其他线程等待
  39. try {
  40. // 睡眠
  41. Thread.sleep(1000);
  42. // 睡醒了之后,调用方法。
  43. testLockLua();
  44. } catch (InterruptedException e) {
  45. e.printStackTrace();
  46. }
  47. }
  48. }

六:总结

为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件

  • 互斥性。在任意时刻,只有一个客户端能持有锁。
  • 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
  • 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
  • 加锁和解锁必须具有原子性。

发表评论

表情:
评论列表 (有 0 条评论,122人围观)

还没有评论,来说两句吧...

相关阅读

    相关 spring boot redis分布式

    随着现在分布式架构越来越盛行,在很多场景下需要使用到分布式锁。分布式锁的实现有很多种,比如基于[数据库][Link 1]、zookeeper等,本文主要介绍使用Redis做分布