在分布锁的实际使用中,可能会遇到一种情况,一个业务执行时间很长,已经超过redis加锁的时间,也就是锁已经释放了,但是业务还没执行完成,这时候其它线程还是可以获取锁,那就没保证线程安全
项目环境:
JDK 1.8
SpringBoot 2.2.1
Maven 3.2+
Mysql 8.0.26
spring-boot-starter-data-redis 2.2.1
jedis3.1.0
开发工具
IntelliJ IDEA
smartGit
先搭建一个springboot集成jedis的例子工程,参考我之前的博客,
抽象类,实现一些共用的逻辑
package com.example.jedis.common;
import lombok.extern.slf4j.Slf4j;
import java.net.SocketTimeoutException;
import java.util.concurrent.TimeUnit;
import static com.example.jedis.common.RedisConstant.DEFAULT_EXPIRE;
import static com.example.jedis.common.RedisConstant.DEFAULT_TIMEOUT;
@Slf4j
public abstract class AbstractDistributedLock implements DistributedLock {
@Override
public boolean acquire(String lockKey, String requestId, int expireTime, int timeout) {
expireTime = expireTime <= 0 ? DEFAULT_EXPIRE : expireTime;
timeout = timeout < 0 ? DEFAULT_TIMEOUT : timeout * 1000;
long start = System.currentTimeMillis();
try {
do {
if (doAcquire(lockKey, requestId, expireTime)) {
watchDog(lockKey, requestId, expireTime);
return true;
}
TimeUnit.MILLISECONDS.sleep(100);
} while (System.currentTimeMillis() - start < timeout);
} catch (Exception e) {
Throwable cause = e.getCause();
if (cause instanceof SocketTimeoutException) {
// ignore exception
log.error("sockTimeout exception:{}", e);
}
else if (cause instanceof InterruptedException) {
// ignore exception
log.error("Interrupted exception:{}", e);
}
else {
log.error("lock acquire exception:{}", e);
}
throw new LockException(e.getMessage(), e);
}
return false;
}
@Override
public boolean release(String lockKey, String requestId) {
try {
return doRelease(lockKey, requestId);
} catch (Exception e) {
log.error("lock release exception:{}", e);
throw new LockException(e.getMessage(), e);
}
}
protected abstract boolean doAcquire(String lockKey, String requestId, int expireTime);
protected abstract boolean doRelease(String lockKey, String requestId);
protected abstract void watchDog(String lockKey, String requestId, int expireTime);
}
具体的实现,主要是基于一个定时任务,时间间隔一定要比加锁时间少一点,这里暂时少1s,加上一个lua脚本进行检测,检测不到数据,就关了定时任务
package com.example.jedis.common;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
@Component
@Slf4j
public class JedisLockTemplate extends AbstractRedisLock implements InitializingBean {
private String UNLOCK_LUA = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
private String WATCH_DOG_LUA = "local lock_key=KEYS[1]\n" +
"local lock_value=ARGV[1]\n" +
"local lock_ttl=ARGV[2]\n" +
"local current_value=redis.call('get',lock_key)\n" +
"local result=0\n" +
"if lock_value==current_value then\n" +
" redis.call('expire',lock_key,lock_ttl)\n" +
" result=1\n" +
"end\n" +
"return result";
private static final Long UNLOCK_SUCCESS = 1L;
private static final Long RENEWAL_SUCCESS = 1L;
@Autowired
private JedisTemplate jedisTemplate;
private ScheduledThreadPoolExecutor scheduledExecutorService;
@Override
public void afterPropertiesSet() throws Exception {
this.UNLOCK_LUA = jedisTemplate.scriptLoad(UNLOCK_LUA);
this.WATCH_DOG_LUA = jedisTemplate.scriptLoad(WATCH_DOG_LUA);
scheduledExecutorService = new ScheduledThreadPoolExecutor(1);
}
@Override
public boolean doAcquire(String lockKey, String requestId, int expire) {
return jedisTemplate.setnxex(lockKey, requestId, expire);
}
@Override
public boolean doRelease(String lockKey, String requestId) {
Object eval = jedisTemplate.evalsha(UNLOCK_LUA, CollUtil.newArrayList(lockKey), CollUtil.newArrayList(requestId));
if (UNLOCK_SUCCESS.equals(eval)) {
scheduledExecutorService.shutdown();
return true;
}
return false;
}
@Override
public void watchDog(String lockKey, String requestId, int expire) {
int period = getPeriod(expire);
if (scheduledExecutorService.isShutdown()) {
scheduledExecutorService = new ScheduledThreadPoolExecutor(1);
}
scheduledExecutorService.scheduleAtFixedRate(
new WatchDogTask(scheduledExecutorService, CollUtil.newArrayList(lockKey), CollUtil.newArrayList(requestId, Convert.toStr(expire))),
1,
period,
TimeUnit.SECONDS
);
}
class WatchDogTask implements Runnable {
private ScheduledThreadPoolExecutor scheduledThreadPoolExecutor;
private List<String> keys;
private List<String> args;
public WatchDogTask(ScheduledThreadPoolExecutor scheduledThreadPoolExecutor, List<String> keys, List<String> args) {
this.scheduledThreadPoolExecutor = scheduledThreadPoolExecutor;
this.keys = keys;
this.args = args;
}
@Override
public void run() {
log.info("watch dog for renewal...");
Object evalsha = jedisTemplate.evalsha(WATCH_DOG_LUA, keys, args);
if (!evalsha.equals(RENEWAL_SUCCESS)) {
scheduledThreadPoolExecutor.shutdown();
}
log.info("renewal result:{}, keys:{}, args:{}", evalsha, keys, args);
}
}
private int getPeriod(int expire) {
if (expire < 1)
throw new LockException("expire不允许小于1");
return expire - 1;
}
}
写一个测试Controller类,开始用SpringBoot测试类的,但是发现有时候还是经常出现一些连接超时情况,这个可能是框架兼容的bug
package com.example.jedis.controller;
import com.example.jedis.common.JedisLockTemplate;
import com.example.jedis.common.Lock;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.stream.IntStream;
@RestController
@Slf4j
public class TestController {
private static final String REDIS_KEY = "test:lock";
@Autowired
private JedisLockTemplate jedisLockTemplate;
@GetMapping("test")
public void test(@RequestParam("threadNum")Integer threadNum) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(threadNum);
IntStream.range(0, threadNum).forEach(e->{
new Thread(new RunnableTask(countDownLatch)).start();
});
countDownLatch.await();
}
@GetMapping("testLock")
@Lock(lockKey = "test:api", requestId = "123", expire = 5, timeout = 3)
public void testLock() throws InterruptedException {
doSomeThing();
}
class RunnableTask implements Runnable {
CountDownLatch countDownLatch;
public RunnableTask(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
redisLock();
countDownLatch.countDown();
}
}
private void redisLock() {
String requestId = getRequestId();
Boolean lock = jedisLockTemplate.acquire(REDIS_KEY, requestId, 5, 3);
if (lock) {
try {
doSomeThing();
} catch (Exception e) {
jedisLockTemplate.release(REDIS_KEY, requestId);
} finally {
jedisLockTemplate.release(REDIS_KEY, requestId);
}
} else {
log.warn("获取锁失败!");
}
}
private void doSomeThing() throws InterruptedException {
log.info("do some thing");
Thread.sleep(15 * 1000);
}
private String getRequestId() {
String str="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
Random random=new Random();
StringBuffer sb=new StringBuffer();
for(int i=0;i<32;i++){
int number=random.nextInt(62);
sb.append(str.charAt(number));
}
return sb.toString();
}
}
# 模拟100个并发请求
curl http://127.0.0.1:8080/springboot-jedis/test?threadNum=100
长事务还没执行完成,会自动进行续期
模拟100个线程的场景,只有一个线程会获取到锁
参考资料: