总结:为了防止线程处理时间超过锁的自动释放时间后,出现误删除其他线程锁的情况,会在手动删除锁的时候判断该锁是否属于此线程,不属于则不删除。为了实现锁的重入,可以通过redis的hash数据结构来构造锁,hash表的field作为线程唯一id,value设置为重入次数即可。
要防止删除别人的锁其实也很简单,就是把redis中锁的值设置成当前线程的唯一标识,然后删除的时候校对下,如果对不上就不删除了。下面代码采用uuid+当前线程id的方式生成。
package com.dazhiyouqiu.api;
import java.util.UUID;
public class test {
public static void main(String[] args) {
//监听线程
Thread monitor = new Thread(new Runnable() {
@Override
public void run() {
int time = 0;
Redis ru = new Redis();
while (time < 100000)
try {
Thread.sleep(500);
time = time + 500;
System.out.println("Time:" + time + "---->" + ru.get("lock"));
} catch (Exception e) {
}
}
});
monitor.start();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
Redis ru = new Redis();
//加锁,默认没获得锁
Boolean notGetLock = true;
String uuid= UUID.randomUUID().toString()+"_"+Thread.currentThread().getId();
//一直等到获得锁,再往下执行
String getLockResult = null;
while (notGetLock) {
getLockResult = ru.set("lock",uuid, 5);
if (getLockResult != null)
notGetLock = false;
}
try {
Thread.sleep(6000);
} catch (Exception e) {
}
//解锁
if(ru.get("lock").equals(uuid))
ru.delete("lock");
else {
System.out.println("当前线程"+Thread.currentThread().getName()+"无法删除锁,uuid不对");
}
}
});
thread1.start();
//等待5s后再启动线程2
try {
Thread.sleep(5000);
} catch (Exception e) {
}
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
Redis ru = new Redis();
//加锁,默认没获得锁
Boolean notGetLock = true;
//生成UUID作为value存储在redis
String uuid= UUID.randomUUID().toString()+"_"+Thread.currentThread().getId();
//一直等到获得锁,再往下执行
String getLockResult = null;
while (notGetLock) {
getLockResult = ru.set("lock", uuid, 5);
if (getLockResult != null)
notGetLock = false;
}
try {
Thread.sleep(4000);
} catch (Exception e) {
}
//解锁
if(ru.get("lock").equals(uuid))
ru.delete("lock");
else {
System.out.println("当前线程"+Thread.currentThread().getName()+"无法删除锁,uuid不对");
}
}
});
thread2.start();
}
}
运行结果如下:
Time:500---->ee9bf6b2-82b5-4891-9d41-3c3761ad05c8_14
Time:1000---->ee9bf6b2-82b5-4891-9d41-3c3761ad05c8_14
Time:1500---->ee9bf6b2-82b5-4891-9d41-3c3761ad05c8_14
Time:2000---->ee9bf6b2-82b5-4891-9d41-3c3761ad05c8_14
Time:2500---->ee9bf6b2-82b5-4891-9d41-3c3761ad05c8_14
Time:3000---->ee9bf6b2-82b5-4891-9d41-3c3761ad05c8_14
Time:3500---->ee9bf6b2-82b5-4891-9d41-3c3761ad05c8_14
Time:4000---->ee9bf6b2-82b5-4891-9d41-3c3761ad05c8_14
Time:4500---->ee9bf6b2-82b5-4891-9d41-3c3761ad05c8_14
Time:5000---->af60b26e-ca2b-4be9-99de-3e1dc53abd18_17
Time:5500---->af60b26e-ca2b-4be9-99de-3e1dc53abd18_17
当前线程Thread-1无法删除锁,uuid不对
Time:6000---->af60b26e-ca2b-4be9-99de-3e1dc53abd18_17
Time:6500---->af60b26e-ca2b-4be9-99de-3e1dc53abd18_17
Time:7000---->af60b26e-ca2b-4be9-99de-3e1dc53abd18_17
Time:7500---->af60b26e-ca2b-4be9-99de-3e1dc53abd18_17
Time:8000---->af60b26e-ca2b-4be9-99de-3e1dc53abd18_17
Time:8500---->af60b26e-ca2b-4be9-99de-3e1dc53abd18_17
Time:9000---->null
Time:9500---->null
Time:10000---->null
Time:10500---->null
Time:11000---->null
Time:11500---->null
这样看似能解决问题,但是我们在第一节说过,如果在java中将两个原子操作拼接在一起的话依旧会有并发问题。我们在后面将查询锁和删除的操作分开了,所以就不是原子操作了。为了解决这个问题我们采用lua语言来操作。采用lua语言来操作redis的好处有这些:
减少网络开销:多个请求通过脚本一次发送,减少网络延迟
原子操作:将脚本作为一个整体执行,中间不会插入其他命令,无需使用事务
复用:客户端发送的脚本永久存在redis中,其他客户端可以复用脚本
可嵌入性:可嵌入JAVA,C#等多种编程语言,支持不同操作系统跨平台交互
所以代码可以改成如下:
package com.dazhiyouqiu.api;
import java.util.UUID;
public class test {
public static void main(String[] args) {
//监听线程
Thread monitor = new Thread(new Runnable() {
@Override
public void run() {
int time = 0;
Redis ru = new Redis();
while (time < 100000)
try {
Thread.sleep(500);
time = time + 500;
System.out.println("Time:" + time + "---->" + ru.get("lock"));
} catch (Exception e) {
}
}
});
monitor.start();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
Redis ru = new Redis();
//加锁,默认没获得锁
Boolean notGetLock = true;
String uuid= UUID.randomUUID().toString()+"_"+Thread.currentThread().getId();
//一直等到获得锁,再往下执行
String getLockResult = null;
while (notGetLock) {
getLockResult = ru.set("lock",uuid, 5);
if (getLockResult != null)
notGetLock = false;
}
try {
Thread.sleep(6000);
} catch (Exception e) {
}
//解锁
if(ru.deleteWithValue("lock",uuid)){
System.out.println("当前线程"+Thread.currentThread().getName()+"手动解锁成功");
}
else {
System.out.println("当前线程"+Thread.currentThread().getName()+"无法删除锁,uuid不对");
}
}
});
thread1.start();
//等待5s后再启动线程2
try {
Thread.sleep(5000);
} catch (Exception e) {
}
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
Redis ru = new Redis();
//加锁,默认没获得锁
Boolean notGetLock = true;
//生成UUID作为value存储在redis
String uuid= UUID.randomUUID().toString()+"_"+Thread.currentThread().getId();
//一直等到获得锁,再往下执行
String getLockResult = null;
while (notGetLock) {
getLockResult = ru.set("lock", uuid, 5);
if (getLockResult != null)
notGetLock = false;
}
try {
Thread.sleep(4000);
} catch (Exception e) {
}
//解锁
if(ru.deleteWithValue("lock",uuid)){
System.out.println("当前线程"+Thread.currentThread().getName()+"手动解锁成功");
}
else {
System.out.println("当前线程"+Thread.currentThread().getName()+"无法删除锁,uuid不对");
}
}
});
thread2.start();
}
}
其中ru.deleteWithValue()方法的代码如下:
public boolean deleteWithValue(String key,String value) {
String luaScript = "if redis.call('get',KEYS[1]) == ARGV[1] then " +
"return redis.call('del',KEYS[1]) else return 0 end";
return jedis.eval(luaScript, Collections.singletonList(key), Collections.singletonList(value)).equals(1L);
}
测试的结果如下:
Time:500---->d92dcae5-876c-4778-bef5-a2c33effaf15_14
Time:1000---->d92dcae5-876c-4778-bef5-a2c33effaf15_14
Time:1500---->d92dcae5-876c-4778-bef5-a2c33effaf15_14
Time:2000---->d92dcae5-876c-4778-bef5-a2c33effaf15_14
Time:2500---->d92dcae5-876c-4778-bef5-a2c33effaf15_14
Time:3000---->d92dcae5-876c-4778-bef5-a2c33effaf15_14
Time:3500---->d92dcae5-876c-4778-bef5-a2c33effaf15_14
Time:4000---->d92dcae5-876c-4778-bef5-a2c33effaf15_14
Time:4500---->d92dcae5-876c-4778-bef5-a2c33effaf15_14
Time:5000---->3f9bacca-f3de-4e3e-b1d5-be2cdc03f76f_17
Time:5500---->3f9bacca-f3de-4e3e-b1d5-be2cdc03f76f_17
当前线程Thread-1无法删除锁,uuid不对
Time:6000---->3f9bacca-f3de-4e3e-b1d5-be2cdc03f76f_17
Time:6500---->3f9bacca-f3de-4e3e-b1d5-be2cdc03f76f_17
Time:7000---->3f9bacca-f3de-4e3e-b1d5-be2cdc03f76f_17
Time:7500---->3f9bacca-f3de-4e3e-b1d5-be2cdc03f76f_17
Time:8000---->3f9bacca-f3de-4e3e-b1d5-be2cdc03f76f_17
Time:8500---->3f9bacca-f3de-4e3e-b1d5-be2cdc03f76f_17
当前线程Thread-3手动解锁成功
Time:9000---->null
Time:9500---->null
Time:10000---->null
不过最好避免同时有两个线程同时访问同一个锁的情况,所以会在某个线程占用锁的时候创建一个守护线程,当锁快要超时的时候,守护线程给锁的过期时间续上。当线程结束的时候守护线程也会跟着结束,所以不用担心守护线程会遗留造成问题。
至于重入锁的实现可以在这基础上做修改,通过redis的hash数据结构来构造锁,hash表的field作为线程唯一id,value设置为重入次数即可。同一线程多次获取锁时将value加1,解锁时将value减1。当value为0时锁才会进行释放。感兴趣的同学可以自己试着实现该特性。
至此我们通过redis实现了简单的单机redis的分布式锁,其核心就是通过set的原子操作和lua语言运行redis的原子特性来实现的。但是当面对redis集群时我们也束手无策,当某个redis服务器宕机时,没法查询其他机器,也没法确保当前的锁是否已经被获取。
因此下面将介绍Redisson实现Redis分布式锁:RedLock。