springboot redis 分布式锁
参考了 深度剖析:Redis 分布式锁到底安全吗?看完这篇文章彻底懂了!参考了 尚学堂老师的课程,感谢老师的讲解前言:在自己的业务中研究了一下redis锁的问题,现整理如下为什么需要分布式锁?/*** 根据 id 更新商品名字 (不考虑业务场景,只研究一下分布式锁的问题)*为什么需要分布式锁?*单机锁: 与分布式锁相对应的是「单机锁」,我们在写多线程程序时,避免同时操作一个共享变量产生数据问题,通
·
参考了 深度剖析:Redis 分布式锁到底安全吗?看完这篇文章彻底懂了!
参考了 尚学堂老师的课程,感谢老师的讲解
前言:在自己的业务中研究了一下redis锁的问题,现整理如下
- 为什么需要分布式锁?
/**
* 根据 id 更新商品名字 (不考虑业务场景,只研究一下分布式锁的问题)
* 为什么需要分布式锁?
* 单机锁: 与分布式锁相对应的是「单机锁」,我们在写多线程程序时,避免同时操作一个共享变量产生数据问题,通常会使用一把锁来「互斥」,以保证共享变量的正确性,其使用范围是在「同一个进程」中。
* 1. 在单进程的情况下, 这种加锁方式是能锁住共享资源的,不会多个线程同时操作数据库
* 2. 如果在多进程(部署多个服务的时候会出现), 则会出现不同进程的线程会同时操作数据库,因此需要加分布式锁
*
* @param project
*/
@Override
public void updateProjectNameById(Project project){
synchronized (name){
int i = projectMapper.updateById(project);
}
// updateProjectNameById06(project);
}
- 分布式锁怎么实现?
/**
* 分布式锁怎么实现?
* 分布式锁: 利用第三方系统 redis 实现分布式锁
* 1. 在多进程的情况下, 能保证资源始终被一个线程访问
* 2. 有可能出现的问题:
* 1. 程序处理业务逻辑异常,没及时释放锁
* 2. 进程挂了,没机会释放锁
* 3. 这时,这个客户端就会一直占用这个锁,而其它客户端就「永远」拿不到这把锁了
*
* @param project
*/
public void updateProjectNameById02(Project project){
String key = "lock";
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "qinenqi"); // 1. 占分布式锁,去redis中占坑
if(flag){ // 加锁成功
int i = projectMapper.updateById(project);
stringRedisTemplate.delete(key); // 删除锁
}else{ // 加锁失败
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
updateProjectNameById02(project);
}
}
- 如何避免死锁?
/**
* 如何避免死锁?
* 在申请锁时,给这把锁设置一个「租期」
* 可能会出现的问题:
* 1. 加锁成功,由于网络原因 还没有来得及设置租期
* 2. 加锁成功之后,redis宕机,还没有来得及设置租期
* 2. 加锁成功,客户端崩溃,还没来得及设置租期
* 总之,这两条命令不能保证是原子操作(一起成功),就有潜在的风险导致过期时间设置失败,依旧发生「死锁」问题。
*
* @param project
*/
public void updateProjectNameById03(Project project){
String key = "lock";
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "qinenqi"); // 1. 占分布式锁,去redis中占坑
if(flag){ // 加锁成功
stringRedisTemplate.expire(key, 10, TimeUnit.SECONDS); // 设置过期时间
int i = projectMapper.updateById(project);
stringRedisTemplate.delete(key); // 删除锁
}else{ // 加锁失败
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
updateProjectNameById03(project);
}
}
- 加锁和设置租期是一个原子性操作
/**
* 加锁和设置租期是一个原子性操作
* 可能出现的问题: 如果由于业务时间很长,锁自己过期了,我们直接删除,有可能把别人正在持有的锁删除了,因为锁过期释放之后,别的请求又加了锁
* 有可能会释放别人加的锁也就是说 一个客户端释放了其它客户端持有的锁
*
* @param project
*/
public void updateProjectNameById04(Project project){
String key = "lock";
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "qinenqi", 10, TimeUnit.SECONDS); // 1. 占分布式锁,去redis中占坑
if(flag){ // 加锁成功
int i = projectMapper.updateById(project);
stringRedisTemplate.delete(key); // 删除锁
}else{ // 加锁失败
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
updateProjectNameById04(project);
}
}
- 一个客户端释放了其它客户端持有的锁
/**
* 一个客户端释放了其它客户端持有的锁
* 解决办法是:客户端在加锁时,设置一个只有自己知道的「唯一标识」进去。
* 可能出现的问题:加锁和释放锁是两条命令,这时,又会遇到我们前面讲的原子性问题了,出现了原子性问题就可能会出现死锁情况
* stringRedisTemplate.delete(key); 删除锁的这条指令不是原子性的, 因此引入了 释放锁的 Lua 脚本
* @param project
*/
public void updateProjectNameById05(Project project){
String key = "lock";
String uuid = UUID.randomUUID().toString().replaceAll("-", "");
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, uuid, 10, TimeUnit.SECONDS); // 1. 占分布式锁,去redis中占坑
if(flag){ // 加锁成功
int i = projectMapper.updateById(project);
String value = stringRedisTemplate.opsForValue().get(key);
if(uuid.equals(value)){
stringRedisTemplate.delete(key); // 删除锁
}
}else{ // 加锁失败
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
updateProjectNameById05(project);
}
}
- 无论出现什么情况都会释放锁
/**
* 无论出现什么情况都会释放锁
* 安全释放锁的 Lua 脚本
* @param project
*/
public void updateProjectNameById06(Project project){
String key = "lock";
String uuid = UUID.randomUUID().toString().replaceAll("-", "");
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, uuid, 10, TimeUnit.SECONDS); // 1. 占分布式锁,去redis中占坑
if(flag){ // 加锁成功
try {
int i = projectMapper.updateById(project);
String value = stringRedisTemplate.opsForValue().get(key);
}finally {
String script = "if redis.call('get', KEYS[1])== ARGV[1] then returnredis.call('del', KEYS[1])else return 0 end";
//删除锁
Long lock1 = stringRedisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class)
, Arrays.asList("lock"), uuid);
}
}else{ // 加锁失败
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
updateProjectNameById06(project);
}
}
- redis锁自动续期
由于设置了锁自动过期,如果业务逻辑耗时较长,有可能出现业务还没有执行完,redis分布式锁就过期了,如果出现了这种情况,有可能会带来严重的问题(如:库存还没有扣除呢,新的购买业务进来了),因此这种情况下,引入了锁自动续期的功能。实现过程大约是,起一个线程,一直观察redis锁还有多长时间,如果时间小于10秒,则自动续期(30秒),等业务完成并释放锁之后,则结束这个观察线程。
新建一个观察线程 WatchDogThread
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.concurrent.TimeUnit;
/**
* 观察线程
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Slf4j
public class WatchDogThread extends Thread{
/**
* K key redis key
*/
private String key;
/**
* 标识符
*/
private boolean flag;
private StringRedisTemplate stringRedisTemplate;
/**
* 起一个线程 时刻观察者,如果redis锁的时间 小于 5 秒,则续期
*/
@Override
public void run() {
while(flag){
Long expire = stringRedisTemplate.getExpire(key);
// 代替调用 releaseLock(), 判断和调用都是为了退出while循环
if(expire <= 0){
break;
}
log.info("我还是在监控中:" + expire);
if(expire < 10L){ // redis 锁的时间小于10秒
// public Boolean expire(K key, long timeout, TimeUnit unit)
stringRedisTemplate.expire(key, 30, TimeUnit.SECONDS);
}
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 释放锁
*/
public void releaseLock(){
flag = false;
}
}
业务逻辑
/**
* 锁过期时间不好评估怎么办
* redis 自动续期 WatchDog
* @param project
*/
public void updateProjectNameById07(Project project){
Date date01 = new Date();
String key = "lock";
String uuid = UUID.randomUUID().toString().replaceAll("-", "");
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, uuid, 30, TimeUnit.SECONDS); // 1. 占分布式锁,去redis中占坑
if(flag){ // 加锁成功
// 采用看门狗 时刻观察者,如果redis锁的时间 小于 5 秒,则续期
WatchDogThread watchDogThread = new WatchDogThread(key, true, stringRedisTemplate);
watchDogThread.start();
try {
int i = projectMapper.updateById(project);
String value = stringRedisTemplate.opsForValue().get(key);
Thread.sleep(1000 * 58);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
String script = "if redis.call('get', KEYS[1])== ARGV[1] then return redis.call('del', KEYS[1])else return 0 end";
//删除锁
Long lock1 = stringRedisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class)
, Arrays.asList("lock"), uuid);
}
}else{ // 加锁失败
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
updateProjectNameById07(project);
}
Date date02 = new Date();
log.info("计算执行时间:" + (date02.getTime() - date01.getTime()));
}
更多推荐


所有评论(0)