基于Redis实现分布式锁
上次谈到了使用ZK实现分布式锁。话不多扯,下面是实现思路以及代码注释。
分布式锁解决方案
一共有一下几种解决方案
1.采用数据库
不推荐 性能不好
2.基于Redis(setnx方法)
setnx也可以存入key,如果存入成功,返回1,如果该key已经存在,返回0
多个jvm同时在redis创建相同的一个key,使用的是setnx命令,因为redis中的key不允许重复,谁能创建key成功,就获取锁,否则进行等待。
3.基于zk实现分布式锁
zk是分布式协调工具,在分布式解决方案中
当多个客户端(jvm)创建相同的一个锁,同时在zk上创建相同的一个临时节点,因为临时节点的路径保证唯一,只要谁能够创建节点成功,就可以获取锁
没有创建成功,就是失败。当释放锁的时候,采用事件通知,通知给客户端。
核心思路:保证有一个jvm进行操作。
下面是代码实现。
代码实现
LockRedis
用于封装释放锁和获取锁的具体实现。
package com.burgess.net;
import java.util.UUID;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
/**
* 基于Redis实现分布式锁
* @ClassName: LockRedis
* @Description: TODO
* @author BurgessLee
* @date 2019年7月1日
*
*/
public class LockRedis {
//redis线程池
private JedisPool jedisPool;
//创建相同的key名称 value 随机数
private String redisLockKey = "redisLock";
public LockRedis(JedisPool jedisPool) {
this.jedisPool = jedisPool;
}
//核心方法 释放锁和获取锁
/**
* 获取锁
* 使用循环方式重复的获取锁
* 为什么获取锁之后还要设置锁的超时时间?
* 为了防止死锁的产生
* zk也是需要防止死锁?
* 当会话关闭,临时节点节点被删除
* 通过设置链接zk的有效期
* @Title: getRedisLock
* @Description: TODO
* @param @param acquireTime
* @param @param timeOut
* @return void
* @throws
*/
//获取锁
public String getRedisLock(Long acquireTime, Long timeOut) {
Jedis conn = null;
try {
//1.创建redis链接
conn = this.jedisPool.getResource();
//2.定义redis 对应key 的value(UUID)
String identifierValue = UUID.randomUUID().toString();
//3.redis实现分布式锁问题,有两个超时时间问题
// 获取锁的超时时间 如果在规定的时间内获取不到锁,那么直接放弃
// 上锁成功后,对应key有对应的有效期
Long expireLock = timeOut /1000;
//4.使用循环机制保证重复进行尝试获取锁
// 如果没有获取到锁,要在规定的acquireTimeOt时间保证重复进行尝试获取锁
long endTime = System.currentTimeMillis() + acquireTime;
while(System.currentTimeMillis() < endTime) {//一直尝试获取锁
//5.使用setnx命令插入对应的key,如果返回1,成功获取锁
if(conn.setnx(redisLockKey, identifierValue) == 1) {
//锁获取成功
conn.expire(redisLockKey, expireLock.intValue());
return identifierValue;
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//关闭资源链接
if(conn != null) {
conn.close();
}
}
//获取锁失败,或者超时
return null;
}
//释放锁
/**
* 释放锁过程
* @Title: unRedisLock
* @Description: TODO
* @param @param identifierValue
* @return void
* @throws
*/
@SuppressWarnings("null")
public void unRedisLock(String identifierValue) {
//释放锁的时候,有两种方式,1.key自动有有效期 2.自动删除key
Jedis conn = null;
try {
//获取链接
conn = jedisPool.getResource();
if(conn.get(redisLockKey) == identifierValue) {//保证删除的是自己的锁
System.out.println(Thread.currentThread().getName() + "删除,或者释放锁,identifierValue:"+identifierValue);
//删除key 缺点,有可能会出现问题是,自己创建的锁被别人删除了,所以需要保证确实是自己的锁
conn.del(redisLockKey);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//释放资源
if(conn == null) {
conn.close();
}
}
}
}
LockService
主要用于封装获取Redis链接,以及测试分布式锁的释放和获取锁的实现代码。当然后期可以处理成AOP方式,对整个项目需要上锁和释放锁的地方进行处理。
package com.burgess.net.service;
import com.burgess.net.LockRedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
/**
*
* @ClassName: LockService
* @Description: TODO
* @author BurgessLee
* @date 2019年7月1日
*
*/
public class LockService {
@SuppressWarnings("unused")
private static JedisPool pool = null;
static {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
//设置最大连接数
jedisPoolConfig.setMaxTotal(200);
//设置最大空闲数
jedisPoolConfig.setMaxIdle(8);
//设置最大等待时间
jedisPoolConfig.setMaxWaitMillis(1000 * 100);
//在borrow一个jedis实例时,是否验证,如果为true,则所有的jedis实例都是可用的
jedisPoolConfig.setTestOnBorrow(true);
pool = new JedisPool(jedisPoolConfig, "39.107.69.43", 6379, 3000);
}
private LockRedis lockRedis = new LockRedis(pool);
//演示redis实现分布式锁
public void seckill() {
//获取锁
String redisLockId = this.lockRedis.getRedisLock(5000l, 5000l);
if(redisLockId == null) {
System.out.println(Thread.currentThread().getName() +",获取锁失败,原因是获取锁超时");
}
System.out.println(Thread.currentThread().getName() +",获取锁成功,id为" + redisLockId +",执行正常业务逻辑");
//释放锁
this.lockRedis.unRedisLock(redisLockId);
System.out.println(Thread.currentThread().getName() +"释放锁成功");
}
}
ThreadRedis
创建多线程测试代码。
package com.burgess.net;
import com.burgess.net.service.LockService;
/**
*
* @ClassName: ThreadRedis
* @Description: TODO
* @author BurgessLee
* @date 2019年7月1日
*
*/
public class ThreadRedis extends Thread{
private LockService lockService;
public ThreadRedis(LockService lockService) {
this.lockService = lockService;
}
//线程测试
@Override
public void run() {
this.lockService.seckill();
}
}
Test001
测试代码
package com.burgess.net;
import com.burgess.net.service.LockService;
/**
* 基于Redis实现分布式锁
* @ClassName: Test001
* @Description: TODO
* @author BurgessLee
* @date 2019年7月1日
*
*/
public class Test001 {
public static void main(String[] args) {
LockService lockService = new LockService();
//启动50个线程测试
for(int i = 0; i < 50; i++) {
new ThreadRedis(lockService).start();
}
}
}
此处通过多个线程测试代码实现是否有问题。
重点总结
Redis实现分布式锁与Zk实现分布式锁区别:
相同点:集群环境下,保证允许只有一个JVM进行执行程序。
区别:
1.Redis是NoSQL数据库,主要特点是缓存
zk是分布式协调工具,主要用于分布式解决方案
2.实现思路:
核心都是通过获取锁,释放锁,死锁问题
获取锁;
zk:多个客户端(jvm)会在zk上创建同一个临时节点,只要谁能创建成功,那么谁可以获取到锁,因为zk节点命名保证路径唯一,不允许出现重复
Redis:多个客户端(jvm)使用setnx命令会在redis上创建同一个key,只要谁能创建成功,那么谁可以获取到锁。
释放锁:
zk:使用直接关闭当前临时节点的会话链接
因为临时节点的生命周期,与会话绑定一起,如果会话链接关闭的话,该节点也会被删除
其他客户端链接,使用事件监听,如果该临时节点被删除的话,重新进入到获取锁的步骤
Redis:
在释放锁的时候,为了确保锁的一致性问题,再删除Redis key的时候,需要判断同一个锁的ID,才可以删除
共同特征:
如何解决死锁现象
Zk:使用会话有效期方式避免死锁现象
Redis:是对key设置有效期解决死锁问题的
3.性能上:
Redis会好一些,毕竟redis是缓存,直接在内存,zk是持久化硬盘
4.可靠性角度:
zk更加可靠,因为Redis有效期不是很好控制,可能产生延迟(获取锁在设置有效期,所以会产生有效期延迟)。
zk自带有有效期的功能,因为带有临时节点,所以相对来说,zk比redis更好 。
以上是大体上的一些总结,当然会不全,也可能存在一些遗漏或者错误,欢迎浏览补充。
总结
以上就是基于Redis实现分布式锁的整个过程以及代码演示,如果有不对的地方,还请留言。