当我们在单机情况下,遇到并发问题,可以使用juc包下的lock锁,或者synchronized关键字来加锁。但是这俩都是JVM级别的锁,如果跨了JVM这两个锁就不能控制并发问题了,也就是说在分布式集群环境中,需要寻求其他方法来解决并发问题。前面也说到可以使用redis的setnx操作,如果不存在则set,如果存在则不set。也就是说每个服务实例都对同一个key进行操作。谁能set成功就认为获取到了锁。可以执行下面的操作。执行完之后释放锁。如下按照上述逻辑来简单实现一个分布式锁:

package com.nijunyang.redis.lock;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;/*** Description:* Created by nijunyang on 2020/3/17 23:53*/
@RestController
public class LockController {@AutowiredValueOperations<String, Object> valueOperations;@AutowiredRedisTemplate<String, Object> redisTemplate;String lock = "lock";String quantityKey = "quantity";@GetMapping("/deduct-stock")public String deductStock() {try {boolean getLock = valueOperations.setIfAbsent(lock, 1);if (!getLock) {return "没有获取到锁";}//使用当做数据库,只是模拟扣减库存场景,因此不使用原子操作Integer quantity = (Integer) valueOperations.get(quantityKey);if (quantity > 0) {--quantity;valueOperations.set(quantityKey, quantity);System.out.println("扣减库存成功,剩余库存: " + quantity);} else {System.out.println("扣减库存成功,剩余库存: " + quantity);}return "true";} finally {redisTemplate.delete(lock);}}}

如果不出意外这个锁是可以用的,但是如果拿到锁之后,在执行业务的过程中,服务挂了,就会导致锁没有释放,其他服务永远无法拿到锁,因此我们可以优化一下,加锁的同时给锁设置一个过期时间,这样来保证,拿到锁在执行业务的时候挂了,到了过期时间之后,其他服务一样可以继续获取锁。

package com.nijunyang.redis.lock;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;import java.util.concurrent.TimeUnit;/*** Description:* Created by nijunyang on 2020/3/17 23:53*/
@RestController
public class LockController {@AutowiredValueOperations<String, Object> valueOperations;@AutowiredRedisTemplate<String, Object> redisTemplate;String lock = "lock";String quantityKey = "quantity";@GetMapping("/deduct-stock")public String deductStock() {try {//设置值,并且设置超时时间boolean getLock = valueOperations.setIfAbsent(lock, 1, 10, TimeUnit.SECONDS);if (!getLock) {return "没有获取到锁";}//使用当做数据库,只是模拟扣减库存场景,因此不使用原子操作Integer quantity = (Integer) valueOperations.get(quantityKey);if (quantity > 0) {--quantity;valueOperations.set(quantityKey, quantity);System.out.println("扣减库存成功,剩余库存: " + quantity);} else {System.out.println("扣减库存成功,剩余库存: " + quantity);}return "true";} finally {redisTemplate.delete(lock);}}}

但是问题又来了,这个超时时间设置多大合适呢,如果网络延迟或者出现了sql的慢查询等,导致业务还没执行完,锁就过期了,这个时候别的服务又拿到了锁,现在并发问题问题又来了。。。A1服务拿到锁,设置过期时间10s,但是业务逻辑需要15s才能执行完,10s过后锁自动释放,这时候A2服务拿到锁执行业务,5s之后A1执行完业务删除锁,但是这个时候A1释放的是A2加的锁,A2这个时候才执行5s,等到A2执行完去释放的又是别的服务拿到的锁,如此恶心循环。。。。

这里整理了一些redis的相关学习资料,需要的朋友可以可以关注公众号:Linux服务器 回复 redis获取。

Linux服务器开发高级架构qun:720209036。   

我们可以将锁的value设置成一个客户端的唯一值,比如生成一个UUID,删除的时候判断一下这个值是否是自己生成,这样就可以避免把其他服务加的锁删掉。

package com.nijunyang.redis.lock;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;import java.util.UUID;
import java.util.concurrent.TimeUnit;/*** Description:* Created by nijunyang on 2020/3/17 23:53*/
@RestController
public class LockController {@AutowiredValueOperations<String, Object> valueOperations;@AutowiredRedisTemplate<String, Object> redisTemplate;String lock = "lock";String quantityKey = "quantity";@GetMapping("/deduct-stock")public String deductStock() {String uuid = UUID.randomUUID().toString();try {//设置值,并且设置超时时间boolean getLock = valueOperations.setIfAbsent(lock, uuid, 10, TimeUnit.SECONDS);
//            boolean getLock = valueOperations.setIfAbsent(lock, 1);if (!getLock) {return "没有获取到锁";}//使用当做数据库,只是模拟扣减库存场景,因此不使用原子操作Integer quantity = (Integer) valueOperations.get(quantityKey);if (quantity > 0) {--quantity;valueOperations.set(quantityKey, quantity);System.out.println("扣减库存成功,剩余库存: " + quantity);} else {System.out.println("扣减库存成功,剩余库存: " + quantity);}return "true";} finally {//删除之前判断是否是自己加的锁if (uuid.equals(valueOperations.get(lock))) {redisTemplate.delete(lock);}}}}

这样只是保证自己的锁不被别人删掉,但是这个判断再删除的操作也不是原子操作,同时超时的问题还是没有解决。怎么办呢,我们给锁续命,可以在加锁的同时再起一个定时任务,去检查锁是否释放,如果没有释放就增加超时时间,然后再去定时检查,直到锁被删除了。比如锁超时时间10s,那么定时任务在8s后去检查,锁是否被释放,如果没有释放则重新设置超时时间。继续监视锁是否释放。

如果我们自己按照这个逻辑去实现,有可能还会有很多bug。Redisson已经帮我们很好的实现了分布式锁。配置好之后,使用就像使用java的lock一样。原理就和上述差不多。

加依赖:

<dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.6.5</version>
</dependency>

写配置:

@Bean
public Redisson redisson() {Config config = new Config();config.useSingleServer().setAddress("redis://127.0.0.1:6379").setPassword("xxx");/*** 哨兵*///config.useSentinelServers().addSentinelAddress("");/*** 集群*///config.useClusterServers().addNodeAddress("redis://111.229.53.45:6379");return (Redisson) Redisson.create(config);
}

使用:

package com.nijunyang.redis.lock;import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;import java.util.UUID;
import java.util.concurrent.TimeUnit;/*** Description:* Created by nijunyang on 2020/3/17 23:53*/
@RestController
public class LockController {@AutowiredValueOperations<String, Object> valueOperations;@AutowiredRedisTemplate<String, Object> redisTemplate;@AutowiredRedisson redisson;String lockKey = "lockKey";String quantityKey = "quantity";@GetMapping("/deduct-stock2")public String deductStock2() {RLock redissonLock = redisson.getLock(lockKey);try {redissonLock.lock();//使用当做数据库,只是模拟扣减库存场景,因此不使用原子操作Integer quantity = (Integer) valueOperations.get(quantityKey);if (quantity > 0) {--quantity;valueOperations.set(quantityKey, quantity);System.out.println("扣减库存成功,剩余库存: " + quantity);return "true";} else {System.out.println("扣减库存成功,剩余库存: " + quantity);return "false";}} finally {redissonLock.unlock();}}}

和JUC包里面Lock锁的使用一模一样,有木有?

Redisson锁源码逻辑简要分析,直接在代码中加的注释说明,里面大量使用lua脚本来封装redis操作的原子性,上面提到的判断再删除的操作,也可以写成lua脚本执行,保证原子性。同时lua脚本中如果出错了,数据还会回滚。

虽然看起来已经很完善了,但是还有一点点问题如果哨兵模式,或者集群模式,锁加载master上面,还未同步到slave的时候,master挂了,这个重新选举,新的master上面是没有加锁的。不过这种几率已经很小很小了,如果是在要求强一致性,那么就只有选择zookeeper来实现,因为zookeeper是强一致性的,它是多数节点数据都同步好了才返回。Master挂了,选举也是在数据一致的节点中,因此重新选上来leader肯定是有锁的。当然ZK的性能肯定就没有redis的高了,怎么选择还是看自己业务是否允许。

Redisson也提供了一个RedissonRedLock,传入多个锁对象,加锁的时候,多个锁都加上才认为加锁成功。但是这样需要连接多个redis。这样肯定是有性能问题的,还有网络问题等等。

浅析Redis分布式锁——从自己实现到Redisson的实现相关推荐

