当前位置:   article > 正文

Redis实现分布式锁_redis分布式锁

redis分布式锁

 


前言

随着时代的发展,分布式系统的运用越来越多,而在分布式系统中,本地锁已经无法解决数据安全问题,分布式锁能够很好的解决这个问题.


 

一、分布式锁是什么?

在分布式系统中,由于多个节点同时访问一个资源,可能会出现脏数据、数据冲突等问题,分布式锁通过加锁、解锁的方式,保证在同一时刻只有一个节点能够访问该资源,从而避免了数据冲突和错误操作。分布式锁的实现方式有很多种,常见的包括基于Redis、Zookeeper、数据库等分布式系统的实现方式。这里主要介绍Redis的方式

二、本地锁示例

1.本地锁代码示例:

  1. //controller层
  2. @GetMapping("/testLock")
  3. public Result testLock(){
  4. testService.testLock1();
  5. return Result.ok();
  6. }
  7. //service层
  8. public synchronized void testLock1(){
  9. String num = redisTemplate.opsForValue().get("num").toString();
  10. if (!StringUtils.isEmpty(num)){
  11. int i = Integer.parseInt(num);
  12. redisTemplate.opsForValue().set("num",String.valueOf(++i));
  13. }
  14. }

2.开启两个相同的服务 模拟分布式(代码一致,端口号不一致)开启网关作为统一访问路径

进行负载均衡

7ac385542057480cadc727b286fdcbaa.png

3.利用ab进行网关压力测试

e44155360a2e46b5a39ec474bba19c16.png

 4.拿到redis中num的值

e5c5c3ad1cc941db86a10e006948f48f.png

从上述实验可以发现:我们进行了1000次请求发送给网关,而num最终的值等于613,而不是我们想要看到的1000,因此可以发现,在分布式系统里,本地锁无法解决数据安全问题,这主要是由于分布式系统中存在多个节点,每个节点拥有自己的本地资源和本地锁。当多个请求同时访问同一份数据时,就会出现数据的并发访问和修改,而本地锁只能控制本地的并发访问,无法控制分布式系统中其他节点的并发访问

三、分布式锁的使用

1.前言:因为分布式集群系统微服务多分布在不同的机器上,这使得原来单机部署下的并发控制锁失效,单纯的javaAPI无法实现分布式锁,因此我们需要一种可以跨JVM的方式来控制共享数据的访问

可以利用Redis中的setnx操作来实现分布式锁

2.setnx有如下优点

2.1.setnx是一个原子性操作,只有一个客户端设置键值能成功,其他客户端再来设置,均会失效

2.2.在分布式环境下可以把setnx这个操作当作锁,如果一个客户端已经获取到锁,那么它将会返回true,就可以往下执行业务逻辑,在这个时候其他客户端又想来获取这把锁就会返回false

3.使用步骤

3.1.导入依赖 写配置文件

  1. <!-- redis -->
  2. <dependency>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter-data-redis</artifactId>
  5. </dependency>
  6. <!-- spring2.X集成redis所需common-pool2-->
  7. <dependency>
  8. <groupId>org.apache.commons</groupId>
  9. <artifactId>commons-pool2</artifactId>
  10. </dependency>
  1. spring:
  2. redis:
  3. host: 192.168.72.166
  4. port: 6379
  5. database: 0
  6. timeout: 1800000
  7. password:
  8. lettuce:
  9. pool:
  10. max-active: 20 #最大连接数
  11. max-wait: -1 #最大阻塞等待时间(负数表示没限制)
  12. max-idle: 5 #最大空闲
  13. min-idle: 0 #最小空闲

3.2.代码实现

  1. //controller层
  2. @GetMapping("/testLock")
  3. public Result testLock(){
  4. testService.testRedisLock();
  5. return Result.ok();
  6. }
  7. //service层
  8. @Autowired
  9. private RedisTemplate redisTemplate;
  10. public void testRedisLock() {
  11. //获取锁
  12. //加上uuid防止误删除锁
  13. String uuid = System.currentTimeMillis() + UUID.randomUUID().toString().replaceAll("-", "");
  14. Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 10, TimeUnit.SECONDS);
  15. //如果获取到锁执行步骤
  16. //最后释放锁
  17. if (lock) {
  18. String num = redisTemplate.opsForValue().get("num").toString();
  19. if (!StringUtils.isEmpty(num)) {
  20. int i = Integer.parseInt(num);
  21. redisTemplate.opsForValue().set("num", String.valueOf(++i));
  22. } else {
  23. return;
  24. }
  25. //在极端情况下仍然会误删除锁
  26. //因此使用lua脚本的方式来防止误删除
  27. String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
  28. "then\n" +
  29. " return redis.call(\"del\",KEYS[1])\n" +
  30. "else\n" +
  31. " return 0\n" +
  32. "end";
  33. DefaultRedisScript defaultRedisScript = new DefaultRedisScript();
  34. defaultRedisScript.setScriptText(script);
  35. defaultRedisScript.setResultType(Long.class);
  36. redisTemplate.execute(defaultRedisScript, Arrays.asList("lock"), uuid);
  37. /* if (redisTemplate.opsForValue().get("lock").toString().equals(uuid)){
  38. redisTemplate.delete("lock");
  39. }*/
  40. } else {
  41. //如果没有获取到锁
  42. //重试
  43. try {
  44. Thread.sleep(100);
  45. testRedisLock();;
  46. } catch (InterruptedException e) {
  47. e.printStackTrace();
  48. }
  49. }
  50. }

3.3.结果截图

b675580cef2f4c808a102db2187a40ed.png

 3.4.总结 :

由此可见,使用redis中setnx的方式实现了分布式锁,解决了数据安全问题

