Redis分布式锁关键

SETNX

语法: SETNX key value

  • 如果key不存在,则存储(key:value)值,返回1

  • 如果key已经不存在,则不执行操作,返回0

因为这个命令的性质,多个线程竞争时只有一个线程能修改key的值。利用这一点可以实现锁的互斥功能。

Redis分布式锁实现

定义接口

public interface Lock {
/**
* 获取锁
* @param lock 锁名称
*/
void lock(String lock); /**
* 释放锁
* @param lock 锁名称
*/
void unlock(String lock);
}
分布式锁代码实现:
public class DistributeLock implements Lock {
private static final Logger logger = LoggerFactory.getLogger(DistributeLock.class); private static final int LOCK_MAX_EXIST_TIME = 5; // 单位s,一个线程持有锁的最大时间
private static final String LOCK_PREX = "lock_"; // 作为锁的key的前缀 private StringRedisTemplate redisTemplate;
private String lockPrex; // 做为锁key的前缀
private int lockMaxExistTime; // 单位s,一个线程持有锁的最大时间
private ThreadLocal<String> threadId = new ThreadLocal<String>(); // 线程变量 public DistributeLock(StringRedisTemplate redisTemplate){
this(redisTemplate, LOCK_PREX, LOCK_MAX_EXIST_TIME);
} public DistributeLock(StringRedisTemplate redisTemplate, String lockPrex, int lockMaxExistTime){
this.redisTemplate = redisTemplate;
this.lockPrex = lockPrex;
this.lockMaxExistTime = lockMaxExistTime;
} @Override
public void lock(String lock){
Assert.notNull(lock, "lock can't be null!");
String lockKey = generatorLockKey(lock);
BoundValueOperations<String,String> keyBoundValueOperations = redisTemplate.boundValueOps(lockKey);
while(true){
// 如果上次拿到锁的是自己,则本次也可以拿到锁:实现可重入
String value = keyBoundValueOperations.get();
// 根据传入的值,判断用户是否持有这个锁
if(value != null && value.equals(String.valueOf(threadId.get()))){
// 重置过期时间
keyBoundValueOperations.expire(lockMaxExistTime, TimeUnit.SECONDS);
break;
} if(keyBoundValueOperations.setIfAbsent(lockKey)){
// 每次获取锁时,必须重新生成id值
String keyUniqueId = UUID.randomUUID().toString(); // 生成key的唯一值
threadId.set(keyUniqueId);
// 显设置value,再设置过期日期,否则过期日期无效
keyBoundValueOperations.set(String.valueOf(keyUniqueId));
// 为了避免一个用户拿到锁后,进行过程中没有正常释放锁,这里设置一个默认过期时间,这段非常重要,如果没有,则会造成死锁
keyBoundValueOperations.expire(lockMaxExistTime, TimeUnit.SECONDS);
// 拿到锁后,跳出循环
break;
}else{
try {
// 短暂休眠,nano避免出现活锁
Thread.sleep(10, (int)(Math.random() * 500));
} catch (InterruptedException e) {
break;
}
}
}
} /**
* 释放锁,同时要考虑当前锁是否为自己所有,以下情况会导致当前线程失去锁:线程执行的时间超过超时的时间,导致此锁被其它线程拿走; 此时用户不可以执行删除
*
* 以上方法的缺陷:
* a. 在本线程获取值,判断锁本线程所有,但是在执行删除前,锁超时被释放同时被另一个线程获取,则本操作释放锁
*
* 最终解决方案
* a. 使用lua脚本,保证检测和删除在同一事物中
*
*/
@Override
public void unlock(final String lock) {
final String lockKey = generatorLockKey(lock);
BoundValueOperations<String,String> keyBoundValueOperations = redisTemplate.boundValueOps(lockKey);
String lockValue = keyBoundValueOperations.get();
if(!StringUtils.isEmpty(lockValue) && lockValue.equals(threadId.get())){
redisTemplate.delete(lockKey);
}else{
logger.warn("key=[{}]已经变释放了,本次不执行释放. 线程Id[{}] ", lock, lockValue);
}
} /**
* 生成key
* @param lock
* @return
*/
private String generatorLockKey(String lock){
StringBuilder sb = new StringBuilder();
sb.append(lockPrex).append(lock);
return sb.toString();
} }

