redis实现分布式锁释放锁和分布式锁实现可重入性
本文为上一篇redis使用setnx实现分布式锁的增加篇 重在体会思想 与开源的框架自然是无法比拟的
如果当前线程已经获取到锁的情况下,不需要重复获取锁,而是直接复用。
秒杀A里面同时调用秒杀B 需要实现锁的复用 不然会报该锁 获取失败 执行错误
package com.shanhe.lock; public interface RedisLock { /** * 获取锁 * * @return */ boolean tryLock(); /** * 释放锁 * * @return */ boolean releaseLock(); }
访问的接口地址
import com.shanhe.entity.CommodityDetails; import com.shanhe.lock.impl.RedisLockImpl; import com.shanhe.task.mapper.CommodityDetailsMapper; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.concurrent.TimeUnit; @Slf4j @RestController public class SeckillRedisService { @Autowired private CommodityDetailsMapper commodityDetailsMapper; @Autowired private StringRedisTemplate stringRedisTemplate; private String redisLockKey = "lock"; @Autowired private RedisLockImpl redisLockImpl; /** * redis实现分布式锁 * */ @RequestMapping("/seckillRedisLock") public String seckillRedisLock(Long commodityId) throws Exception { try { return seckillLockA(commodityId); } catch (Exception e) { log.error("<e:>", e); return "fail"; } finally { // 释放锁 redisLockImpl.releaseLock(); } } private String seckilLockA(Long commodityId) { // 获取锁 boolean getLock = redisLockImpl.tryLock(); if (!getLock) { return "获取锁失败,请稍后重试!"; } CommodityDetails commodityDetails = commodityDetailsMapper.getCommodityDetails(commodityId); return seckillLockB(commodityDetails); } private String seckillLockB(CommodityDetails commodityDetails) { boolean getLock = redisLockImpl.tryLock(); if (!getLock) { return "获取锁失败,请稍后重试!"; } Long stock = commodityDetails.getStock(); if (stock > 0) { log.info("<开始执行扣库存..>"); int result = commodityDetailsMapper.reduceInventory(1l); return result > 0 ? "扣库存成功" : "扣库存失败"; } // 扣库存失败 log.info("<扣库存失败>"); return "fail"; } }
分布式锁具体实现
package com.shanhe.lock.impl; import com.shanhe.entity.RedisLockInfo; import com.shanhe.lock.RedisLock; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @Component @Slf4j public class RedisLockImpl implements RedisLock { @Autowired private StringRedisTemplate stringRedisTemplate; private String redisLockKey = "lock"; /** * 缓存redis锁 */ private static Map<Thread, RedisLockInfo> lockCacheMap = new ConcurrentHashMap<>(); /** * 重试时间 */ private Long timeout = 3000L; private ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(); @Override public boolean tryLock() { // 1.多个jvm执行setnx 命令 最终只有一个jvm能够成功 // setnx Thread cuThread = Thread.currentThread(); RedisLockInfo redisLockInfo = lockCacheMap.get(cuThread); if (redisLockInfo != null && redisLockInfo.isState()) { // 这把锁可重入次数—+1 log.info("<您在之前已经获取过锁,锁直接可重入>"); return true; } // 重试机制 重试次数 或者 重试10s Long startTime = System.currentTimeMillis(); Long lockExpire = 30000l; for (; ; ) { Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent(redisLockKey, "1", lockExpire, TimeUnit.SECONDS); if (lock) { log.info("<获取锁成功>"); lockCacheMap.put(cuThread, new RedisLockInfo(cuThread, lockExpire)); // 开始续命监听 return true; } // 控制一个超时的时间 Long endTime = System.currentTimeMillis(); if (endTime - startTime > timeout) { log.info("<重试的时间已经过了,不能够在继续重试啦>"); return false; } // 继续循环 try { Thread.sleep(10); } catch (Exception e) { } } } @Override public boolean releaseLock() { // A线程创建的锁 B线程执行释放锁 是谁创建的锁,就应该给谁删除。 lua脚本实现删除 return stringRedisTemplate.delete(redisLockKey); } public RedisLockImpl() { //开始定时任务实现续命 this.scheduledExecutorService.scheduleAtFixedRate(new LifeExtensionThread(), 0, 5, TimeUnit.SECONDS); } /** * 当我们的获取锁的jvm业务执行时间>过期key的超时时间 应该实现续命: * 当key过期的时候:走事件回调到客户端。---时间在延长 * 延长过期key 应该是提前的。通过定时任务提前延长过期key * 算法实现: * 开启一个定时任务,每隔一段时间检测获取到锁的线程,延长该过期key的时间 */ class LifeExtensionThread implements Runnable { @Override public void run() { lockCacheMap.forEach((k, lockInfo) -> { try { // 如何判断当前线程还没有执行完毕? Thread lockServiceThread = lockInfo.getLockThread(); if (lockServiceThread.isInterrupted()) { log.info(">>当前线程已经被停止,不需要实现续命<<"); lockCacheMap.remove(k); return; } // 需要控制续命多次,如果获取到锁的jvm 续命多次还是没有将业务逻辑执行完毕的情况下处理: 主动释放锁 事务会回滚 Integer lifeCount = lockInfo.getLifeCount(); if (lifeCount > 3) { log.info(">>您已经续命了多次当前线程还没有释放锁,现在主动将该锁释放 避免死锁的问题"); // 1.事务回滚 // 2.释放锁 dele releaseLock(); // 3.将该线程主动停止 lockServiceThread.interrupt(); // 4.移除监听 lockCacheMap.remove(k); return; } //提前实现续命 延长过期key的时间 stringRedisTemplate.expire(redisLockKey, lockInfo.getExpire(), TimeUnit.SECONDS); } catch (Exception e) { e.printStackTrace(); } }); } } }
早年同窗始相知,三载瞬逝情却萌。年少不知愁滋味,犹读红豆生南国。别离方知相思苦,心田红豆根以生。