抽象类RedisLock实现java.util.concurrent包下的Lock接口,然后对一些方法提供默认实现,子类只需实现lock方法和unlock方法即可。代码如下:
public abstract class RedisLock implements Lock {
protected Jedis jedis;
protected String lockKey;
public RedisLock(Jedis jedis,String lockKey) {
this(jedis, lockKey);
}
public void sleepBySencond(int sencond){
try {
Thread.sleep(sencond*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void lockInterruptibly(){}
@Override
public Condition newCondition() {
return null;
}
@Override
public boolean tryLock() {
return false;
}
@Override
public boolean tryLock(long time, TimeUnit unit){
return false;
}
}
public class LockCase1 extends RedisLock {
public LockCase1(Jedis jedis, String name) {
super(jedis, name);
}
@Override
public void lock() {
while(true){
String result = jedis.set(lockKey, "value", "NX");
if(OK.equals(result)){
System.out.println(Thread.currentThread().getId()+"加锁成功!");
break;
}
}
}
@Override
public void unlock() {
jedis.del(lockKey);
}
}
假设有两个客户端A和B,A获取到分布式的锁。A执行了一会,突然A所在的服务器断电了(或者其他什么的),也就是客户端A挂了。这时出现一个问题,这个锁一直存在,且不会释放,其他客户端永远获取不到锁。
public void lock() {
while(true){
String result = jedis.set(lockKey, "value", NOT_EXIST,SECONDS,30);
if(OK.equals(result)){
System.out.println(Thread.currentThread().getId()+"加锁成功!");
break;
}
}
}
这时又出现一个问题,问题出现的步骤如下:
抽象类RedisLock增加lockValue字段,lockValue字段的默认值为UUID随机值假设当前线程ID。
import redis.clients.jedis.Jedis;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
public abstract class RedisLock implements Lock {
protected Jedis jedis;
protected String lockKey;
protected String lockValue;
public RedisLock(Jedis jedis,String lockKey) {
this(jedis, lockKey, UUID.randomUUID().toString()+Thread.currentThread().getId());
}
public RedisLock(Jedis jedis, String lockKey, String lockValue) {
this.jedis = jedis;
this.lockKey = lockKey;
this.lockValue = lockValue;
}
....
}
实现类里lock和unlock方法
public void lock() {
while(true){
String result = jedis.set(lockKey, lockValue, NOT_EXIST,SECONDS, 30);
if(OK.equals(result)){
System.out.println(Thread.currentThread().getId()+"加锁成功!");
break;
}
}
}
...
public void unlock() {
String lockValue = jedis.get(lockKey);
if (lockValue.equals(lockValue)){
jedis.del(lockKey);
}
}
问题在于如果调用jedis.del()方法的时候,这把锁已经不属于当前客户端的时候会解除他人加的锁。那么是否真的有这种场景?答案是肯定的,比如客户端A加锁,一段时间之后客户端A解锁,在执行jedis.del()之前,锁突然过期了,此时客户端B尝试加锁成功,然后客户端A再执行del()方法,则将客户端B的锁给解除了。
根本原因是if判断和del操作不是原子性的。
public void unlock() {
// 使用lua脚本进行原子删除操作
String checkAndDelScript = "if redis.call('get', KEYS[1]) == ARGV[1] then" +
"return redis.call('del', KEYS[1]) " +
"else " +
"return 0 " +
"end";
jedis.eval(checkAndDelScript, 1, lockKey, lockValue);
}
这时还有最后一个问题:过期时间如何保证大于业务执行时间。
抽象类中RedisLock增加一个boolean类型的属性isOpenExpirationRenewal,用来标识是否开启定时刷新过期时间。
在增加一个scheduleExpirationRenewal方法用于开启刷新过期时间的线程。
package com.zhuawa.course.biz.redis.distributedlock;
import redis.clients.jedis.Jedis;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
public abstract class RedisLock implements Lock {
protected Jedis jedis;
protected String lockKey;
protected String lockValue;
protected volatile boolean isOpenExpirationRenewal = true;
public RedisLock(Jedis jedis,String lockKey) {
this(jedis, lockKey, UUID.randomUUID().toString()+Thread.currentThread().getId());
}
public RedisLock(Jedis jedis, String lockKey, String lockValue) {
this.jedis = jedis;
this.lockKey = lockKey;
this.lockValue = lockValue;
}
public void sleepBySencond(int sencond){
try {
Thread.sleep(sencond*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 开启定时刷新
*/
protected void scheduleExpirationRenewal(){
Thread renewalThread = new Thread(new ExpirationRenewal());
renewalThread.start();
}
/**
* 刷新key的过期时间
*/
private class ExpirationRenewal implements Runnable{
@Override
public void run() {
while (isOpenExpirationRenewal){
System.out.println("执行延迟失效时间中...");
String checkAndExpireScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('expire',KEYS[1],ARGV[2]) " +
"else " +
"return 0 end";
jedis.eval(checkAndExpireScript, 1, lockKey, lockValue, "30");
//休眠10秒
sleepBySencond(10);
}
}
}
@Override
public void lockInterruptibly(){}
@Override
public Condition newCondition() {
return null;
}
@Override
public boolean tryLock() {
return false;
}
@Override
public boolean tryLock(long time, TimeUnit unit){
return false;
}
}
加锁代码在获取锁成功后将isOpenExpirationRenewal置为true,并且调用scheduleExpirationRenewal方法,开启刷新过期时间的线程。
public void lock() {
while (true) {
String result = jedis.set(lockKey, lockValue, NOT_EXIST, SECONDS, 10);
if (OK.equals(result)) {
System.out.println("线程id:"+Thread.currentThread().getName() + "加锁成功!时间:"+LocalTime.now());
//开启定时刷新过期时间
isOpenExpirationRenewal = true;
scheduleExpirationRenewal();
break;
}
System.out.println("线程id:"+Thread.currentThread().getName() + "获取锁失败,休眠10秒!时间:"+LocalTime.now());
//休眠10秒
sleepBySencond(10);
}
}
解锁代码增加一行代码,将isOpenExpirationRenewal属性置为false,停止刷新过期时间的线程轮询。
public void unlock() {
System.out.println("线程id:"+Thread.currentThread().getId() + "解锁!时间:"+LocalTime.now());
String checkAndDelScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) " +
"else " +
"return 0 " +
"end";
jedis.eval(checkAndDelScript, 1, lockKey, lockValue);
isOpenExpirationRenewal = false;
}
常量类:
public class LockConstants {
public static final String OK = "OK";
/** NX|XX, NX -- Only set the key if it does not already exist. XX -- Only set the key if it already exist. **/
public static final String NOT_EXIST = "NX";
public static final String EXIST = "XX";
/** expx EX|PX, expire time units: EX = seconds; PX = milliseconds **/
public static final String SECONDS = "EX";
public static final String MILLISECONDS = "PX";
private LockConstants() {}
}
抽象类:
import redis.clients.jedis.Jedis;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
public abstract class RedisLock implements Lock {
protected Jedis jedis;
protected String lockKey;
protected String lockValue;
protected volatile boolean isOpenExpirationRenewal = true;
public RedisLock(Jedis jedis,String lockKey) {
this(jedis, lockKey, UUID.randomUUID().toString()+Thread.currentThread().getId());
}
public RedisLock(Jedis jedis, String lockKey, String lockValue) {
this.jedis = jedis;
this.lockKey = lockKey;
this.lockValue = lockValue;
}
public void sleepBySencond(int sencond){
try {
Thread.sleep(sencond*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 开启定时刷新
*/
protected void scheduleExpirationRenewal(){
Thread renewalThread = new Thread(new ExpirationRenewal());
renewalThread.start();
}
/**
* 刷新key的过期时间
*/
private class ExpirationRenewal implements Runnable{
@Override
public void run() {
while (isOpenExpirationRenewal){
System.out.println("执行延迟失效时间中...");
String checkAndExpireScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('expire',KEYS[1],ARGV[2]) " +
"else " +
"return 0 end";
jedis.eval(checkAndExpireScript, 1, lockKey, lockValue, "30");
//休眠10秒
sleepBySencond(10);
}
}
}
@Override
public void lockInterruptibly(){}
@Override
public Condition newCondition() {
return null;
}
@Override
public boolean tryLock() {
return false;
}
@Override
public boolean tryLock(long time, TimeUnit unit){
return false;
}
}
实现类:
import redis.clients.jedis.Jedis;
import java.time.LocalTime;
import static com.zhuawa.course.biz.redis.distributedlock.LockConstants.NOT_EXIST;
import static com.zhuawa.course.biz.redis.distributedlock.LockConstants.OK;
import static com.zhuawa.course.biz.redis.distributedlock.LockConstants.SECONDS;
public class RedisDistributedLock extends RedisLock {
public RedisDistributedLock(Jedis jedis, String lockKey) {
super(jedis, lockKey);
}
@Override
public void lock() {
while (true) {
String result = jedis.set(lockKey, lockValue, NOT_EXIST, SECONDS, 10);
if (OK.equals(result)) {
System.out.println("线程id:"+Thread.currentThread().getName() + "加锁成功!时间:"+LocalTime.now());
//开启定时刷新过期时间
isOpenExpirationRenewal = true;
scheduleExpirationRenewal();
break;
}
System.out.println("线程id:"+Thread.currentThread().getName() + "获取锁失败,休眠10秒!时间:"+LocalTime.now());
//休眠10秒
sleepBySencond(10);
}
}
@Override
public void unlock() {
System.out.println("线程id:"+Thread.currentThread().getId() + "解锁!时间:"+LocalTime.now());
String checkAndDelScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) " +
"else " +
"return 0 " +
"end";
jedis.eval(checkAndDelScript, 1, lockKey, lockValue);
isOpenExpirationRenewal = false;
}
}
setnx+lua
4大要点:
set key value px milliseconds nx