1、ThreadLocal threadId:通过threadId保存每个线程锁的UUID值,用于区分当前锁是否为自己所有,并且锁的value也存储此值
2、lock主要逻辑:通过BoundValueOperations的setIfAbsent设置lockKey值(setIfAbsent其实就是封装了SETNX的命令),如果返回true,则表示已经获取锁;如果返回false,则进入等待
unlock主要逻辑:通过redisTemplate.delete释放锁。在释放锁前,需要判断当前锁被当前线程所有,如果是,才执行释放锁,否则不执行
3、避免死锁:如果线程A拿到锁后,在执行释放锁前,突然死掉了,则其它线程都无法再次获取锁,从而出现死锁。为了避免死锁,我们获取锁后,需要为锁设置一个有效期,即使锁的拥有者死掉了,此锁也可以被自动释放
4、锁可重入:线程A拿到锁后,如果他再次执行lock,也可以再次拿到锁,而不是出现在等待锁的队列中; 如果当前线程已经获取锁,则再次请求锁则一定可以获取锁,否则会出现自己等待自己释放锁,从而出现死锁

封装分布式锁代码逻辑暴露调用API
public interface LockManager {
/**
* 通过加锁安全执行程序,无返回的数据
* @param lockKeyName key名称
* @param callback
*/
void lockCallBack(String lockKeyName, SimpleCallBack callback);
/**
* 通过加锁安全执行程序,有返回数据
* @param lockKeyName
* @param callback
* @return
*/
<T> T lockCallBackWithRtn(String lockKeyName, ReturnCallBack<T> callback);
}
@Component
public class SimpleRedisLockManager implements LockManager { @Autowired
protected StringRedisTemplate redisTemplate; protected Lock distributeLock; // 分布锁 @PostConstruct
public void init(){
// 初始化锁
distributeLock = new DistributeLock(redisTemplate, "mylock_", 5);
} @Override
public void lockCallBack(String lockKeyName, SimpleCallBack callback){
Assert.notNull("lockKeyName","lockKeyName 不能为空");
Assert.notNull("callback","callback 不能为空");
try{
// 获取锁
distributeLock.lock(lockKeyName);
callback.execute();
}finally{
// 必须释放锁
distributeLock.unlock(lockKeyName);
}
} @Override
public <T> T lockCallBackWithRtn(String lockKeyName, ReturnCallBack<T> callback){
Assert.notNull("lockKeyName","lockKeyName 不能为空");
Assert.notNull("callback","callback 不能为空");
try{
// 获取锁
distributeLock.lock(lockKeyName);
return callback.execute();
}finally{
// 必须释放锁
distributeLock.unlock(lockKeyName);
}
}
}
/**
* 无返回值的回调函数
* @author hry
*
*/
public interface SimpleCallBack {
void execute();
}
/**
* 有返回数据的回调函数
*
* @author hry
*
* @param <T>
*/
public interface ReturnCallBack<T> {
T execute();
}
测试分布式锁
@Autowired
private SimpleRedisLockManager simpleRedisLockManager; simpleRedisLockManager.lockCallBack("distributeLock" + ThreadLocalRandom.current().nextInt(1000), new SimpleCallBack() {
@Override
public void execute() {
System.out.println("lockCallBack");
}
});

1、如果线程A拿到锁超过规定的时间还没有结束,则此时redis会自动释放锁。此时线程B拿到锁,则同时线程A和线程B同时拿到锁。对于这种情况,可以通过设置合理的超时时间解决。
2、如果并发量很大,则可能出现多个线程同时拥有锁。这是因为在DistributeLock的lock和unlock方法都执行多条语句且这些语句不是事务的。比如线程A在unlock时,通过get方法得知自己拥有锁,然后他执行释放锁操作。在这两个操作之间,redis发现锁到期,自动删除锁,此时线程B申请并且得到锁。这时线程A才执行删除锁操作,则另外线程C也可以得到锁,此时线程B,C同时得到锁。这种情况可以通过下文的lua方法解决

