【分布式锁】Redis实现可重入的分布式锁
一、前言
之前写的一篇文章《细说分布式锁》介绍了分布式锁的三种实现方式,但是Redis实现分布式锁关于Lua脚本实现、自定义分布式锁注解以及需要注意的问题都没描述。本文就是详细说明如何利用Redis实现重入的分布式锁。
二、方案
死锁问题
当一个客户端获取锁成功之后,假如它崩溃了导致它再也无法和 Redis 节点通信,那么它就会一直持有这个锁,导致其它客户端永远无法获得锁了,因此锁必须要有一个自动释放的时间。
我们需要保证setnx命令和expire命令以原子的方式执行,否则如果客户端执行setnx获得锁后,这时客户端宕机了,那么这把锁没有设置过期时间,导致其他客户端永远无法获得锁了。
锁被其他线程释放
如果不加任何处理即简单使用 SETNX 实现 Redis 分布式锁,就会遇到一个问题:如果线程 C1 获得锁,但由于业务处理时间过长,锁在线程 C1 还未处理完业务之前已经过期了,这时线程 C2 获得锁,在线程 C2 处理业务期间线程 C1 完成业务执行释放锁操作,但这时线程 C2 仍在处理业务线程 C1 释放了线程 C2 的锁,导致线程 C2 业务处理实际上没有锁提供保护机制;同理线程 C2 可能释放线程 C3 的锁,从而导致严重的问题。
因此每个线程释放锁的时候只能释放自己的锁,即锁必须要有一个拥有者的标记,并且也需要保证释放锁的原子性操作。
在释放锁的时候判断拥有者的标记(value是否相同),只有相同时才可以删除,同时利用Lua脚本来达到原子操作,脚本如下:
- if redis.call("get", KEYS[1]) == ARGV[1] then
- return redis.call("del", KEYS[1])
- else
- return 0
- end
可重入问题
可重入锁指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,如果没有可重入锁的支持,在第二次尝试获得锁时将会进入死锁状态。
这里有两种解决方案:
- 客户端在获得锁后保存value(拥有者标记),然后释放锁的时候将value和key同时传过去。
- 利用ThreadLocal实现,获取锁后将Redis中的value保存在ThreadLocal中,同一线程再次尝试获取锁的时候就先将 ThreadLocal 中的 值 与 Redis 的 value 比较,如果相同则表示这把锁所以该线程,即实现可重入锁。
这里的实现的方案是基于单机Redis,之前说的集群问题这里暂不考虑。
三、编码
我们通过自定义分布式锁注解+AOP可以更加方便的使用分布式锁,只需要在加锁的方法上加上注解即可。
Redis分布式锁接口
/**
* Redis分布式锁接口
* Created by 2YSP on 2019/9/20.
*/
public interface IRedisDistributedLock {
/**
*
* @param key
* @param requireTimeOut 获取锁超时时间 单位ms
* @param lockTimeOut 锁过期时间,一定要大于业务执行时间 单位ms
* @param retries 尝试获取锁的最大次数
* @return
*/
boolean lock(String key, long requireTimeOut, long lockTimeOut, int retries);
/**
* 释放锁
* @param key
* @return
*/
boolean release(String key);
}
Redis 分布式锁实现类
/**
* Redis 分布式锁实现类
* Created by 2YSP on 2019/9/20.
*/
@Slf4j
@Component
public class RedisDistributedLockImpl implements IRedisDistributedLock {
/**
* key前缀
*/
public static final String PREFIX = "Lock:";
/**
* 保存锁的value
*/
private ThreadLocal<String> threadLocal = new ThreadLocal<>();
private static final Charset UTF8 = Charset.forName("UTF-8");
/**
* 释放锁脚本
*/
private static final String UNLOCK_LUA;
/*
* 释放锁脚本,原子操作
*/
static {
StringBuilder sb = new StringBuilder();
sb.append("if redis.call(\"get\",KEYS[1]) == ARGV[1] ");
sb.append("then ");
sb.append(" return redis.call(\"del\",KEYS[1]) ");
sb.append("else ");
sb.append(" return 0 ");
sb.append("end ");
UNLOCK_LUA = sb.toString();
}
@Autowired
private RedisTemplate redisTemplate;
@Override
public boolean lock(String key, long requireTimeOut, long lockTimeOut, int retries) {
//可重入锁判断
String originValue = threadLocal.get();
if (!StringUtils.isBlank(originValue) && isReentrantLock(key, originValue)) {
return true;
}
String value = UUID.randomUUID().toString();
long end = System.currentTimeMillis() + requireTimeOut;
int retryTimes = 1;
try {
while (System.currentTimeMillis() < end) {
if (retryTimes > retries) {
log.error(" require lock failed,retry times [{}]", retries);
return false;
}
if (setNX(wrapLockKey(key), value, lockTimeOut)) {
threadLocal.set(value);
return true;
}
// 休眠10ms
Thread.sleep(10);
retryTimes++;
}
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
private boolean setNX(String key, String value, long expire) {
/**
* List设置lua的keys
*/
List<String> keyList = new ArrayList<>();
keyList.add(key);
return (boolean) redisTemplate.execute((RedisCallback<Boolean>) connection -> {
Boolean result = connection
.set(key.getBytes(UTF8),
value.getBytes(UTF8),
Expiration.milliseconds(expire),
SetOption.SET_IF_ABSENT);
return result;
});
}
/**
* 是否为重入锁
*/
private boolean isReentrantLock(String key, String originValue) {
String v = (String) redisTemplate.opsForValue().get(key);
return v != null && originValue.equals(v);
}
@Override
public boolean release(String key) {
String originValue = threadLocal.get();
if (StringUtils.isBlank(originValue)) {
return false;
}
return (boolean) redisTemplate.execute((RedisCallback<Boolean>) connection -> {
return connection
.eval(UNLOCK_LUA.getBytes(UTF8), ReturnType.BOOLEAN, 1, wrapLockKey(key).getBytes(UTF8),
originValue.getBytes(UTF8));
});
}
private String wrapLockKey(String key) {
return PREFIX + key;
}
}
分布式锁注解
@Retention(value = RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DistributedLock {
/**
* 默认包名加方法名
* @return
*/
String key() default "";
/**
* 过期时间 单位:毫秒
* <pre>
* 过期时间一定是要长于业务的执行时间.
* </pre>
*/
long expire() default 30000;
/**
* 获取锁超时时间 单位:毫秒
* <pre>
* 结合业务,建议该时间不宜设置过长,特别在并发高的情况下.
* </pre>
*/
long timeout() default 3000;
/**
* 默认重试次数
* @return
*/
int retryTimes() default Integer.MAX_VALUE;
}
aop切片类
@Component
@Aspect
@Slf4j
public class RedisLockAop {
@Autowired
private IRedisDistributedLock redisDistributedLock;
@Around(value = "@annotation(lock)")
public Object doAroundAdvice(ProceedingJoinPoint proceedingJoinPoint, DistributedLock lock) {
// 加锁
String key = getKey(proceedingJoinPoint, lock);
Boolean success = null;
try {
success = redisDistributedLock
.lock(key, lock.timeout(), lock.expire(), lock.retryTimes());
if (success) {
log.info(Thread.currentThread().getName() + " 加锁成功");
return proceedingJoinPoint.proceed();
}
log.info(Thread.currentThread().getName() + " 加锁失败");
return null;
} catch (Throwable throwable) {
throwable.printStackTrace();
return null;
} finally {
if (success){
boolean result = redisDistributedLock.release(key);
log.info(Thread.currentThread().getName() + " 释放锁结果:{}",result);
}
}
}
private String getKey(JoinPoint joinPoint, DistributedLock lock) {
if (!StringUtils.isBlank(lock.key())) {
return lock.key();
}
return joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature()
.getName();
}
}
业务逻辑处理类
@Service
public class TestService {
@DistributedLock(retryTimes = 1000,timeout = 1000)
public String lockTest() {
try {
System.out.println("模拟执行业务逻辑。。。");
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
return "error";
}
return "ok";
}
}
四、测试
- 启动本地redis,启动项目
- 打开cmd,利用ab压力测试设置3个线程100个请求。
ab -c 3 -n 100 http://localhost:8000/lock/test
idea控制台输出如下:
至此大功告成,代码地址。
ps: 遇到一个奇怪的问题,我用 RedisTemplate.execute(RedisScript script, List keys, Object... args) 这个方法,通过加载resource目录下的lua脚本来释放锁的时候一直不成功,参数没任何问题,而且我之前的文章就是用这个方法可以正确的释放锁。
【分布式锁】Redis实现可重入的分布式锁的更多相关文章
- java并发编程(一)可重入内置锁
每个Java对象都可以用做一个实现同步的锁,这些锁被称为内置锁或监视器锁.线程在进入同步代码块之前会自动获取锁,并且在退出同步代码块时会自动释放锁.获得内置锁的唯一途径就是进入由这个锁保护的同步代码块 ...
- 转:【Java并发编程】之一:可重入内置锁
每个Java对象都可以用做一个实现同步的锁,这些锁被称为内置锁或监视器锁.线程在进入同步代码块之前会自动获取锁,并且在退出同步代码块时会自动释放锁.获得内置锁的唯一途径就是进入由这个锁保护的同步代码块 ...
- 【Java并发编程】之一:可重入内置锁
每个Java对象都可以用做一个实现同步的锁,这些锁被称为内置锁或监视器锁.线程在进入同步代码块之前会自动获取锁,并且在退出同步代码块时会自动释放锁.获得内置锁的唯一途径就是进入由这个锁保护的同步代码块 ...
- Java锁的深度化--重入锁、读写锁、乐观锁、悲观锁
Java锁 锁一般来说用作资源控制,限制资源访问,防止在并发环境下造成数据错误 锁作为并发共享数据,保证一致性的工具,在JAVA平台有多种实现(如 synchronized(重量级) 和 Reentr ...
- Java并发-显式锁篇【可重入锁+读写锁】
作者:汤圆 个人博客:javalover.cc 前言 在前面并发的开篇,我们介绍过内置锁synchronized: 这节我们再介绍下显式锁Lock 显式锁包括:可重入锁ReentrantLock.读写 ...
- Redis分布式锁—Redisson+RLock可重入锁实现篇
前言 平时的工作中,由于生产环境中的项目是需要部署在多台服务器中的,所以经常会面临解决分布式场景下数据一致性的问题,那么就需要引入分布式锁来解决这一问题. 针对分布式锁的实现,目前比较常用的就如下几种 ...
- JUC回顾之-可重入的互斥锁ReentrantLock
1.什么是可重锁ReentrantLock? 就是支持重新进入的锁,表示该锁能够支持一个线程对资源的重复加锁. 2.ReentrantLock分为公平锁和非公平锁:区别是在于获取锁的机制上是否公平. ...
- 浅谈Java中的锁:Synchronized、重入锁、读写锁
Java开发必须要掌握的知识点就包括如何使用锁在多线程的环境下控制对资源的访问限制 ◆ Synchronized ◆ 首先我们来看一段简单的代码: 12345678910111213141516171 ...
- Java内置锁synchronized的可重入性
学习自 https://blog.csdn.net/aigoogle/article/details/29893667 对我很有帮助 感谢作者
随机推荐
- Hadoop完全分布式模式安装部署
在Linux上搭建Hadoop系列:1.Hadoop环境搭建流程图2.搭建Hadoop单机模式3.搭建Hadoop伪分布式模式4.搭建Hadoop完全分布式模式 注:此教程皆是以范例讲述的,当然你可以 ...
- 第10.11节 Python模块和包小结
Python的模块就是一个独立的Python文件,Python的包是一些功能相关的Python文件放到一个目录下进行统一管理的文件管理结构,包本质上是模块,加载包就是加载包下特定的模块文件__init ...
- urllib.request.urlopen(req).read().decode解析http报文报“utf-8 codec can not decode”错处理
老猿前期执行如下代码时报"'utf-8' codec can't decode byte"错,代码及错误信息如下: >>> import urllib.reque ...
- 浅谈php反序列化漏洞
关于php的反序列化漏洞要先说到序列化和反序列化的两个函数,即: serialize() 和unserialize(). 简单的理解: 序列化就是将一个对象变成字符串 反序列化是将字符串恢复成对象 这 ...
- 【Alpha冲刺阶段】Scrum Meeting Daily3
[Alpha冲刺阶段]Scrum Meeting Daily3 1.会议简述 会议开展时间 2020/5/24 8:00-8:15 PM 会议基本内容摘要 每日汇报 个人进度.遇到的困难.明日的计划. ...
- Deep Learning with Differential Privacy
原文链接:Deep Learning with Differential Privacy abstract:新的机器学习算法,差分隐私框架下隐私成本的改良分析,使用非凸目标训练深度神经网络. 数学中最 ...
- jfinal 导出excle
Controller Map<String,List<PoiUtilHeader>> result = new HashMap<String, List<PoiUt ...
- CSS文本溢出效果&滚动条样式设置
一.文本溢出 1.overflow: hidden; 超出文本会被剪裁隐藏不可见 scroll;超出文本会被剪裁, 显示滚动条 auto; 如果文本超出会显示滚动条,没超出不会显示, overflo ...
- js上 十三、函数初步-2
13-1.函数的参数 函数的本质: ü 函数的作用,代码重用,编写一个函数,就是为了解决一类问题. ü 函数每次调用,都有一个结果,那么结果和什么相关呢?y = x , y = x2,y = sin( ...
- MAC 安装Python3.7
查看下python版本 macosdeMacBook:Versions macos$ cd /System/Library/Frameworks/Python.framework/Versions/ ...