之前总是被diss,然后终于写了个基于redis操作的锁。当然有别人已经写好了的产品,如 redisson,想着已经有了redistemplate了,就自己实现一个吧。
先说明代码是修改于 redis分布式锁过期时间到了,但业务没执行完怎么办?
同时也参照了ReetrantLock的,但是由于是自己初次写的工具类,没什么扩展性,和ReetrantLock还是没法比的。我也是一个小小的菜鸡,里面可能有很多性能上不合理的地方,也希望大家指出。
就直接放代码了,基础依赖基本上都是懂的都懂了,不懂的查看缺失的类查查是哪个包的吧。核心概念就是固定30秒的超时时间,每隔20秒所有都刷新,不管它是什么时候加入的。开销可能还是有点大的(不过没测试过),所以这个锁不能替代jdk的锁(个人认为)。或许更新的时候写进一个lua脚本能提升一点性能,特别是redis被阻塞的时候。
再次声明,由于本人的水平实在有限,不保证不出问题。若是存在同步上的严重问题或者是性能上的问题,请指出。
package com.scaleamer.concurrent.lock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Component;
import java.io.Serializable;
import java.util.*;
import java.util.concurrent.*;
/**
* 来自https://blog.csdn.net/weixin_40098891/article/details/102771805
* 改动,删除request id。不允许跨request加解锁行为
*/
@Component
public class RedisLock {
@Autowired
private StringRedisTemplate redisTemplate;
private static final String LOCK_PREFIX = "lock:";
private static final long DEFAULT_LOCK_TIMEOUT = 30;
private static final long TIME_SECONDS_FIVE = 5;
/**
* lock 的缓存
*/
private Map lockContentMap = new ConcurrentHashMap<>(512);
/**
* redis执行成功的返回
*/
private static final Long EXEC_SUCCESS = 1L;
private static final String LOCK_SCRIPT = "return redis.call('set', KEYS[1], ARGV[1], 'NX', 'EX', ARGV[2])";
private static final String UNLOCK_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) " +
"else return 0 end";
private static final String RENEW_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('expire', KEYS[1], ARGV[2]) else " +
"return 0 end";
/**
* 改为setter注入
*
* @param redisTemplate
*/
public void setRedisTemplate(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
public RedisLock() {
startSchedule(0, 20, TimeUnit.SECONDS);
}
/**
* 没有那么高的自由度,自由度会导致性能不稳定
*
* @param lockKey
*/
public void lock(String lockKey) {
//自适应自旋
int count = 0;
int maxTry = 32;
for (; ; ) {
// 判断是否已经有线程持有锁,减少redis的压力
LockContent lockContentOld = lockContentMap.get(lockKey);
boolean unLocked = (null == lockContentOld);
// 如果没有被锁,就获取锁
if (unLocked) {
RedisScript script = RedisScript.of(LOCK_SCRIPT, String.class);
//省点内存吧
List keys = new ArrayList<>(1);
keys.add(lockKey);
String id_random = UUID.randomUUID().toString();
String success = redisTemplate.execute(script, keys, id_random, Long.toString(DEFAULT_LOCK_TIMEOUT));
if (null != success && success.equals("OK")) {
// 将锁放入map
System.out.println(Thread.currentThread()+" GET SUCCESS");
LockContent lockContent = new LockContent();
lockContent.setId_random(id_random);
lockContent.setThread(Thread.currentThread());
lockContent.setLockCount(1);
lockContentMap.put(lockKey, lockContent);
return;
}//else就申请redis锁失败了
}
// 重复获取锁,在线程池中由于线程复用,线程相等并不能确定是该线程的锁
// 如果这么说的话jdk所有lock都不能用了 给id只是为了防止其他人删了
// 总的来说执行到这一步已经代表了已经有锁了,但是还没判断是谁的。同时如果是本线程的话,由于是可重入的,就判断一下是否是本线程。
// 另外由于是本线程行为,不可能出现那种判断的时候被unlock了。
if (lockContentOld != null && Thread.currentThread() == lockContentOld.getThread()) {
// 计数 +1
lockContentOld.setLockCount(lockContentOld.getLockCount() + 1);
return;
}
count++;
if (count >= maxTry) {
count = 0;
int next_maxTry = maxTry / 2;
maxTry = next_maxTry > 0 ? next_maxTry : 1;
// 如果被锁或获取锁失败,则等待100毫秒
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException("Fail to obtain a lock");
}
}
}
}
/**
* 先remove redis 还是先remove缓存。
* 假如先remove redis 本进程内查看到有锁的缓存是不会去上锁的,貌似没问题,但是白等了
* 假如先remove 缓存, 假如这个时刻进来一个线程去获取缓存,空了然后去redis操作,有可能失败,增多了一次开销。
* 考虑两步之内有其他程序插入的问题?也只有lock能插入了,就是上述的问题
*
* @param lockKey
*/
public void unlock(String lockKey) {
String lockKeyRenew = lockKey + "_renew";
LockContent lockContent = lockContentMap.get(lockKey);
if (lockContent == null || Thread.currentThread() != lockContent.getThread()) {
throw new IllegalMonitorStateException();
}
int c = lockContent.getLockCount() - 1;
if (c == 0) {
RedisScript script = RedisScript.of(UNLOCK_SCRIPT, Long.class);
List keys = new ArrayList<>(1);
keys.add(lockKey);
Long result = redisTemplate.execute(script, keys, lockContent.getId_random());
lockContentMap.remove(lockKey);
}
}
private boolean renew(String lockKey, LockContent lockContent) {
// 检测执行业务线程的状态
Thread.State state = lockContent.getThread().getState();
if (Thread.State.TERMINATED == state) {
return false;
}
String random_id = lockContent.getId_random();
RedisScript script = RedisScript.of(RENEW_SCRIPT, Long.class);
List keys = new ArrayList<>();
keys.add(lockKey);
Long result = redisTemplate.execute(script, keys, random_id, Long.toString(DEFAULT_LOCK_TIMEOUT));
return EXEC_SUCCESS.equals(result);
}
public void startSchedule(long initialDelay, long period, TimeUnit unit) {
ScheduleTask task = new ScheduleTask();
long delay = unit.toMillis(initialDelay);
long period_ = unit.toMillis(period);
// 定时执行
new Timer("Lock-Renew-Task").schedule(task, delay, period_);
}
private class ScheduleTask extends TimerTask {
@Override
public void run() {
if (lockContentMap.isEmpty()) {
return;
}
Set> entries = lockContentMap.entrySet();
for (Map.Entry entry : entries) {
String lockKey = entry.getKey();
LockContent lockContent = entry.getValue();
ThreadPool.submit(() -> {
boolean renew = renew(lockKey, lockContent);
if (!renew) {
// 续约失败,说明已经执行完 OR redis 出现问题
// 确实有可能是unlock之后 然后执行到这里
lockContentMap.remove(lockKey);
}
});
}
}
}
public static class LockContent implements Serializable {
/**
* 用于防止锁的误删,全局唯一
*/
private String id_random;
/**
* 执行业务的线程
*/
private volatile Thread thread;
/**
* 可重入的计数器
*/
private int lockCount = 0;
public String getId_random() {
return id_random;
}
public void setId_random(String id_random) {
this.id_random = id_random;
}
public Thread getThread() {
return thread;
}
public void setThread(Thread thread) {
this.thread = thread;
}
public int getLockCount() {
return lockCount;
}
public void setLockCount(int lockCount) {
this.lockCount = lockCount;
}
}
static class ThreadPool {
private static final int CORESIZE = Runtime.getRuntime().availableProcessors();
private static final int MAXSIZE = 200;
private static final int KEEPALIVETIME = 60;
private static final int CAPACITY = 2000;
private static ThreadPoolExecutor threadPool;
static {
init();
}
private ThreadPool() {
}
/**
* 初始化所有线程池。
*/
private static void init() {
threadPool = new ThreadPoolExecutor(CORESIZE, MAXSIZE, KEEPALIVETIME,
TimeUnit.SECONDS, new LinkedBlockingQueue<>(CAPACITY));
//log.info("初始化线程池成功。");
}
public static Future> submit(Runnable task) {
if (null == task) {
throw new IllegalArgumentException("任务不能为空");
}
return threadPool.submit(task);
}
}
}
对了,在测试的时候发现了个很有意思的问题。假如用spring MVC的controller进行测试的话,对于两个普通的一模一样的get请求,后面的会被前面的有一定程度上的阻塞。
一开始以为是spring MVC的锅,然后从dispatchServlet进去看了半天无果,然后用jstack查看,发现根本没有线程在被阻塞。然后怀疑是tomcat的锅,心想tomcat也太牛逼了。最后换了个浏览器测试,是chrome的锅。。如果有不对请指正。