改进代码加入lua脚本保证原子性操作
lock.lua => 加锁脚本
-- Set a lock
-- 如果获取锁成功,则返回 1
local key = KEYS[1]
local content = KEYS[2]
local ttl = ARGV[1]
local lockSet = redis.call('setnx', key, content)
if lockSet == 1 then
redis.call('pexpire', key, ttl)
else
-- 如果value相同,则认为是同一个线程的请求,则认为重入锁
local value = redis.call('get', key)
if(value == content) then
lockSet = 1;
redis.call('pexpire', key, ttl)
end
end
return lockSet
----------------------------
unlock.lua => 解锁脚本
-- unlock key
local key = KEYS[1]
local content = KEYS[2]
local value = redis.call('get', key)
if value == content then
return redis.call('del', key);
end
return 0
基于lua脚本实现分布式锁
public class LuaDistributeLock implements Lock {
private static final int LOCK_MAX_EXIST_TIME = 5; // 单位s,一个线程持有锁的最大时间
private static final String LOCK_PREX = "lock_"; // 作为锁的key的前缀 private StringRedisTemplate redisTemplate;
private String lockPrex; // 做为锁key的前缀
private int lockMaxExistTime; // 单位s,一个线程持有锁的最大时间
private DefaultRedisScript<Long> lockScript; // 加锁锁脚本
private DefaultRedisScript<Long> unlockScript; // 解锁脚本 // 线程变量
private ThreadLocal<String> threadKeyId = new ThreadLocal<String>(){
@Override
protected String initialValue() {
return UUID.randomUUID().toString();
}
}; public LuaDistributeLock(StringRedisTemplate redisTemplate){
this(redisTemplate, LOCK_PREX, LOCK_MAX_EXIST_TIME);
} public LuaDistributeLock(StringRedisTemplate redisTemplate, String lockPrex, int lockMaxExistTime){
this.redisTemplate = redisTemplate;
this.lockPrex = lockPrex;
this.lockMaxExistTime = lockMaxExistTime;
// init
init();
} /**
* 初始化lua的加锁和解锁脚本对象
*/
public void init() {
// Lock script
lockScript = new DefaultRedisScript<Long>();
lockScript.setScriptSource(
new ResourceScriptSource(new ClassPathResource("com/mmren/edu/spring/boot/redis/distributedlock/lock.lua")));
lockScript.setResultType(Long.class);
// unlock script
unlockScript = new DefaultRedisScript<Long>();
unlockScript.setScriptSource(
new ResourceScriptSource(new ClassPathResource("com/mmren/edu/spring/boot/redis/distributedlock/unlock.lua")));
unlockScript.setResultType(Long.class);
} @Override
public void lock(String lock2){
Assert.notNull(lock2, "lock2 can't be null!");
String lockKey = generatorLockKey(lock2);
while(true){
List<String> keyList = new ArrayList<String>();
keyList.add(lockKey);
keyList.add(threadKeyId.get());
if(redisTemplate.execute(lockScript, keyList, String.valueOf(lockMaxExistTime * 1000)) > 0){
break;
}else{
try {
// 短暂休眠,nano避免出现活锁
Thread.sleep(10, (int)(Math.random() * 500));
} catch (InterruptedException e) {
break;
}
}
}
} /**
* 释放锁,同时要考虑当前锁是否为自己所有,以下情况会导致当前线程失去锁:线程执行的时间超过超时的时间,导致此锁被其它线程拿走; 此时用户不可以执行删除
*/
@Override
public void unlock(final String lock) {
final String lockKey = generatorLockKey(lock);
List<String> keyList = new ArrayList<String>();
keyList.add(lockKey);
keyList.add(threadKeyId.get());
redisTemplate.execute(unlockScript, keyList);
} /**
* 生成key
* @param lock
* @return
*/
private String generatorLockKey(String lock){
StringBuilder sb = new StringBuilder();
sb.append(lockPrex).append(lock);
return sb.toString();
} }
封装lua分布式锁暴露API
@Component
public class LuaLockRedisLockManager extends SimpleRedisLockManager {
@PostConstruct
public void init(){
// 初始化锁
distributeLock = new LuaDistributeLock(redisTemplate, "mylock_", 5);
}
}
测试lua分布式锁
@Autowired
private LuaLockRedisLockManager luaLockRedisLockManager; luaLockRedisLockManager.lockCallBack("distributeLock2" + ThreadLocalRandom.current().nextInt(1000), new SimpleCallBack() {
@Override
public void execute() {
System.out.println("distributeLock2");
}
});

