需求
- 互斥性:只有一个客户端能持有锁
- 不死锁:保证后续客户端加锁
- 容错性:大部分redis节点正常运行,客户端就可以加锁解锁
- 阻塞性:其他客户端尝试请求锁的时间内阻塞状态
- 可重入性:在锁里面加锁
- 不能自己失效:在业务执行过程中,锁不能自己失效(异步续命)
- 解铃还须系铃人:持有锁的线程才能解自己上的锁
Java手写实现
基于spring整合的redisTemplate对redis进行操作
使用异步任务线程池ScheduledThreadPoolExecutor进行异步续命
@Slf4jpublic class RedisLock {private RedisTemplate<String, String> redisTemplate;/*** 避免自己的锁被别的线程释放了*/private final ThreadLocal<String> local = new ThreadLocal<>();/*** 用于实现可重入锁*/private final ThreadLocal<Integer> reentrant = new ThreadLocal<>();/*** 分布式锁key前缀*/private static final String PREFIX = "LOCK_";private String lockKey;/*** 用于异步续命的对象*/private RenewLockTimes renewLockTimes;/*** 创建锁对象** @param key 要锁的key* @param redisTemplate* @author YangYudi* @date 2021/10/29 9:49*/public RedisLock(String key, RedisTemplate<String, String> redisTemplate) {this.lockKey = PREFIX + key;this.redisTemplate = redisTemplate;}/*** 加锁** @param tryTime 尝试时间 单位秒* @param timeout 锁时间 单位秒* @return java.lang.Boolean* @author YangYudi* @date 2021/10/29 9:49*/public Boolean tryLock(long tryTime, long timeout) {long startTime = System.currentTimeMillis();//确保本线程的值唯一String uuid = UUID.randomUUID().toString();Boolean lock = false;if (StringUtils.isEmpty(local.get())) {//每个线程的value都不一样//使用threadLocal保证线程安全local.set(uuid);lock = redisTemplate.opsForValue().setIfAbsent(lockKey, uuid, timeout, TimeUnit.SECONDS);//没拿到锁就阻塞 自旋锁 直到拿到锁就不阻塞while (lock != null && !lock) {lock = redisTemplate.opsForValue().setIfAbsent(lockKey, uuid, timeout, TimeUnit.SECONDS);//超时就直接返回falseif (System.currentTimeMillis() - startTime >= (tryTime * 1000)) {log.error("{} 放弃拿到锁", lockKey);break;}}if (lock != null && lock) {//得到锁了 开启异步续命renewLockTimes = new RenewLockTimes(lockKey, timeout, redisTemplate);renewLockTimes.schedule();}} else {//可重入性lock = true;}//可重入锁//加锁成功后计数器加一if (lock != null && lock) {Integer count = reentrant.get() == null ? 0 : reentrant.get();reentrant.set(++count);log.info("加锁成功,{}", lockKey);}return lock;}/*** 加锁** @param tryTime* @return java.lang.Boolean* @author YangYudi* @date 2021/10/29 9:49*/public Boolean tryLock(long tryTime) {return tryLock(tryTime, 10);}/*** 加锁** @return java.lang.Boolean* @author YangYudi* @date 2021/10/29 9:49*/public Boolean tryLock() {return tryLock(5, 10);}/*** 释放锁** @return java.lang.Boolean* @author YangYudi* @date 2021/10/29 9:49*/public void releaseLock() {//get和delete是两个方法 没有原子性 如果不用lua脚本 就需要用到监听和事务来完成//保证本线程的锁只能被本线程释放//先判断锁是不是本线程的 如果是就删除key返回1 否则返回 0String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +"then\n" +" return redis.call(\"del\",KEYS[1])\n" +"else\n" +" return 0\n" +"end";Integer count = reentrant.get();if (count == null || count - 1 <= 0) {RedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);//执行lua脚本 只有当前线程才能释放锁Long execute = redisTemplate.execute(redisScript, Collections.singletonList(lockKey), local.get());if (!ObjectUtils.isEmpty(execute) && execute == 1) {//锁释放log.info("{} 锁释放", lockKey);//结束续命renewLockTimes.shutdown();local.remove();reentrant.remove();}} else {//可重入锁 加几次就要解几次锁reentrant.set(--count);}}private static class RenewLockTimes {private String key;private long timeout;/*** 续命的时间*/private long plusTime;private RedisTemplate<String, String> redisTemplate;private ScheduledExecutorService service;public RenewLockTimes(String key, long timeout, RedisTemplate<String, String> redisTemplate) {this.key = key;this.timeout = timeout;this.redisTemplate = redisTemplate;plusTime = timeout / 3;service = new ScheduledThreadPoolExecutor(1);}/*** 定时续命** @author YangYudi* @date 2020/12/24 16:29*/public void schedule() {service.scheduleAtFixedRate(() -> {//先获取key的时间 再判断时间是否快结束了 快结束了就续命//这段代码用lua脚本做好一点Long expire = redisTemplate.getExpire(key);if (!ObjectUtils.isEmpty(expire) && expire > 0 && expire <= plusTime) {redisTemplate.expire(key, expire + plusTime, TimeUnit.SECONDS);log.info("{} 续命成功", key);}},1, timeout / 3, TimeUnit.SECONDS);}/*** 停止续命** @author YangYudi* @date 2020/12/24 16:29*/public void shutdown() {service.shutdown();}}}
调用
测试是否支持异步续命,可以在释放锁之前让线程睡眠
public String buy() {int buy = 0;//传入要锁住的key和redisTemplateRedisLock redisLock = new RedisLock("1", redisTemplate);try {//加锁redisLock.tryLock();buy = orderService.buy();try {Thread.sleep(30000);} catch (InterruptedException e) {e.printStackTrace();}} finally {//解锁redisLock.releaseLock();}return String.valueOf(buy);}
