spring-boot 中实现标准 redis 分布式锁
一,前言
redis 现在已经成为系统缓存的必备组件,针对缓存读取更新操作,通常我们希望当缓存过期之后能够只有一个请求去更新缓存,其它请求依然使用旧的数据。这就需要用到锁,因为应用服务多数以集群方式部署,因此这里的锁就必需要是分布式锁才能符合需求。
二,spring-boot 引入 redis
在 pom 文件中加入如下依赖,spring-boot 的自动注册功能会帮我们准备好,我们直接使用 StringRedisTemplate 就可以了。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
三,redis 分布式锁实现
/**
* @author koma <komazhang@foxmail.com>
* @date 2018-09-19 11:24
*/
@Slf4j
@Service
public class CacheService {
private static final Long RELEASE_SUCCESS = 1L;
private static final String LOCK_SUCCESS = "OK";
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "EX";
private static final String RELEASE_LOCK_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 该加锁方法仅针对单实例 Redis 可实现分布式加锁
* 对于 Redis 集群则无法使用
*
* 支持重复,线程安全
*
* @param lockKey 加锁键
* @param clientId 加锁客户端唯一标识(采用UUID)
* @param seconds 锁过期时间
* @return
*/
public Boolean tryLock(String lockKey, String clientId, long seconds) {
redisTemplate.opsForValue().set();
return redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> {
Jedis jedis = (Jedis) redisConnection.getNativeConnection();
String result = jedis.set(lockKey, clientId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, seconds);
if (LOCK_SUCCESS.equals(result)) {
return Boolean.TRUE;
}
return Boolean.FALSE;
});
}
/**
* 与 tryLock 相对应,用作释放锁
*
* @param lockKey
* @param clientId
* @return
*/
public Boolean releaseLock(String lockKey, String clientId) {
return redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> {
Jedis jedis = (Jedis) redisConnection.getNativeConnection();
Object result = jedis.eval(RELEASE_LOCK_SCRIPT, Collections.singletonList(lockKey),
Collections.singletonList(clientId));
if (RELEASE_SUCCESS.equals(result)) {
return Boolean.TRUE;
}
return Boolean.FALSE;
});
}
}
上述代码实现,仅对 redis 单实例架构有效,当面对 redis 集群时就无效了。但是一般情况下,我们的 redis 架构多数会做成“主备”模式,然后再通过 redis 哨兵实现主从切换,这种模式下我们的应用服务器直接面向主机,也可看成是单实例,因此上述代码实现也有效。但是当在主机宕机,从机被升级为主机的一瞬间的时候,如果恰好在这一刻,由于 redis 主从复制的异步性,导致从机中数据没有即时同步,那么上述代码依然会无效,导致同一资源有可能会产生两把锁,违背了分布式锁的原则。