redis命令和lua实现分布式锁的更多相关文章

  1. Redis中是如何实现分布式锁的?

    分布式锁常见的三种实现方式: 数据库乐观锁: 基于Redis的分布式锁: 基于ZooKeeper的分布式锁. 本地面试考点是,你对Redis使用熟悉吗?Redis中是如何实现分布式锁的. 要点 Red ...

  2. Redis的“假事务”与分布式锁

    关注公众号:CoderBuff,回复"redis"获取<Redis5.x入门教程>完整版PDF. <Redis5.x入门教程>目录 第一章 · 准备工作 第 ...

  3. python使用redis实现协同控制的分布式锁

    python使用redis实现协同控制的分布式锁 上午的时候,有个腾讯的朋友问我,关于用zookeeper分布式锁的设计,他的需求其实很简单,就是节点之间的协同合作. 我以前用redis写过一个网络锁 ...

  4. 使用数据库、Redis、ZK分别实现分布式锁!

    分布式锁三种实现方式: 基于数据库实现分布式锁: 基于缓存(Redis等)实现分布式锁: 基于Zookeeper实现分布式锁: 基于数据库实现分布式锁 悲观锁 利用select - where - f ...

  5. 基于redis集群实现的分布式锁,可用于秒杀商品的库存数量管理,有測试代码(何志雄)

    转载请标明出处. 在分布式系统中,常常会出现须要竞争同一资源的情况,本代码基于redis3.0.1+jedis2.7.1实现了分布式锁. redis集群的搭建,请见我的另外一篇文章:<>& ...

  6. 自己动手基于 Redis 实现一个 .NET 的分布式锁

    分布式锁的核心其实就是采用一个集中式的服务,然后多个应用节点进行抢占式锁定来进行实现,今天介绍如何采用Redis作为基础服务,实现一个分布式锁的类库,本方案不考虑 Redis 集群多节点问题,如果引入 ...

  7. 基于数据库、redis和zookeeper实现的分布式锁

    基于数据库 基于数据库(MySQL)的方案,一般分为3类:基于表记录.乐观锁和悲观锁 基于表记录 用表主键或表字段加唯一性索引便可实现,如下: CREATE TABLE `database_lock` ...

  8. 4、Redis底层原理(持久化+分布式锁)

    Redis底层原理 持久化 Redis虽然是个内存数据库,但是Redis支持RDB和AOF两种持久化机制,将数据写往磁盘,可以有效地避免因进程退出造成的数据丢失问题,当下次重启时利用之前持久化的文件即 ...

  9. 分布式缓存技术redis学习系列(五)——redis实战(redis与spring整合,分布式锁实现)

    本文是redis学习系列的第五篇,点击下面链接可回看系列文章 <redis简介以及linux上的安装> <详细讲解redis数据结构(内存模型)以及常用命令> <redi ...

  10. 使用redis设计一个简单的分布式锁

    最近看了有关redis的一些东西,了解了redis的一下命令,就记录一下: redis中的setnx命令: 关于redis的操作命令,我们一般会使用set,get等一系列操作,数据结构也有很多,这里我 ...

随机推荐

  1. 线上RocktMQ重复投递半事务消息故障排查

    1. 故障现象 2020-11-18 10:40开始,业务线反馈线上收到大量的重复MQ半事务消息,导致容器资源消耗急剧攀升,经查看MQ日志,发现broker-b的Master服务,报出大量半事务消息回 ...

  2. Mac下使用Docker快速布署FastGPT实现AI私有知识库

    FastGPT 是一个基于 LLM 大语言模型的知识库问答系统,提供开箱即用的数据处理.模型调用等能力.同时可以通过 Flow 可视化进行工作流编排,从而实现复杂的问答场景! 官网地址为:https: ...

  3. 序列图 时序图 PlantUML vscode drawio 制作

    序列图 时序图 PlantUML vscode drawio 制作 需求 最近发现 序列图 很多文档都用到,而且很好用.经过研究用vscode,idea都可以编写.这里用vscode编写比较简单. d ...

  4. python使用replace将数组写入txt文本

    一 概念 1 Python replace() 方法把字符串中的 old(旧字符串) 替换成 new(新字符串),如果指定第三个参数max,则替换不超过 max 次. 2 用法:str.replace ...

  5. 逆向通达信Level-2 续六 (调试pad控件)

    调试终端面版单元, 以及宿主窗口 调试大数据面版单元, 以及宿主窗口 逆向通达信Level-2 续十一 (无帐号登陆itrend研究版) 逆向通达信Level-2 续十 (trace脱壳) 逆向通达信 ...

  6. [置顶] 彻底停止运行线程池ThreadPoolExecutor

    最近系统开发时遇到这样一个需求: 该功能执行时间很久,如果运行过程出现错误,也无法将其停止,必须眼睁睁的看着它浪费很久时间,除非停止服务器. 于是,我就想着如何给该功能加上一个"停止&quo ...

  7. php处理序列化jQuery serializeArray数据

    介绍jquery的几个常用处理表单的函数: 1.序列化表单内容元素为字符串,常用于ajax提交. $("form").serialize() 2. serializeArray() ...

  8. 干货分享 | 3个Zbrush实用减面工具分享

    一.使用Sculptris Pro Sculptris Pro是zbrush中的一个功能按钮,点击此工具按钮,同时将笔刷转换至standard笔刷,即可减去需要平滑的面. 点击开启Sculptris ...

  9. 使用docker运行nginx服务,挂载自定义配置文件

    错误命令: 下面的方式,启动容器时,-d 后面跟一个指定容器ID的参数写在前面,导致容器不能正常启动,出现异常 docker run --name testnginx -d 7f0fd59e0094  ...

  10. 手把手带你用香橙派AIpro开发AI推理应用

    本文分享自华为云社区<如何基于香橙派AIpro开发AI推理应用>,作者:昇腾CANN. 01 简介 香橙派AIpro开发板采用昇腾AI技术路线,接口丰富且具有强大的可扩展性,提供8/20T ...