springmvc单Redis实例实现分布式锁(解决锁超时问题)
一、前言
关于redis分布式锁, 查了很多资料, 发现很多只是实现了最基础的功能, 但是, 并没有解决当锁已超时而业务逻辑还未执行完的问题, 这样会导致: A线程超时时间设为10s(为了解决死锁问题), 但代码执行时间可能需要30s, 然后redis服务端10s后将锁删除, 此时, B线程恰好申请锁, redis服务端不存在该锁, 可以申请, 也执行了代码, 那么问题来了, A、B线程都同时获取到锁并执行业务逻辑, 这与分布式锁最基本的性质相违背: 在任意一个时刻, 只有一个客户端持有锁, 即独享
为了解决这个问题, 本文将用完整的代码和测试用例进行验证, 希望能给小伙伴带来一点帮助
二、准备工作
也可以直接官网下载, 我这边都整理到网盘了
需要postman是因为我还没找到jmeter多开窗口的办法, 哈哈
三、说明
springmvc项目
maven依赖
<!--redis-->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>1.6.5.RELEASE</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.7.3</version>
</dependency>
- 核心类
分布式锁工具类: DistributedLock
测试接口类: PcInformationServiceImpl
锁延时守护线程类: PostponeTask
四、实现思路
先测试在不开启锁延时线程的情况下, A线程超时时间设为10s, 执行业务逻辑时间设为30s, 10s后, 调用接口, 查看是否能够获取到锁, 如果获取到, 说明存在线程安全性问题
同上, 在加锁的同时, 开启锁延时线程, 调用接口, 查看是否能够获取到锁, 如果获取不到, 说明延时成功, 安全性问题解决
五、实现
- 版本01代码
1)、DistributedLock
package com.cn.pinliang.common.util;
import com.cn.pinliang.common.thread.PostponeTask;
import com.google.common.collect.Lists;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;
import java.io.Serializable;
import java.util.Collections;
@Component
public class DistributedLock {
@Autowired
private RedisTemplate<Serializable, Object> redisTemplate;
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";
// 解锁脚本(lua)
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";
/**
* 分布式锁
* @param key
* @param value
* @param expireTime 单位: 秒
* @return
*/
public boolean lock(String key, String value, long expireTime) {
return redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> {
Jedis jedis = (Jedis) redisConnection.getNativeConnection();
String result = jedis.set(key, value, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
if (LOCK_SUCCESS.equals(result)) {
return Boolean.TRUE;
}
return Boolean.FALSE;
});
}
/**
* 解锁
* @param key
* @param value
* @return
*/
public Boolean unLock(String key, String value) {
return redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> {
Jedis jedis = (Jedis) redisConnection.getNativeConnection();
Object result = jedis.eval(RELEASE_LOCK_SCRIPT, Collections.singletonList(key), Collections.singletonList(value));
if (RELEASE_SUCCESS.equals(result)) {
return Boolean.TRUE;
}
return Boolean.FALSE;
});
}
}
说明: 就2个方法, 加锁解锁, 加锁使用jedis setnx方法, 解锁执行lua脚本, 都是原子性操作
2)、PcInformationServiceImpl
public JsonResult add() throws Exception {
String key = "add_information_lock";
String value = RandomUtil.produceStringAndNumber(10);
long expireTime = 10L;
boolean lock = distributedLock.lock(key, value, expireTime);
String threadName = Thread.currentThread().getName();
if (lock) {
System.out.println(threadName + " 获得锁...............................");
Thread.sleep(30000);
distributedLock.unLock(key, value);
System.out.println(threadName + " 解锁了...............................");
} else {
System.out.println(threadName + " 未获取到锁...............................");
return JsonResult.fail("未获取到锁");
}
return JsonResult.succeed();
}
说明: 测试类很简单, value随机生成, 保证唯一, 不会在超时情况下解锁其他客户端持有的锁
3)、打开redis-desktop-manager客户端, 刷新缓存, 可以看到, 此时是没有add_information_lock
的key的
4)、启动jmeter, 调用接口测试
设置5个线程同时访问, 在10s的超时时间内查看redis, add_information_lock
存在, 多次调接口, 只有一个线程能够获取到锁
redis
1-4个请求, 都未获取到锁
第5个请求, 获取到锁
OK, 目前为止, 一切正常, 接下来测试10s之后, A仍在执行业务逻辑, 看别的线程是否能获取到锁
可以看到, 操作成功, 说明A和B同时执行了这段本应该独享的代码, 需要优化
- 版本02代码
1)、DistributedLock
package com.cn.pinliang.common.util;
import com.cn.pinliang.common.thread.PostponeTask;
import com.google.common.collect.Lists;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;
import java.io.Serializable;
import java.util.Collections;
@Component
public class DistributedLock {
@Autowired
private RedisTemplate<Serializable, Object> redisTemplate;
private static final Long RELEASE_SUCCESS = 1L;
private static final Long POSTPONE_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";
// 解锁脚本(lua)
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";
// 延时脚本
private static final String POSTPONE_LOCK_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('expire', KEYS[1], ARGV[2]) else return '0' end";
/**
* 分布式锁
* @param key
* @param value
* @param expireTime 单位: 秒
* @return
*/
public boolean lock(String key, String value, long expireTime) {
// 加锁
Boolean locked = redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> {
Jedis jedis = (Jedis) redisConnection.getNativeConnection();
String result = jedis.set(key, value, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
if (LOCK_SUCCESS.equals(result)) {
return Boolean.TRUE;
}
return Boolean.FALSE;
});
if (locked) {
// 加锁成功, 启动一个延时线程, 防止业务逻辑未执行完毕就因锁超时而使锁释放
PostponeTask postponeTask = new PostponeTask(key, value, expireTime, this);
Thread thread = new Thread(postponeTask);
thread.setDaemon(Boolean.TRUE);
thread.start();
}
return locked;
}
/**
* 解锁
* @param key
* @param value
* @return
*/
public Boolean unLock(String key, String value) {
return redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> {
Jedis jedis = (Jedis) redisConnection.getNativeConnection();
Object result = jedis.eval(RELEASE_LOCK_SCRIPT, Collections.singletonList(key), Collections.singletonList(value));
if (RELEASE_SUCCESS.equals(result)) {
return Boolean.TRUE;
}
return Boolean.FALSE;
});
}
/**
* 锁延时
* @param key
* @param value
* @param expireTime
* @return
*/
public Boolean postpone(String key, String value, long expireTime) {
return redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> {
Jedis jedis = (Jedis) redisConnection.getNativeConnection();
Object result = jedis.eval(POSTPONE_LOCK_SCRIPT, Lists.newArrayList(key), Lists.newArrayList(value, String.valueOf(expireTime)));
if (POSTPONE_SUCCESS.equals(result)) {
return Boolean.TRUE;
}
return Boolean.FALSE;
});
}
}
说明: 新增了锁延时方法, lua脚本, 自行脑补相关语法
2)、PcInformationServiceImpl不需要改动
3)、PostponeTask
package com.cn.pinliang.common.thread;
import com.cn.pinliang.common.util.DistributedLock;
public class PostponeTask implements Runnable {
private String key;
private String value;
private long expireTime;
private boolean isRunning;
private DistributedLock distributedLock;
public PostponeTask() {
}
public PostponeTask(String key, String value, long expireTime, DistributedLock distributedLock) {
this.key = key;
this.value = value;
this.expireTime = expireTime;
this.isRunning = Boolean.TRUE;
this.distributedLock = distributedLock;
}
@Override
public void run() {
long waitTime = expireTime * 1000 * 2 / 3;// 线程等待多长时间后执行
while (isRunning) {
try {
Thread.sleep(waitTime);
if (distributedLock.postpone(key, value, expireTime)) {
System.out.println("延时成功...........................................................");
} else {
this.stop();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
private void stop() {
this.isRunning = Boolean.FALSE;
}
}
说明: 调用lock同时, 立即开启PostponeTask线程, 线程等待超时时间的2/3时间后, 开始执行锁延时代码, 如果延时成功, add_information_lock
这个key会一直存在于redis服务端, 直到业务逻辑执行完毕, 因此在此过程中, 其他线程无法获取到锁, 也即保证了线程安全性
下面是测试结果
10s后, 查看redis服务端, add_information_lock
仍存在, 说明延时成功
此时用postman再次请求, 发现获取不到锁
看一下控制台打印
A线程在19:09:11获取到锁, 在10 * 2 / 3 = 6s后进行延时, 成功, 保证了业务逻辑未执行完毕的情况下不会释放锁
A线程执行完毕, 锁释放, 其他线程又可以竞争锁
OK, 目前为止, 解决了锁超时而业务逻辑仍在执行的锁冲突问题, 还很简陋, 而最严谨的方式还是使用官方的 Redlock 算法实现, 其中 Java 包推荐使用 redisson, 思路差不多其实, 都是在快要超时时续期, 以保证业务逻辑未执行完毕不会有其他客户端持有锁
后面学习redisson, 看一下大神是怎么实现的
如果有什么不对的或者可以优化的希望小伙伴多多指教, 留言评论什么的, 谢谢
参考文章: https://blog.csdn.net/weixin_33943347/article/details/88009397
springmvc单Redis实例实现分布式锁(解决锁超时问题)的更多相关文章
- 高可用Redis服务架构分析与搭建(单redis实例)
原文地址:https://www.cnblogs.com/xuning/p/8464625.html 基于内存的Redis应该是目前各种web开发业务中最为常用的key-value数据库了,我们经常在 ...
- redis过期策略+事务+分布式锁+单redis服务器锁
过期策略 相关知识:redis 内存数据集大小上升到一定大小的时候,就会施行数据淘汰策略.redis 提供 6种数据淘汰策略: voltile-lru:从已设置过期时间的数据集(server.db[i ...
- springMVC 实现redis分布式锁
1.先配置spring-data-redis 首先是依赖 <dependency> <groupId>org.springframework.data</groupId& ...
- 高并发场景系列(一) 利用redis实现分布式事务锁,解决高并发环境下减库存
原文:http://blog.csdn.net/heyewu4107/article/details/71009712 高并发场景系列(一) 利用redis实现分布式事务锁,解决高并发环境下减库存 问 ...
- 利用redis实现分布式事务锁,解决高并发环境下库存扣减
利用redis实现分布式事务锁,解决高并发环境下库存扣减 问题描述: 某电商平台,首发一款新品手机,每人限购2台,预计会有10W的并发,在该情况下,如果扣减库存,保证不会超卖 解决方案一 利用数据 ...
- redis分布式锁解决超卖问题
redis事务 redis事务介绍: 1. redis事务可以一次执行多个命令,本质是一组命令的集合. 2.一个事务中的所有命令都会序列化,按顺序串行化的执行而不会被其他命令插入 作用:一个队列 ...
- redis中的分布式锁
分布式锁的实现场景 在平时的开发中,对于高并发的开发场景,我们不可避免要加锁进行处理,当然redis中也是不可避免的,下面是我总结出来的几种锁的场景 Redis分布式锁方案一 使用Redis实现分布式 ...
- Redis分布式锁----悲观锁实现,以秒杀系统为例
摘要:本文要实现的是一种使用redis来实现分布式锁. 1.分布式锁 分布式锁在是一种用来安全访问分式式机器上变量的安全方案,一般用在全局id生成,秒杀系统,全局变量共享.分布式事务等.一般会有两种实 ...
- redis客户端、分布式锁及数据一致性
Redis Java客户端有很多的开源产品比如Redission.Jedis.lettuce等. Jedis是Redis的Java实现的客户端,其API提供了比较全面的Redis命令的支持:Redis ...
随机推荐
- PCL配置即常见问题
1 下载 把与VS版本对应PCL的AllInOne包下载下来.要下对安装包,需要了解安装包的命名的含义,以下面的一个AllInOne包的名字为例. PCL-1.8.0-AllInOne-msvc ...
- 学习JDK1.8集合源码之--LinkedList
1. LinkedList简介 LinkedList是一种可以在任何位置进行高效地插入和移除操作的有序序列,它是基于双向链表实现的.因为它实现了Deque接口,所以也是双端队列的一种实现. 2.Lin ...
- 用Direct2D和DWM来做简单的动画效果2
原文:用Direct2D和DWM来做简单的动画效果2 版权声明:本文为博主原创文章,未经博主允许不得转载. https://blog.csdn.net/sunnyloves/article/detai ...
- oracle-ASM存储器
自动存储管理 Oracle 10g引进的一种新型存储机制.它依靠oracle来维护企业的数据库存储器,被设计用来解除磁盘和存储器管理的负担,可以使用ASM来定义用于文件管理的磁盘组. 磁盘组类似于某些 ...
- ls command not found
编辑profile文件没有写正确,导致在命令行下 ls等命令不能够识别. 在命令行下打入下面这段就可以了 export PATH=/usr/local/sbin:/usr/local/bin:/sbi ...
- 【linux】Ubuntu16.04中文输入法安装
最近刚给笔记本装了Ubuntu+win10双系统,但是ubuntu16.04没有自带中文输入法,所以经过网上的一些经验搜索整合,分享一下安装中文输入法的心得.本文主要介绍了谷歌拼音跟ibus中文输入法 ...
- linux下播放器设计和开发
http://blog.csdn.net/henryjee/article/details/6737392 本文根据DawnLightPlayer的开发经验写成.DawnLithtPlayer是今天3 ...
- Framework7 无限滚动
html结构 <div class="page"> <div class="page-content infinite-scroll"> ...
- oracle-Restrict权限
启动数据库并把它置于open模式,直给拥有restricted session权限的用户赋予访问权. Alter system disable restricted session; 另外,启动时,o ...
- oracle-Immediate
从shutdown immediate命令发布起,禁止建立任何新的oracle连接 未提交的事务被回退.因此,处于一个事务中间的用户将失去所有未提交的劳动成果. oracle不等待客户断开连接.任何未 ...