redis 单实例架构示意图
为什么上面的代码可以实现分布式锁,根本原因在于 redis 对 set 命令中的 NX 选项和对 lua 脚本的执行都是原子的,因此当多个客户端去争抢执行上锁或解锁代码时,最终只会有一个客户端执行成功。同时 set 命令还可以指定key的有效期,这样即使当前客户端奔溃,过一段时间锁也会被 redis 自动释放,这就给了其它客户端获取锁的机会。
上述代码不能使用 spring-boot 提供的 redisTemplate.opsForValue().set() 命令是因为 spring-boot 对 jedis 的封装中没有返回 set 命令的返回值,这就导致上层没有办法判断 set 执行的结果,因此需要通过 execute 方法调用 RedisCallback 去拿到底层的 Jedis 对象,来直接调用 set 命令。这个问题主要是在 spring-data-redis 的封装上,了解即可。
四,分布式锁的原则
独享: 即互斥属性,在同一时刻,一个资源只能有一把锁被一个客户端持有
无死锁: 当持有锁的客户端奔溃后,锁仍然可以被其它客户端获取
容错性: 当部分节点失活之后,其余节点客户端依然可以获取和释放锁
统一性: 即释放锁的客户端只能由获取锁的客户端释放
五,一类常见错误实现和推荐使用方式
if (redisTemplate.opsForValue().setIfAbsent(lockKey, clientId)) {
//这里存在宕机风险,导致设置有效期失败
redisTemplate.expire(lockKey, seconds, TimeUnit.SECONDS);
}
这是一种典型的错误实现,在早期的 redis 分布式锁实践中我们经常可以看到类似的实现,其中 spring-boot 中的 setIfAbsent 方法在底层调用的是 redis 的 setNx 命令,该命令和 set 命令的 NX 选项具有同样的功能,但是 setNx 命令不能够设置 key 的有效期,这也是为什么我们会在获取到锁之后马上去设置锁的有效期,但是恰好这里却隐藏着风险,因为这一整个操作并非是原子的。
if (clientId.equals(redisTemplate.opsForValue().get(lockKey))) {
redisTemplate.delete(lockKey);
}
对于解锁代码,也存在同样的风险,因为在执行 delete 的时候,lockKey 现在可能已经被另外一个客户端持有了,那么这里直接删除就是删除了其它客户端的锁,导致的最终结果就是真正应该持有锁的客户端在没有完全执行完之后,锁又被另外的客户端持有了,这样一个资源就产生了两把锁,同样违背了分布式锁的原则。
推荐的使用方式是,当 redis 的架构如上图所示一样是单实例模式时,如果存在主备且可以忍受小概率的锁出错,那么就可以直接使用上述代码,当然最严谨的方式还是使用官方的 Redlock 算法实现。其中 Java 包推荐使用 redisson。
六,参考资料
spring-boot 中实现标准 redis 分布式锁的更多相关文章
- redis分布式锁-spring boot aop+自定义注解实现分布式锁
接这这一篇redis分布式锁-java实现末尾,实现aop+自定义注解 实现分布式锁 1.为什么需要 声明式的分布式锁 编程式分布式锁每次实现都要单独实现,但业务量大功能复杂时,使用编程式分布式锁无疑 ...
- spring boot redis分布式锁
随着现在分布式架构越来越盛行,在很多场景下需要使用到分布式锁.分布式锁的实现有很多种,比如基于数据库. zookeeper 等,本文主要介绍使用 Redis 做分布式锁的方式,并封装成spring b ...
- spring boot 利用redisson实现redis的分布式锁
原文:http://liaoke0123.iteye.com/blog/2375469 利用redis实现分布式锁,网上搜索的大部分是使用java jedis实现的. redis官方推荐的分布式锁实现 ...
- spring boot redis分布式锁 (转)
一. Redis 分布式锁的实现以及存在的问题 锁是针对某个资源,保证其访问的互斥性,在实际使用当中,这个资源一般是一个字符串.使用 Redis 实现锁,主要是将资源放到 Redis 当中,利用其原子 ...
- spring boot(九):Spring Boot中Redis的使用
Redis实战代码 1.引入 spring-boot-starter-redis <dependency> <groupId>org.springframework.boot& ...
- spring boot(三):Spring Boot中Redis的使用
spring boot对常用的数据库支持外,对nosql 数据库也进行了封装自动化. redis介绍 Redis是目前业界使用最广泛的内存数据存储.相比memcached,Redis支持更丰富的数据结 ...
- springboot(三):Spring boot中Redis的使用
spring boot对常用的数据库支持外,对nosql 数据库也进行了封装自动化. redis介绍 Redis是目前业界使用最广泛的内存数据存储.相比memcached,Redis支持更丰富的数据结 ...
- Spring Cloud分布式微服务系统中利用redssion实现分布式锁
在非分布式系统中要实现锁的机制很简单,利用java.util.concurrent.locks包下的Lock和关键字synchronized都可以实现.但是在分布式系统中,如何实现各个单独的微服务需要 ...
- Spring Boot(十一)Redis集成从Docker安装到分布式Session共享
一.简介 Redis是一个开源的使用ANSI C语言编写.支持网络.可基于内存亦可持久化的日志型.Key-Value数据库,并提供多种语言的API,Redis也是技术领域使用最为广泛的存储中间件,它是 ...
随机推荐
- linux安装git服务器和svn服务器
linux版本 linux版本为CentOS 6.8 (要注意有些软件的安装方法在各个linux版本之间也是存在差异的) git服务器 git服务器需要提供一个UI供开发人员创建项目管理项目,选择使用 ...
- shell脚本之nginx启动脚本、统计日志字段、for循环实战、跳板机
1.NGINX启动脚本 #!/bin/bash # chkconfig: 235 32 62 # description: nginx [ -f /etc/init.d/functions ] &am ...
- Java 子类继承父类成员中的问题
之前搞错了,变量没有“重写”一说,只有方法才能被“重写”.如果我们在子类中声明了一个和父类中一样的变量,那么实际的情况是,子类的内存堆中会有类型和名字都相同的两个变量. 现在考虑一种情况,如下所示,我 ...
- npm操作命令
查看所有高级的npm moudles npm list --depth= 查看所有全局安装的模块 npm list --depth= -global 查找npm全局安装模块路径 npm config ...
- angular使用@angular/material 出现"export 'ɵɵinject' was not found in '@angular/core'
WARNING in ./node_modules/@angular/cdk/esm5/a11y.es5.js 2324:206-214 "export 'ɵɵinject' was not ...
- 使输入框(input & textarea)变为只可读状态readonly="readonly",禁用输入框disabled="disabled"
使输入框变为只可读状态 readonly="readonly" <input class="select-city" placeholder=" ...
- python中format所有用法
平时只用参数匹配,偶尔看到别人的format用法楞住没反应过来,遂记下 #通过位置 print '{0},{1}'.format('hehe',20) print '{},{}'.format('he ...
- (转)Ubuntu换源方法
I. 查看系统版本及内核 首先查看自己的ubuntu系统的codename,这一步很重要,直接导致你更新的源是否对你的系统起效果,查看方法: lsb_release -a 如,我的系统显示: No L ...
- 像@Transactional一样利用注解自定义aop切片
在spring中,利用@Transactional注解可以很轻松的利用aop技术进行事物管理.在实际项目中,直接利用自定义注解实现切片可以大大的提高我们的编码效率以及代码的简洁性. 实现以上的目标,主 ...
- c++ 初学者的画图库EasyX
EasyX 什么是easyx? EasyX 是针对 C++ 的图形库,可以帮助 C++语言初学者快速上手图形和游戏编程.其实就是c++的一个图形库让初学者不用只在控制台输出代码,而是在图形界面进行开发 ...