Redis 官方文档
点击查看 Redis 中文官方文档
举个案例
高并发秒杀场景中,看一下一段代码,会发生哪些问题?
@Slf4j
@RestController
public class GoodController {
private static final String KEY = "goods:001";
@Autowired
private StringRedisTemplate stringRedisTemplate;
@RequestMapping("/goods")
public String buyGoods() {
String result = stringRedisTemplate.opsForValue().get(KEY);
int num = Objects.isNull(result) ? 0 : Integer.parseInt(result);
if (num > 0) {
int realNum = num - 1;
stringRedisTemplate.opsForValue().set(KEY,Integer.toString(realNum));
log.info(">>>>>>>>>>你已经成功秒杀了商品,还剩余{}件",realNum);
return "你已经成功秒杀了商品,还剩余"+realNum+"件";
}
log.info(">>>>>>>>>>活动已经售罄,欢迎下次光临.");
return "活动已经售罄,欢迎下次光临";
}
}
肯定会发生超卖(重复卖同一件商品)问题,所以这里可以加锁保证数据安全性,可以使用 Synchornized、ReentantLock 锁,但是推荐使用 ReentantLock 锁,它有一个 tryLock() 方法可以尝试加锁,不会死等。从而可以做一些自己的业务操作,改进之后如下:
@RequestMapping("/goods2")
public String buyGoods() {
if (lock.tryLock()) {
try {
String result = stringRedisTemplate.opsForValue().get(KEY);
int num = Objects.isNull(result) ? 0 : Integer.parseInt(result);
if (num > 0) {
int realNum = num - 1;
stringRedisTemplate.opsForValue().set(KEY, Integer.toString(realNum));
log.info(">>>>>>>>>>你已经成功秒杀了商品,还剩余{}件", realNum);
return "你已经成功秒杀了商品,还剩余" + realNum + "件";
}
log.info(">>>>>>>>>>活动已经售罄,欢迎下次光临.");
return "活动已经售罄,欢迎下次光临";
} finally {
lock.unlock();
}
} else {
// do_something....
}
return "程序结束";
}
测试效果如下:
2022-09-14 11:10:26.028 INFO 92342 --- [o-9292-exec-120] com.gwm.cloud.redislock.GoodController2 : >>>>>>>>>>你已经成功秒杀了商品,还剩余9件
2022-09-14 11:10:26.125 INFO 92342 --- [o-9292-exec-185] com.gwm.cloud.redislock.GoodController2 : >>>>>>>>>>你已经成功秒杀了商品,还剩余8件
2022-09-14 11:10:26.148 INFO 92342 --- [o-9292-exec-109] com.gwm.cloud.redislock.GoodController2 : >>>>>>>>>>你已经成功秒杀了商品,还剩余7件
2022-09-14 11:10:26.157 INFO 92342 --- [o-9292-exec-179] com.gwm.cloud.redislock.GoodController2 : >>>>>>>>>>你已经成功秒杀了商品,还剩余6件
2022-09-14 11:10:26.183 INFO 92342 --- [o-9292-exec-120] com.gwm.cloud.redislock.GoodController2 : >>>>>>>>>>你已经成功秒杀了商品,还剩余5件
2022-09-14 11:10:26.189 INFO 92342 --- [io-9292-exec-93] com.gwm.cloud.redislock.GoodController2 : >>>>>>>>>>你已经成功秒杀了商品,还剩余4件
2022-09-14 11:10:26.192 INFO 92342 --- [io-9292-exec-74] com.gwm.cloud.redislock.GoodController2 : >>>>>>>>>>你已经成功秒杀了商品,还剩余3件
2022-09-14 11:10:26.198 INFO 92342 --- [io-9292-exec-67] com.gwm.cloud.redislock.GoodController2 : >>>>>>>>>>你已经成功秒杀了商品,还剩余2件
2022-09-14 11:10:26.203 INFO 92342 --- [o-9292-exec-192] com.gwm.cloud.redislock.GoodController2 : >>>>>>>>>>你已经成功秒杀了商品,还剩余1件
2022-09-14 11:10:26.207 INFO 92342 --- [io-9292-exec-43] com.gwm.cloud.redislock.GoodController2 : >>>>>>>>>>你已经成功秒杀了商品,还剩余0件
2022-09-14 11:10:26.210 INFO 92342 --- [o-9292-exec-110] com.gwm.cloud.redislock.GoodController2 : >>>>>>>>>>活动已经售罄,欢迎下次光临.
2022-09-14 11:10:26.212 INFO 92342 --- [io-9292-exec-89] com.gwm.cloud.redislock.GoodController2 : >>>>>>>>>>活动已经售罄,欢迎下次光临.
以上虽然能够解决单机在 Redis 上的安全行,但是如果是分布式微服务就不能保证了,所以最终还是要使用 Redis 来做分布式锁。
那么这里就是用 Nginx 做负载均衡转发。
安装和配置 nginx 请看:nginx 安装 nginx配置
重新修改后代码如下:
@RequestMapping("/goods4")
public String buyGoods() {
// 加上分布式锁
String randStr = UUID.randomUUID().toString();
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_KEY, randStr);
if (!flag) {
return "抢锁失败,请进行重试...";
}
String result = stringRedisTemplate.opsForValue().get(KEY);
int num = Objects.isNull(result) ? 0 : Integer.parseInt(result);
if (num > 0) {
int realNum = num - 1;
stringRedisTemplate.opsForValue().set(KEY, Integer.toString(realNum));
log.info(">>>>>>>>>>你已经成功秒杀了商品,还剩余{}件,serverPort={}", realNum,serverPort);
// 正常执行业务之后直接释放锁
stringRedisTemplate.delete(REDIS_KEY);
return "你已经成功秒杀了商品,还剩余" + realNum + "件"+serverPort;
}
log.info(">>>>>>>>>>活动已经售罄,欢迎下次光临."+serverPort);
return "活动已经售罄,欢迎下次光临"+serverPort;
}
改成用分布式锁来保证数据安全性,但是这里还有一个问题,就是如果业务执行不正常或者不没有释放锁,就会导致所有请求执行不了,所以必须要把释放锁这个操作放到 finally 操作快里面执行,无论如何都要释放掉锁。
那么重新改进后的代码如下:
@RequestMapping("/goods5")
public String buyGoods() {
try {
// 加上分布式锁
String randStr = UUID.randomUUID().toString();
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_KEY, randStr);
if (!flag) {
return "抢锁失败,请进行重试...";
}
String result = stringRedisTemplate.opsForValue().get(KEY);
int num = Objects.isNull(result) ? 0 : Integer.parseInt(result);
if (num > 0) {
int realNum = num - 1;
stringRedisTemplate.opsForValue().set(KEY, Integer.toString(realNum));
log.info(">>>>>>>>>>你已经成功秒杀了商品,还剩余{}件,serverPort={}", realNum,serverPort);
return "你已经成功秒杀了商品,还剩余" + realNum + "件"+serverPort;
}
log.info(">>>>>>>>>>活动已经售罄,欢迎下次光临."+serverPort);
} finally {
// 正常执行业务之后直接释放锁
stringRedisTemplate.delete(REDIS_KEY);
}
return "活动已经售罄,欢迎下次光临"+serverPort;
}
但是发现这都是正常操作所以可以保证正常释放掉锁,此时如果程序刚好执行到 try 代码块的时候,还没来得及执行 finally 语句块,然后就停电了关机了,那么此时也就释放不了这把锁。
所以这里还需要对锁加入失效时间,保证宕机之后能够正常释放掉锁。修改过后的代码如:
@RequestMapping("/goods5")
public String buyGoods() {
try {
// 加上分布式锁
String randStr = UUID.randomUUID().toString();
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_KEY, randStr);
// 加上锁过期时间
stringRedisTemplate.expire(REDIS_KEY,30L, TimeUnit.SECONDS);
if (!flag) {
return "抢锁失败,请进行重试...";
}
String result = stringRedisTemplate.opsForValue().get(KEY);
int num = Objects.isNull(result) ? 0 : Integer.parseInt(result);
if (num > 0) {
int realNum = num - 1;
stringRedisTemplate.opsForValue().set(KEY, Integer.toString(realNum));
log.info(">>>>>>>>>>你已经成功秒杀了商品,还剩余{}件,serverPort={}", realNum,serverPort);
return "你已经成功秒杀了商品,还剩余" + realNum + "件"+serverPort;
}
log.info(">>>>>>>>>>活动已经售罄,欢迎下次光临."+serverPort);
} finally {
// 正常执行业务之后直接释放锁
stringRedisTemplate.delete(REDIS_KEY);
}
return "活动已经售罄,欢迎下次光临"+serverPort;
}
加上了过期时间,但是还是有个问题,那就是加锁和设置锁失效时间不是一个原子操作,如果加锁成功,然后又停电宕机了,没有给锁设置过期时间,那么锁又会释放不成功。
将加锁和设置锁失效时间两个步骤换成一个操作,如下所示:
@RequestMapping("/goods6")
public String buyGoods() {
try {
String randStr = UUID.randomUUID().toString();
// 加上分布式锁、设置锁过期时间
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_KEY, randStr,30L, TimeUnit.SECONDS);
if (!flag) {
return "抢锁失败,请进行重试...";
}
String result = stringRedisTemplate.opsForValue().get(KEY);
int num = Objects.isNull(result) ? 0 : Integer.parseInt(result);
if (num > 0) {
int realNum = num - 1;
stringRedisTemplate.opsForValue().set(KEY, Integer.toString(realNum));
log.info(">>>>>>>>>>你已经成功秒杀了商品,还剩余{}件,serverPort={}", realNum,serverPort);
return "你已经成功秒杀了商品,还剩余" + realNum + "件"+serverPort;
}
log.info(">>>>>>>>>>活动已经售罄,欢迎下次光临."+serverPort);
} finally {
// 正常执行业务之后直接释放锁
stringRedisTemplate.delete(REDIS_KEY);
}
return "活动已经售罄,欢迎下次光临"+serverPort;
}
现在已经保证了加锁的正确性了,但是还存在一个巨大的问题,就是误删问题,因为你给锁设置的超时时间是 30s,假设业务处理时间需要消耗 40s,那么此时第一个还在处理业务,超过了 30s,锁直接失效释放了,此时第二个线程就会加锁成功,等到第一个线程执行完之后,执行 finally 块释放锁,就把第二个线程的锁给释放了,这就是误删问题,非常严重。那么怎么解决呢?
是不是可以在释放前先获取到锁,然后判断这把锁是不是自己的呢?是自己的就可以释放,不是自己的就不能释放,修改之后如下:
@RequestMapping("/goods6")
public String buyGoods() {
String randStr = UUID.randomUUID().toString();
try {
// 加上分布式锁、设置锁过期时间
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_KEY, randStr,30L, TimeUnit.SECONDS);
if (!flag) {
return "抢锁失败,请进行重试...";
}
String result = stringRedisTemplate.opsForValue().get(KEY);
int num = Objects.isNull(result) ? 0 : Integer.parseInt(result);
if (num > 0) {
int realNum = num - 1;
stringRedisTemplate.opsForValue().set(KEY, Integer.toString(realNum));
log.info(">>>>>>>>>>你已经成功秒杀了商品,还剩余{}件,serverPort={}", realNum,serverPort);
return "你已经成功秒杀了商品,还剩余" + realNum + "件"+serverPort;
}
log.info(">>>>>>>>>>活动已经售罄,欢迎下次光临."+serverPort);
} finally {
// 加上锁判断、是否允许释放条件
if (stringRedisTemplate.opsForValue().get(REDIS_KEY).equals(randStr)) {
// 正常执行业务之后直接释放锁
stringRedisTemplate.delete(REDIS_KEY);
}
}
return "活动已经售罄,欢迎下次光临"+serverPort;
}
但是此时,判断锁和释放锁的步骤不是一个原子操作,还是可能存在时间先后的差异,也会导致误删锁的问题,所以需要保证判断和释放锁必须是一个原子操作,如下所示:
@RequestMapping("/goods7")
public String buyGoods() {
String randStr = UUID.randomUUID().toString();
try {
// 加上分布式锁、设置锁过期时间
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_KEY, randStr,30L, TimeUnit.SECONDS);
if (!flag) {
return "抢锁失败,请进行重试...";
}
String result = stringRedisTemplate.opsForValue().get(KEY);
int num = Objects.isNull(result) ? 0 : Integer.parseInt(result);
if (num > 0) {
int realNum = num - 1;
stringRedisTemplate.opsForValue().set(KEY, Integer.toString(realNum));
log.info(">>>>>>>>>>你已经成功秒杀了商品,还剩余{}件,serverPort={}", realNum,serverPort);
return "你已经成功秒杀了商品,还剩余" + realNum + "件"+serverPort;
}
log.info(">>>>>>>>>>活动已经售罄,欢迎下次光临."+serverPort);
} finally {
// 通过 Lua 脚本删除分布式锁的 key
Jedis jedis = RedisUtils.getJedis();
String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
"then\n" +
" return redis.call(\"del\",KEYS[1])\n" +
"else\n" +
" return 0\n" +
"end";
try {
Object result = jedis.eval(script, Collections.singletonList(REDIS_KEY), Collections.singletonList(randStr));
// 这里记得 toString() 否则不会相等
if ("1".equals(result.toString())) {
log.info(">>>>>>>>>del lock_key success!!!");
} else {
log.info(">>>>>>>>del lock_key error... ");
}
} finally {
if (Objects.nonNull(jedis)) {
// 关闭资源
jedis.close();
}
}
}
return "活动已经售罄,欢迎下次光临"+serverPort;
}
RedisUtils 工具类如下:
public class RedisUtils {
private static JedisPool jedisPool = null;
/**
* 创建 Jedis 连接池
*/
static {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(20);
jedisPoolConfig.setMaxIdle(10);
jedisPool = new JedisPool(jedisPoolConfig, "localhost", 6379);
}
/**
* 从 JedisPool 中获取到 Jedis 对象
*/
public static Jedis getJedis() {
if (Objects.nonNull(jedisPool)) {
return jedisPool.getResource();
}
throw new RuntimeException("连接池创建失败...");
}
}
使用到了传说中的 lua 脚本。点击查看 lua 脚本 或者 中文文档
if redis.call("get",KEYS[1]) == ARGV[1]
then
return redis.call("del",KEYS[1])
else
return 0
end
经过修改之后发现还是会存在一定问题,Redis 集群是 AP 模式,不像 Zookeeper 是 CP 模式,如下:
而我们的 Redis 有点像 Eureka 集群,如下:
因为 Redis 之间通过异步方式通信,所以如果 master 宕机了,并且此时 master 还没有把锁状态同步给从节点,异步复制失败,导致锁丢失。此时上面写的代码又不能保证数据安全性了,所以这个时候只能采用 RedLock,天生解决这种问题,RedLock 红锁的具体实现是交给 Redisson 、并且 Redisson 还是 Java 语言编写的。
@RequestMapping("/goods7")
public String buyGoods() {
RLock lock = redisson.getLock(REDIS_KEY);
// 加锁
lock.lock();
try {
String result = stringRedisTemplate.opsForValue().get(KEY);
int num = Objects.isNull(result) ? 0 : Integer.parseInt(result);
if (num > 0) {
int realNum = num - 1;
stringRedisTemplate.opsForValue().set(KEY, Integer.toString(realNum));
log.info(">>>>>>>>>>你已经成功秒杀了商品,还剩余{}件,serverPort={}", realNum,serverPort);
return "你已经成功秒杀了商品,还剩余" + realNum + "件"+serverPort;
}
log.info(">>>>>>>>>>活动已经售罄,欢迎下次光临."+serverPort);
} finally {
// 直接调用 unlock() 解锁即可
lock.unlock();
}
return "活动已经售罄,欢迎下次光临"+serverPort;
}
但是这里还是存在一个问题,就是解锁不能解锁别人的锁,需要判断能不能解锁才可以解锁,修改之后如下:
@RequestMapping("/goods9")
public String buyGoods() {
// 直接上 Redisson
RLock lock = redisson.getLock(REDIS_KEY);
// 加锁
lock.lock();
try {
String result = stringRedisTemplate.opsForValue().get(KEY);
int num = Objects.isNull(result) ? 0 : Integer.parseInt(result);
if (num > 0) {
int realNum = num - 1;
stringRedisTemplate.opsForValue().set(KEY, Integer.toString(realNum));
log.info(">>>>>>>>>>你已经成功秒杀了商品,还剩余{}件,serverPort={}", realNum,serverPort);
return "你已经成功秒杀了商品,还剩余" + realNum + "件"+serverPort;
}
log.info(">>>>>>>>>>活动已经售罄,欢迎下次光临."+serverPort);
} finally {
// 先判断自己是否能去释放这把锁
if (lock.isLocked() && lock.isHeldByCurrentThread()) {
// 直接调用 unlock() 解锁即可
lock.unlock();
}
}
return "活动已经售罄,欢迎下次光临"+serverPort;
}
经过压测后数据正常,如下:
但是最终发现还是会存在一个非常严重的问题,就是你的锁失效时间具体要怎么填写?你怎么知道要设置多长的过期时间,这个是要和你的处理业务时间联系再一起,通常会经过 Jmeter 工具测试 qps,然后看平均耗时,但是这个还不是一个好的解决方案。
其实我们可以为这个过期时间续命,或者说是刷新过期时间,在你处理业务的时候,后台线程去刷新这个过期时间。
下面先插个小插曲,先来了解下 Redisson
的东西
Redisson 文档: http://redis.cn/topics/
Redis 分布式锁:http://redis.cn/topics/distlock.html
Github 地址: https://github.com/redisson/redisson
传统的基于 setnx 分布式锁有什么缺点,如下图示?
上述图片讲述了基于 setnx 传统的分布式锁的缺点,那么怎么解决呢?
Redis 中就提供了 RedLock 算法,用来实现基于多个实例的分布式锁,锁由多个实例维护,这样即使你有实例发生故障,锁变量依旧还存在,客户端还可以继续完成锁操作。RedLock 算法是实现高可靠用分布式锁的一种有效解决方案,可以在实际开发中应用。
在多主机群模式中,需要部署几台 Redis 服务器,由计算容错率公式推到:
N(需部署 Redis 台数) = 2 * X(宕机的 Redis 台数) + 1(奇数+1),
假设我么允许有 1 台机器宕机,那么最少部署 2 * 1+1 = 3 台 Redis 服务器就可以保证高可用集群
假设我么允许有 2 台机器宕机,那么最少部署 2 * 1+1 = 5 台 Redis 服务器就可以保证高可用集群
+1 的操作是用最少的开销做到高可用集群,+2 的话虽然也可以,但是你需要多准别一台服务器,实现的效果和 +1 实现的效果是一样的。
采用 docker 部署三台 Redis 服务器
docker 命令如下:
docker run -p 6381:6379 --name redis-master-1 -d redis:6.0.7
docker run -p 6382:6379 --name redis-master-2 -d redis:6.0.7
docker run -p 6383:6379 --name redis-master-3 -d redis:6.0.7
配置类如下:
@ConfigurationProperties(prefix = "spring.redis", ignoreUnknownFields = false)
@Data
public class RedisPropertiesAutoConfiguration {
private int database;
/**
* 等待节点回复命令的时间。该时间从命令发送成功时开始计时
*/
private int timeout;
private String password;
private String mode;
/**
* 池配置
*/
private RedisPoolProperties pool;
/**
* 单机信息配置
*/
private RedisSingleProperties single;
}
@Data
public class RedisPoolProperties {
private int maxIdle;
private int minIdle;
private int maxActive;
private int maxWait;
private int connTimeout;
private int soTimeout;
/**
* 池大小
*/
private int size;
}
@Data
public class RedisSingleProperties {
private String address1;
private String address2;
private String address3;
}
然后配置三个 Redisson 客户端,如下:
@Configuration
@EnableConfigurationProperties(RedisPropertiesAutoConfiguration.class)
public class CacheConfiguration {
@Autowired
RedisPropertiesAutoConfiguration redisPropertiesAutoConfiguration;
@Bean
RedissonClient redissonClient1() {
Config config = new Config();
String node = redisPropertiesAutoConfiguration.getSingle().getAddress1();
node = node.startsWith("redis://") ? node : "redis://" + node;
SingleServerConfig serverConfig = config.useSingleServer()
.setAddress(node)
.setTimeout(redisPropertiesAutoConfiguration.getPool().getConnTimeout())
.setConnectionPoolSize(redisPropertiesAutoConfiguration.getPool().getSize())
.setConnectionMinimumIdleSize(redisPropertiesAutoConfiguration.getPool().getMinIdle());
if (StringUtils.isNotBlank(redisPropertiesAutoConfiguration.getPassword())) {
serverConfig.setPassword(redisPropertiesAutoConfiguration.getPassword());
}
return Redisson.create(config);
}
@Bean
RedissonClient redissonClient2() {
Config config = new Config();
String node = redisPropertiesAutoConfiguration.getSingle().getAddress2();
node = node.startsWith("redis://") ? node : "redis://" + node;
SingleServerConfig serverConfig = config.useSingleServer()
.setAddress(node)
.setTimeout(redisPropertiesAutoConfiguration.getPool().getConnTimeout())
.setConnectionPoolSize(redisPropertiesAutoConfiguration.getPool().getSize())
.setConnectionMinimumIdleSize(redisPropertiesAutoConfiguration.getPool().getMinIdle());
if (StringUtils.isNotBlank(redisPropertiesAutoConfiguration.getPassword())) {
serverConfig.setPassword(redisPropertiesAutoConfiguration.getPassword());
}
return Redisson.create(config);
}
@Bean
RedissonClient redissonClient3() {
Config config = new Config();
String node = redisPropertiesAutoConfiguration.getSingle().getAddress3();
node = node.startsWith("redis://") ? node : "redis://" + node;
SingleServerConfig serverConfig = config.useSingleServer()
.setAddress(node)
.setTimeout(redisPropertiesAutoConfiguration.getPool().getConnTimeout())
.setConnectionPoolSize(redisPropertiesAutoConfiguration.getPool().getSize())
.setConnectionMinimumIdleSize(redisPropertiesAutoConfiguration.getPool().getMinIdle());
if (StringUtils.isNotBlank(redisPropertiesAutoConfiguration.getPassword())) {
serverConfig.setPassword(redisPropertiesAutoConfiguration.getPassword());
}
return Redisson.create(config);
}
最终代码如下:
@RequestMapping("/goods10")
public String buyGoods() {
// 直接上 Redisson
RLock lock1 = redissonClient1.getLock(REDIS_KEY);
RLock lock2 = redissonClient2.getLock(REDIS_KEY);
RLock lock3 = redissonClient3.getLock(REDIS_KEY);
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
boolean isLockBoolean;
try {
// 开始加锁
isLockBoolean = redLock.tryLock(3, 300, TimeUnit.SECONDS);
if (isLockBoolean) {
String result = stringRedisTemplate.opsForValue().get(KEY);
int num = Objects.isNull(result) ? 0 : Integer.parseInt(result);
if (num > 0) {
int realNum = num - 1;
stringRedisTemplate.opsForValue().set(KEY, Integer.toString(realNum));
log.info(">>>>>>>>>>你已经成功秒杀了商品,还剩余{}件,serverPort={}", realNum,serverPort);
return "你已经成功秒杀了商品,还剩余" + realNum + "件"+serverPort;
}
log.info(">>>>>>>>>>活动已经售罄,欢迎下次光临."+serverPort);
}
} catch (Exception e) {
e.printStackTrace();
}
finally {
// 先判断自己是否能去释放这把锁
if (redLock.isLocked() && redLock.isHeldByCurrentThread()) {
// 直接调用 unlock() 解锁即可
redLock.unlock();
}
}
return "活动已经售罄,欢迎下次光临"+serverPort;
}
这样下面数据安全性保证已经提高到 99% 了。现在就剩下最后一个锁续命问题了。
分布式锁在 Redis 中存的数据格式如下所示:
在 Redisson 的实现中,它会额外开一个线程定期检查线程是否还持有这把锁,如果有则延迟锁过期时间,定期检查默认是每 1/3 的锁时间检查一次,如果锁还持有,那么就会刷新过期时间。而这个额外的线程就叫做 Watch Dog
摘取官网的一段话如下所示:
所以最后一个缓存锁续命问题 Redisson 也帮我们实现了,那么现在我们这个分布式锁应该算是比较完整的了。后续的 Redisson 源码分析请看另一篇文章。
所以实现分布式锁优先推荐 Redisson 去实现,使用传统的 setnx 需要自己解决很多问题。