这里有三个问题需要注意:

3.4.1.为什么要将锁加上过期时间

示例代码:

 Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 10, TimeUnit.SECONDS);

当一个请求进入到该方法里面时,正在一行行的执行业务逻辑,如果在某一行出现了问题报了异常,而这时还没有将释放锁那一行代码执行完毕,这时就会导致锁无法释放,从而导致其他请求也无法进入.而在这时如果设置了过期时间,那么就会在时间到了之后自动释放锁,其他请求就也能获取锁了

3.4.2.为什么要给锁设置UUID

示例代码:

  1. String uuid = System.currentTimeMillis() + UUID.randomUUID().toString().replaceAll("-", "");
  2. Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 10, TimeUnit.SECONDS);

这是为了防止误删锁的发生,比如业务逻辑执行的时间是7s,而锁的过期时间是3s,现在有A1和A2两个请求,A1正在执行业务逻辑,还没有执行到释放锁的那一行代码时,锁的过期时间已经超过3s了,这时A1的锁释放,A2就能拿到锁了,A2在执行的过程中,A1又执行之后的代码,最终导致A1进行了释放锁的操作,由于它们连接的是同一个redis,使用的锁的键名也相同,因此A1成功的将A2的锁给释放掉了。为了防止这种情况的发生,我们可以将每一把锁都设置唯一的UUID,这样在不同的请求到来时就会生成不同的UUID并将它存入redis中,在释放锁的时候,就可以根据UUID来判断是否是自己的锁来进行删除

3.4.3为什么要使用lua脚本

示例代码:

  1. //在极端情况下仍然会误删除锁
  2. //因此使用lua脚本的方式来防止误删除
  3. String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
  4. "then\n" +
  5. " return redis.call(\"del\",KEYS[1])\n" +
  6. "else\n" +
  7. " return 0\n" +
  8. "end";
  9. DefaultRedisScript defaultRedisScript = new DefaultRedisScript();
  10. defaultRedisScript.setScriptText(script);
  11. defaultRedisScript.setResultType(Long.class);
  12. redisTemplate.execute(defaultRedisScript, Arrays.asList("lock"), uuid);

使用lua脚本也是为了保证原子性操作,保证判断uuid是否相等和释放锁一起执行,防止极端情况的发生,我们先来看使用lua脚本前的代码是如果进行防误删的:

  1. if (redisTemplate.opsForValue().get("lock").toString().equals(uuid)){
  2. redisTemplate.delete("lock");

这里所存在的问题是删除操作缺乏原子性,我们还保持和3.4.2一样的条件,有A1和A2两个请求,假如A1在执行完了上图的if代码后,锁因为超过过期时间而被释放了,这时A2获取到锁执行业务逻辑,生成了自己的uuid,在执行的过程中,A1又接着往下进行,尽管此时两者uuid已经不同,但是由于A1已经进行过if判断,所以可以直接删除掉A2的锁。因此我们需要用lua脚本的方式,将判断和删除合为一步,保证其原子性,这样就可以解决锁的误删问题,加强锁的健壮性

4.还可以使用Redisson实现分布式锁

4.1简单介绍:

Redisson是一个在Redis的基础上实现的Java驻内存数据网格,它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。提供了使用Redis的最简单和最便捷的方法。

4.2 使用步骤

4.2.1: 导入依赖并配置RedissonClient对象,配置文件上文已给出

  1. <!--1.导入依赖 service-util -->
  2. <!-- redisson -->
  3. <dependency>
  4. <groupId>org.redisson</groupId>
  5. <artifactId>redisson</artifactId>
  6. <version>3.15.3</version>
  7. </dependency>
  8. //配置redisson
  9. package com.atguigu.gmall.common.config;
  10. @Data
  11. @Configuration
  12. @ConfigurationProperties("spring.redis")
  13. public class RedissonConfig {
  14. private String host;
  15. private String password;
  16. private String port;
  17. private int timeout = 3000;
  18. private static String ADDRESS_PREFIX = "redis://";
  19. /**
  20. * 自动装配
  21. */
  22. @Bean
  23. RedissonClient redissonSingle() {
  24. Config config = new Config();
  25. if(StringUtils.isEmpty(host)){
  26. throw new RuntimeException("host is empty");
  27. }
  28. SingleServerConfig serverConfig = config.useSingleServer()
  29. .setAddress(ADDRESS_PREFIX + this.host + ":" + port)
  30. .setTimeout(this.timeout);
  31. if(!StringUtils.isEmpty(this.password)) {
  32. serverConfig.setPassword(this.password);
  33. }
  34. return Redisson.create(config);
  35. }
  36. }

4.2.2.实现代码

  1. public void testRedissonLock() {
  2. //获取锁
  3. RLock lock = redissonClient.getLock("lock");
  4. //开始加锁
  5. try {
  6. boolean tryLock = lock.tryLock(100, 10, TimeUnit.SECONDS);
  7. if (tryLock){
  8. String num = redisTemplate.opsForValue().get("num").toString();
  9. if (!StringUtils.isEmpty(num)) {
  10. int i = Integer.parseInt(num);
  11. redisTemplate.opsForValue().set("num", String.valueOf(++i));
  12. } else {
  13. return;
  14. }
  15. }
  16. } catch (InterruptedException e) {
  17. e.printStackTrace();
  18. }finally {
  19. if (lock.isLocked()){
  20. lock.unlock();
  21. }
  22. }
  23. }

 

 


总结

以上就是今天所总结的内容,主要学习的是分布式锁的实现,希望大神指正哪里有错误之处!!!

 

 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Gausst松鼠会/article/detail/696675
推荐阅读
相关标签
  

闽ICP备14008679号