  1. Redis 作者 Antirez 讲如何实现分布式锁?Redis 实现分布式锁天然的缺陷分析Redis分布式锁的正确使用姿势!...

    Redis分布式锁基本原理 采用 redis 实现分布式锁,主要是利用其单线程命令执行的特性,一般是 setnx, 只会有一个线程会执行成功,也就是只有一个线程能成功获取锁:看着很完美. 然而-- 看 ...

  2. Redis分布式锁原理(一)——redis分布式锁需要注意的问题

    下一篇:Redis分布式锁原理(二)--Redisson分布式锁源码浅析 虽然目前Redisson框架已经帮我们封装好了分布式锁的实现逻辑,我们可以直接像调用本地锁一样使用即可,但本文并不直接剖析Re ...

  3. Redis分布式锁的实现原理看这篇就够了~

    2019独角兽企业重金招聘Python工程师标准>>> 一.写在前面 现在面试,一般都会聊聊分布式系统这块的东西.通常面试官都会从服务框架(Spring Cloud.Dubbo)聊起 ...

  4. 面试必问:如何实现Redis分布式锁

    摘要:今天我们来聊聊分布式锁这块知识,具体的来看看Redis分布式锁的实现原理. 一.写在前面 现在面试,一般都会聊聊分布式系统这块的东西.通常面试官都会从服务框架(Spring Cloud.Dubb ...

  5. redis 分布式锁 看门狗_redis分布式锁原理及实现

    一.写在前面 现在面试,一般都会聊聊分布式系统这块的东西.通常面试官都会从服务框架(Spring Cloud.Dubbo)聊起,一路聊到分布式事务.分布式锁.ZooKeeper等知识. 所以咱们这篇文 ...

  6. 死磕 java同步系列之redis分布式锁进化史

    问题 (1)redis如何实现分布式锁? (2)redis分布式锁有哪些优点? (3)redis分布式锁有哪些缺点? (4)redis实现分布式锁有没有现成的轮子可以使用? 简介 Redis(全称:R ...

  7. java redis的同步_java同步系列之redis分布式锁进化史

    标题: 死磕 java同步系列之redis分布式锁进化史 - 彤哥读源码 - 博客园 转帖原地址: https://www.cnblogs.com/tong-yuan/p/11621361.html ...

  8. redis分布式锁原理及实现

    一.写在前面 现在面试,一般都会聊聊分布式系统这块的东西.通常面试官都会从服务框架(Spring Cloud.Dubbo)聊起,一路聊到分布式事务.分布式锁.ZooKeeper等知识. 所以咱们这篇文 ...

  9. Redis分布式锁浅析

    redis有一个命令: set key value nx: 该命令的用处是,只有当redis中不含key时,才能set成功. 基于以上原理,可以设计分布式锁. 分布式锁可用于防止redis缓存击穿,也 ...

最新文章

  1. c语言中eof_C语言的标准 “输入输出”!今天是你学C语言的第几天?
  2. Css:背景色透明,内容不透明之终极方法!兼容所有浏览器
  3. Dojo学习笔记(六):dojo/_base/declare
  4. 关于合并“.a”文件时遇到的问题
  5. windows tensorrt python
  6. mysql对其他IP授权访问
  7. .NET正则表达式使用高级技巧之替换类
  8. php网页生命周期函数,PHP的生命周期
  9. 大数据:Parquet文件存储格式
  10. 一图看懂 ASP.NET Core 中的服务生命周期
  11. AddStaticMeshComponent
  12. vueJs的简单入门以及基础语法
  13. Windows10下手工强制清理删掉安装版的JRE8导致java.exe无法运行的解决办法
  14. 在学Python前学Linux,Python原来这么好学-1.2节: 在Linux中安装python
  15. 【转】JAVA 读写二进制文件
  16. python无头浏览器兼容问题_docker+python无头浏览器爬虫
  17. DAO层和Service层的究极理解--这波我在大气层
  18. leaflet 常用方法总结
  19. linux的xshell怎么保存密码,Xshell保存账号密码方法
  20. linux安装project lemon测评机

热门文章

  1. hp服务器装系统用usb启动不了怎么办,惠普电脑u盘装不了GHOST系统怎么办
  2. 笔记本连接投影仪常见问题及解决方案
  3. html css animate,animate.css
  4. [Audio]硬件设备
  5. 碟片在台式计算机无法识别,谁知道CD-R光碟为何在电脑不能读取?
  6. Goolge浏览器预览markdown文件
  7. Python Scrapy 爬取煎蛋网妹子图实例(一)
  8. 数据恢复相关注意事项
  9. 网络安全——身份认证与PKI原理
  10. 一 springcloud hystrix源码深度分析