单机Redis实现分布式互斥锁
0、准备工作
0-1 运行环境
- jdk1.8
- gradle
- 一个能支持以上两者的代码编辑器,作者使用的是IDEA。
0-2 知识储备
- 对Java并发包,互斥锁 有一定的理解。
- 对Redis数据库有一定了解。
- 本案例偏难,案例内有注释讲解,有不明白的地方,或者不正确的地方,欢迎联系作者本人或留言。
1、设计思路
1.1 项目结构
图2——项目结构
/lock/DestributeLock.java:锁的具体实现类,有lock()和unlock()两个方法。
/lock/DestributeLockRepository.java:锁的工厂类,用来配置Redis连接信息,获取锁的实例。
/lock/Expired**.java:Redis的 pub/sub 功能相关类,用来实现超时自动解锁。
/test.java:3个测试类,用来模拟不同的加锁情况。
1.2 实现难点
咱们可以在网上轻松的找到,用Redis实现简单的互斥锁的案例。
那为什么说是简单的?因为不安全:
1.Redis的stNX()与expire()方法是两个独立的操作,即非原子性。咱们可以假设这么一个情况,当你执行stNX()之后,服务器挂了,没有执行expire()方法。那如果没有去解锁的话,是不是就死锁了?所以,咱们需要保证这两个操作的原子性。
2.expire()方法只是告知Redis在一定时间后,自动删除某个键。但是,服务器并不知道expire()在超时之后,是否成功地解锁(删除了key)。所以,咱们需要Redis通知服务器expire()方法已经彻底执行完毕,即Redis已经删除了key,才能确定为解锁状态。
2、具体实现
2.1 DestributeLockRepository.java
public class DistributeLockRepository {
private String host;
private int port;
private int maxTotal;
private JedisPool jedisPool;
/**
* @param host redis地址
* @param port 端口
* @param maxTotal 锁的最大个数,也就是说最多有maxTotal个线程能同时操作锁
*
**/
public DistributeLockRepository(String host,int port,int maxTotal){
this.host = host;
this.port = port;
this.maxTotal = maxTotal;
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(maxTotal);
jedisPool = new JedisPool(jedisPoolConfig, host, port);
}
public DistributeLock instance(String lockname) {
Jedis jedis = jedisPool.getResource();
// 若超过最大连接数,会在这里阻塞
return new DistributeLock(jedis, lockname);
}
}
2.2 DestributeLock.java
public class DistributeLock implements ExpiredListener {
private Jedis redisClient = null;
private String key = ""; //锁的key
private int expire = 0; //Redis自动删除时间
private long startTime = 0L; //尝试获取锁的开始时间
private long lockTime = 0L; //获取到锁的时间
private boolean lock = false; //锁状态
private void setLock(boolean lock) {
this.lock = lock;
}
private void closeClient() {
redisClient.close();
}
private static String script =
"if redis.call('setnx',KEYS[1],KEYS[2]) == 1 then\n"
+ "redis.call('expire',KEYS[1],KEYS[3]);\n"
+ "return 1;\n"
+ "else\n"
+ "return 0;\n"
+ "end\n";
DistributeLock(Jedis jedis, String key) {
this.redisClient = jedis;
this.key = key;
}
@Override
public void onExpired() {
ExpiredManager.remove(key,this);
this.setLock(false);
redisClient.close();//关闭连接
redisClient = null;
System.out.println(key +lockTime+ "Redis超时自动解锁" + Thread.currentThread().getName());
}
//redisClient.psubscribe(new ExpiredSub(this),"__key*__:expired");
/**
* @param timeout 锁阻塞超时的时间 单位:毫秒
* @param expire redis锁超时自动删除的时间 单位:秒
* @return true-加锁成功 false-加锁失败
*/
public synchronized boolean lock(long timeout, int expire) {
this.expire = expire;
this.startTime = System.currentTimeMillis();
if (!lock) {
//System.out.println(Thread.currentThread().getName() + lock);
try {
//在timeout的时间范围内不断轮询锁
while (System.currentTimeMillis() - startTime < timeout) {
//System.out.println(Thread.currentThread().getName() + "inWhile");
//使用Lua脚本保证setnx与expire的原子性
Object object = redisClient.eval(script, 3, key, "a", String.valueOf(expire));
//System.out.println(Thread.currentThread().getName() + "afterScript");
if ((long) object == 1) {
this.lockTime = System.currentTimeMillis();
//锁的情况下锁过期后消失,不会造成永久阻塞
this.lock = true;
System.out.println(key+lockTime + "加锁成功" + Thread.currentThread().getName());
//交给超时管理器
ExpiredManager.add(key, this);
return this.lock;
}
System.out.println(key+lockTime +"出现锁等待" + Thread.currentThread().getName());
//短暂休眠,避免可能的活锁
Thread.sleep(500);
}
System.out.println(key+lockTime +"锁超时" + Thread.currentThread().getName());
} catch (Exception e) {
if(e instanceof NullPointerException){
throw new RuntimeException("无法对已经解锁后的锁重新加锁,请重新获取", e);
}
throw new RuntimeException("locking error", e);
}
} else {
//System.out.println(key + "不可重入/用");
throw new RuntimeException(key +lockTime+ "不可重入/用");
}
this.lock = false;
return this.lock;
}
public synchronized void unlock() {
if (this.lock) {
//解决在 Redis自动删除锁后,尝试解锁的问题
if (System.currentTimeMillis() - lockTime <= expire) {
redisClient.del(key);//直接删除 如果没有key,也没关系,不会有异常
}
this.lock = false;
redisClient.close();//关闭连接
redisClient = null;
System.out.println(key+ lockTime+ "解锁成功" + Thread.currentThread().getName());
}else {
System.out.println(key +lockTime+ "已经解锁" + Thread.currentThread().getName());
}
}
}
2.3 ExpiredManager.java
public class ExpiredManager {
private static final String HOST = "localhost";
private static final Integer PORT = 16379;
private static boolean isStart = false;
private static Jedis jedis;
private static ConcurrentHashMap<String,CopyOnWriteArrayList<ExpiredListener>> locks = new ConcurrentHashMap<>();
public static void add(String key,ExpiredListener listener){
CopyOnWriteArrayList<ExpiredListener> copyOnWriteArrayList = locks.get(key);
if(copyOnWriteArrayList==null){
copyOnWriteArrayList = new CopyOnWriteArrayList<ExpiredListener>();
copyOnWriteArrayList.add(listener);
locks.put(key,copyOnWriteArrayList);
}else {
copyOnWriteArrayList.add(listener);
}
}
public static void remove(String key,ExpiredListener listener){
CopyOnWriteArrayList<ExpiredListener> copyOnWriteArrayList = locks.get(key);
if(copyOnWriteArrayList!=null){
copyOnWriteArrayList.remove(listener);
}
}
public synchronized static void start(){
if(!isStart) {
isStart = true;
jedis = new Jedis(HOST, PORT);
new Thread(new Runnable() {
@Override
public void run() {
try {
jedis.psubscribe(new ExpiredSub(locks), "__key*__:expired");
}catch (Exception e){
System.out.println(e.getMessage());
}
}
}).start();
}
}
public synchronized static void close(){
if(isStart) {
isStart = false;
jedis.close();
}
}
}
2.4 测试
public class Test3 {
public static void main(String[] args) {
//第三个参数表示 同一时间 最多有多少锁能 处于加锁或者阻塞状态 其实就是连接池大小
DistributeLockRepository distributeLockRepository = new DistributeLockRepository("localhost", 16379, 6);
//获取锁实例
DistributeLock lock1 = distributeLockRepository.instance("lock[A]");
DistributeLock lock2 = distributeLockRepository.instance("lock[A]");
//开启超时解锁管理器
ExpiredManager.start();
//lock1和lock2其实模拟的是两个消费者,对同一资源(lock[A])的竞争使用
lock1.lock(1000 * 20L, 5);
try {
Thread.sleep(6000);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock2.lock(1000 * 20L, 5);
lock1.unlock();
System.out.println("----");
//关闭超时解锁管理器
ExpiredManager.close();
}
}
Test3的运行结果:
3、总结
上面是贴出的主要代码,完整的请下载demo包,有不明白的地方请在下方评论,或者联系邮箱yaoyunxiaoli@163.com。
我是妖云小离,这是我第一次在Demo大师上发文章,感谢阅读。单机Redis实现分布式互斥锁
注:本文著作权归作者,由demo大师代发,拒绝转载,转载需要作者授权
单机Redis实现分布式互斥锁的更多相关文章
- 基于(Redis | Memcache)实现分布式互斥锁
设计一个缓存系统,不得不要考虑的问题就是:缓存穿透.缓存击穿与失效时的雪崩效应. 缓存击穿 缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则 ...
- 基于Zookeeper实现的分布式互斥锁 - InterProcessMutex
Curator是ZooKeeper的一个客户端框架,其中封装了分布式互斥锁的实现,最为常用的是InterProcessMutex,本文将对其进行代码剖析 简介 InterProcessMutex基于Z ...
- 高并发场景系列(一) 利用redis实现分布式事务锁,解决高并发环境下减库存
原文:http://blog.csdn.net/heyewu4107/article/details/71009712 高并发场景系列(一) 利用redis实现分布式事务锁,解决高并发环境下减库存 问 ...
- 利用redis实现分布式事务锁,解决高并发环境下库存扣减
利用redis实现分布式事务锁,解决高并发环境下库存扣减 问题描述: 某电商平台,首发一款新品手机,每人限购2台,预计会有10W的并发,在该情况下,如果扣减库存,保证不会超卖 解决方案一 利用数据 ...
- 【redis】基于redis实现分布式并发锁
基于redis实现分布式并发锁(注解实现) 说明 前提, 应用服务是分布式或多服务, 而这些"多"有共同的"redis"; (2017-12-04) 笑哭, 写 ...
- 基于单机redis的分布式锁实现
最近我们有个服务经常出现存储的数据出现重复,首先上一个系统流程图: 用户通过http请求可以通知任务中心结束掉自己发送的任务,这时候任务中心会通过MQ通知结束服务去结束任务保存数据,由于任务结束数据计 ...
- springboot 中单机 redis 实现分布式锁
在微服务中经常需要使用分布式锁,来执行一些任务.例如定期删除过期数据,在多个服务中只需要一个去执行即可. 以下说明非严格意义的分布式锁,因为 redis 实现严格意义的分布式锁还是比较复杂的,对于日常 ...
- 基于redis的分布式锁(转)
基于redis的分布式锁 1 介绍 这篇博文讲介绍如何一步步构建一个基于Redis的分布式锁.会从最原始的版本开始,然后根据问题进行调整,最后完成一个较为合理的分布式锁. 本篇文章会将分布式锁的实现分 ...
- 基于redis的分布式锁(不适合用于生产环境)
基于redis的分布式锁 1 介绍 这篇博文讲介绍如何一步步构建一个基于Redis的分布式锁.会从最原始的版本开始,然后根据问题进行调整,最后完成一个较为合理的分布式锁. 本篇文章会将分布式锁的实现分 ...
随机推荐
- POCO C++ SOCKET
// client program #include "Poco/Net/DatagramSocket.h" #include "Poco/Net/SocketAddre ...
- BufferedInputStream&BufferedOutputStream
使用字符缓冲区相关实现copy文件: public static void main(String[] args) { //创建文件对象指定要拷贝的文件路径(源文件),文件须存在,测试用例不做判断 F ...
- Windows环境Vim编辑器如何执行Ruby代码
1.下载 Ruby 1.8.5(2006-8-25) for Windows: 在网页http://www.rubychina.net/downloads/ 上找到 --〉Ruby on Windo ...
- 用jsp开发web应用并不是一个高效率的选择
1. Android里有办法让js使用java方法像使用自己的方法一样方便,和flex的很相似,flex里面使用java方法就像使用自己的方法一样. 2. 用Flex开发准确的说就是用as ...
- Visual Studio跨平台开发(1):Hello Xamarin!
前言 应用程序发展的脚步, 从来没有停过. 从早期的Windows 应用程序, 到网络时代的web 应用程序, 再到近几年相当盛行的行动装置应用程序(Mobile Application), 身为C# ...
- 牛客练习赛16 B 漂亮的树【哈希hash/思维】
链接:https://www.nowcoder.com/acm/contest/84/B 来源:牛客网 题目描述 街上有n棵树,标号为1...n,第i棵树的高度为ai. 定义这n棵树是漂亮的,当且仅当 ...
- Cookie和Session在Node.JS中的实践(三)
Cookie和Session在Node.JS中的实践(三) 前面作者写的COOKIE篇.SESSION篇,算是已经比较详细的说明了两者间的区别.机制.联系了.阅读时间可能稍长,因为作者本身作图也做了不 ...
- mysql里的知识
1.mysql基础 (1)mysql存储结构:数据库->表-> 数据 sql语句 (2)管理数据库: 增加: create database 数据库 default character ...
- SQL Server 2008 R2 Build List
By Steve Jones, 2014/09/30 (first published: 2010/05/25) This is a list of the builds for SQL Server ...
- 链接服务器 "(null)" 的 OLE DB 访问接口 "SQLNCLI11" 指示该对象没有列,或当前用户没有访问该对象的权限。
原文:链接服务器 "(null)" 的 OLE DB 访问接口 "SQLNCLI11" 指示该对象没有列,或当前用户没有访问该对象的权限. SELECT * F ...