本文参考借鉴了论坛大佬的一篇很详细的博文并在此基础上加以实现,在此谢谢此位博主!,博文连接:
https://www.cnblogs.com/linjiqin/p/8003838.html
前言
首先,为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:
- 互斥性。在任意时刻,只有一个客户端能持有锁。
- 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
- 具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
- 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了
实现
引入依赖:
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.7.2</version>
</dependency>
和分布式锁一,二一样的orderSevice
package com.th.order;
public interface OrderService {
void createOrder();
}
模拟共享资源的OrderCodeGenerator:
package com.th.order;
import java.text.SimpleDateFormat;
import java.util.Date;
public class OrderCodeGenerator {
private static int i = 0;
public String getOrderCode() {
Date now = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-");
return sdf.format(now) + ++i;
}
}
主角RedisLock:
下面是加锁的方法:下面包含代码解析
@Override
public void lock() {
if (!tryLock()) {
// 没有获得锁,阻塞自己
waitForLock();
// 再次尝试加锁
lock();
}
}
@Override
public boolean tryLock() {
String result = jedis.set(LOCK_KEY, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
if (LOCK_SUCCESS.equals(result))
return true;
return false;
}
private void waitForLock() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
可以看到,我们加锁就一行代码:jedis.set(String key, String value, String nxxx, String expx, int time)
,这个set()方法一共有五个形参:
-
第一个为key,我们使用key来当锁,因为key是唯一的。
-
第二个为value,我们传的是requestId,很多童鞋可能不明白,有key作为锁不就够了吗,为什么还要用到value?原因就是我们在上面讲到可靠性时,分布式锁要满足第四个条件解铃还须系铃人,通过给value赋值为requestId,我们就知道这把锁是哪个请求加的了,在解锁的时候就可以有依据。requestId可以使用
UUID.randomUUID().toString()
方法生成。 -
第三个为nxxx,这个参数我们填的是NX,意思是SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作;
-
第四个为expx,这个参数我们传的是PX,意思是我们要给这个key加一个过期的设置,具体时间由第五个参数决定。
-
第五个为time,与第四个参数相呼应,代表key的过期时间。
总的来说,执行上面的set()方法就只会导致两种结果:1. 当前没有锁(key不存在),那么就进行加锁操作,并对锁设置个有效期,同时value表示加锁的客户端。2. 已有锁存在,不做任何操作。
心细的童鞋就会发现了,我们的加锁代码满足我们可靠性里描述的三个条件。首先,set()加入了NX参数,可以保证如果已有key存在,则函数不会调用成功,也就是只有一个客户端能持有锁,满足互斥性。其次,由于我们对锁设置了过期时间,即使锁的持有者后续发生崩溃而没有解锁,锁也会因为到了过期时间而自动解锁(即key被删除),不会发生死锁。最后,因为我们将value赋值为requestId,代表加锁的客户端请求标识,那么在客户端在解锁的时候就可以进行校验是否是同一个客户端。由于我们只考虑Redis单机部署的场景,所以容错性我们暂不考虑。
解锁方法:
@Override
public void unlock() {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(LOCK_KEY), Collections.singletonList(requestId));
if (RELEASE_SUCCESS.equals(result)) {
// return true;
System.out.println("解锁成功!");
} else {
System.out.println("不是自己持有锁,或者锁已经过期,不用解锁!");
}
// return false;
}
RedisLock完整代码:
package com.th.order;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import redis.clients.jedis.Jedis;
public class RedisLock implements Lock {
private Jedis jedis;
private String requestId; // 锁的value 用于解锁的时候 比较是否自己获得锁
private int expireTime; // 设置自动过期时间
private static final String LOCK_KEY = "lock_key";
private static final String SET_IF_NOT_EXIST = "NX"; // 如果key不存在,才set
// 否则不做任何操作
private static final String SET_WITH_EXPIRE_TIME = "PX";// 设置过期
private static final String LOCK_SUCCESS = "OK";// redis 加锁成功后返回的值
private static final Long RELEASE_SUCCESS = 1L;// 解锁成功后返回的值
RedisLock(String ip, int port, String requestId, int expireTime) {
this.jedis = new Jedis(ip, port);
this.requestId = requestId;
this.expireTime = expireTime;
}
@Override
public void lock() {
if (!tryLock()) {
// 没有获得锁,阻塞自己
waitForLock();
// 再次尝试加锁
lock();
}
}
@Override
public boolean tryLock() {
String result = jedis.set(LOCK_KEY, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
if (LOCK_SUCCESS.equals(result))
return true;
return false;
}
private void waitForLock() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void unlock() {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(LOCK_KEY), Collections.singletonList(requestId));
if (RELEASE_SUCCESS.equals(result)) {
// return true;
System.out.println("解锁成功!");
} else {
System.out.println("不是自己持有锁,或者锁已经过期,不用解锁!");
}
// return false;
}
@Override
public void lockInterruptibly() throws InterruptedException {
// TODO Auto-generated method stub
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
// TODO Auto-generated method stub
return false;
}
@Override
public Condition newCondition() {
return null;
}
}
OrderServiceImplWithZkDis测试类:
package com.th.order;
import java.util.UUID;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.locks.Lock;
public class OrderServiceImplWithZkDis implements OrderService {
private static OrderCodeGenerator org = new OrderCodeGenerator();
// private Lock lock = new ZookeeperDisLock("/LOCK_TEST");
// private Lock lock = new ZookeeperReAbleDisLock("/LOCK_TEST");
// private Lock lock = new DbLock(new User("1","张三丰"));
private Lock lock = new RedisLock("192.168.1.118", 6379, UUID.randomUUID().toString(), 1000);
@Override
public void createOrder() {
String orderCode = null;
try {
lock.lock();
orderCode = org.getOrderCode();
// TestReLock();
System.out.println(Thread.currentThread().getName() + "生成订单:" + orderCode);
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void TestReLock() {
lock.lock();
System.out.println(Thread.currentThread().getName() + "测试重入锁成功...");
lock.unlock();
}
public static void main(String[] args) {
int num = 20;
CyclicBarrier cyclicBarrier = new CyclicBarrier(num);
for (int i = 0; i < num; i++) {
new Thread(new Runnable() {
@Override
public void run() {
OrderService orderService = new OrderServiceImplWithZkDis();
System.out.println(Thread.currentThread().getName() + ": 我准备好了");
try {
cyclicBarrier.await();
} catch (Exception e) {
e.printStackTrace();
}
orderService.createOrder();
}
}).start();
}
}
}