极致性能的redis分布式锁工具
设计思路
基于Redis实现,采用了setex命令来获取锁,并支持自旋等待和重入锁机制。同时,针对查询缓存场景,提供了cacheLock方法,利用先检查缓存再加锁的方式避免了缓存击穿问题,提高并发性能
缓存锁场景举例:
缓存击穿,当某一个热点缓存过期,突然大并发流量进来取数,在获取缓存的时候都没取到值,全部走数据库,
导致数据库崩溃,这种情况一般解决方案就是对整段代码加锁(先判断缓存,在查库,在添加至缓存),
但是同时也带来一个问题:加锁后,后面的线程都会依次排队加锁(即使已经有缓存,不需要加锁了!)
去取缓存,没取到的会自旋占用不必要cpu开销 **缓存锁就是对这个情况的统一优化**
优势
- 采用ThreadLocal记录重入次数,避免不必要加锁解锁减少对Redis的访问,降低IO开销
- 缓存锁多线程取的时候,如果第一个线程已经获取了缓存,后续线程不需要自旋排队了,无需加锁解锁直接取缓存,减少不必要io,和cpu压力
- 简洁的代码调用,避免冗余沉重的加锁代码和屏蔽不必要的细节
- 能自己权衡性能权重自定义设置线程自旋等待时间 spinWaitTime,默认=1s
使用例子
对getUser方法加缓存锁
@Autowired
private RedisCacheLock redisCacheLock;
String result = redisCacheLock.cacheLock(key, 60L(缓存过期时间), () -> getUser(userId));
工具类代码如下.
注意:redisUtils替换成你们公司的redis工具类即可
/**
* 分布式锁工具
* 分两类api:
* 1.lock: 普通的加锁方法,分有返回值的和无返回值的
* 2.cacheLock: 对一些查询缓存的方法加锁的场景,使用这个能有效避免缓存击穿同时,区别相比其他的加锁方法,只能一个个排队取一个个放(串行返回),
* cacheLock只要缓存有值就不会走加锁排队了,线程会直接从缓存读值,不走加锁方法(并行返回),因此在高并发场景,该锁几乎不占用性能,
* 将并发查询压力下放到redis。
* 以上类api都可以通过调用重载方法,去权衡性能权重减少自旋等待时间 spinWaitTime,默认=1s
*
* @Author: lsp
* @Date: 2022/11/23 13:51
*/
@Slf4j
@Component
public class RedisCacheLock {
@Autowired
private RedisUtils redisUtils;
//获取方法返回值的锁类型
private static final Integer getTheLock = 1;
//获取缓存值锁的类型
private static final Integer getTheCache = 2;
//重入锁操作记录
private static final ThreadLocal<Map<String, Integer>> lockCount = ThreadLocal.withInitial(HashMap::new);
/**
* 对有返回值的方法加锁
*
* @param lockKey
* @param method
* @param <T>
* @return
*/
public <T> T lock(String lockKey, Supplier<T> method) {
return lock(lockKey, 1000L, method);
}
/**
* 对有返回值的方法加锁 可以设置等待锁的自旋时间
*
* @param lockKey
* @param spinWaitTime 自旋时间 单位毫秒
* @param method
* @param <T>
* @return
*/
public <T> T lock(String lockKey, Long spinWaitTime, Supplier<T> method) {
if (spinWaitTime == null || spinWaitTime > 1000) {
throw new IllegalArgumentException("spinWaitTime is invalid!");
}
return doCacheLock(lockKey, "", spinWaitTime, null, method);
}
/**
* 带缓存机制的锁,自旋等待锁的线程如果判断缓存有值了,就不会排队了,直接从缓存取值返回
*
* @param lockKey
* @param valueKey
* @param method
* @param <T>
* @return
* @Param cacheExpire 缓存过期时间 单位秒
*/
public <T> T cacheLock(String lockKey, String valueKey, Long cacheExpire, Supplier<T> method) {
return cacheLock(lockKey, valueKey, 1000L, cacheExpire, method);
}
/**
* 带缓存机制的锁,自旋等待锁的线程如果判断缓存有值了,就不会排队了,直接从缓存取值返回
* tip:缓存key不传默认等于锁的key(会由不同前缀做区分),如果需要自定义缓存key,请使用重载方法cacheLock(String lockKey, String valueKey, Supplier<T> method)
*
* @param lockKey
* @param method
* @param <T>
* @return
*/
public <T> T cacheLock(String lockKey, Long cacheExpire, Supplier<T> method) {
return cacheLock(lockKey, lockKey, 1000L, cacheExpire, method);
}
/**
* 带缓存机制的锁,自旋等待锁的线程如果判断缓存有值了,就不会排队了,直接从缓存取值返回,可以设置等待锁的线程的自旋休眠时间-spinWaitTime
*
* @param lockKey
* @param valueKey
* @param spinWaitTime 单位毫秒
* @param method
* @param <T>
* @return
* @Param cacheExpire 缓存过期时间 单位秒
*/
public <T> T cacheLock(String lockKey, String valueKey, Long spinWaitTime, Long cacheExpire, Supplier<T> method) {
if (StringUtils.isBlank(valueKey)) {
throw new IllegalArgumentException("valueKey is null!");
}
if (spinWaitTime == null || spinWaitTime > 1000) {
throw new IllegalArgumentException("spinWaitTime is invalid!");
}
return doCacheLock(lockKey, valueKey, spinWaitTime, cacheExpire, method);
}
/**
* 对没有返回值的方法加锁
*
* @param lockKey
* @param method
*/
public void lock(String lockKey, Runnable method) {
lock(lockKey, 1000L, method);
}
/**
* 对没有返回值的方法加锁,可以设置锁等待的自旋时间
*
* @param lockKey
* @param spinWaitTime 自旋时间 单位毫秒
* @param method
*/
public void lock(String lockKey, Long spinWaitTime, Runnable method) {
if (spinWaitTime == null || spinWaitTime > 1000) {
throw new IllegalArgumentException("spinWaitTime is invalid!");
}
doLock(lockKey, spinWaitTime, method);
}
/**
* 对有缓存场景的加锁优化版,从redis获取锁前先判断缓存是否有值,如果缓存有值,则无需加锁直接返回缓存的值,类似double check
*
* @param lockKey 加锁key
* @param valueKey 缓存key
* @param spinWaitTime 自旋等待时间 单位毫秒
* @param method 加锁方法
*/
@SneakyThrows
@SuppressWarnings("unchecked")
private <T> T doCacheLock(String lockKey, String valueKey, Long spinWaitTime, Long cacheExpire, Supplier<T> method) {
if (StringUtils.isBlank(lockKey)) {
throw new IllegalArgumentException("lockKey is null!");
}
String value = UUID.randomUUID().toString() + System.currentTimeMillis();
Integer status = null;
try {
lockKey = RedisConstants.LockKey.LOCK_PREFIX + lockKey;
valueKey = RedisConstants.LockKey.LOCK_VALUE_PREFIX + valueKey;
LockResult lockResult = tryLock(lockKey, valueKey, value, spinWaitTime);
status = lockResult.getStatus();
if (getTheCache.equals(status)) {
//这里需要注意的是因为该值是从redis缓存取的,是Object类型,需要配合redis对javaBean的序列化和反序列化,才能直接强转
return (T) lockResult.getRes();
}
if (getTheLock.equals(status)) {
T methodRes = method.get();
if (cacheExpire != null) {
redisUtils.setBean(valueKey, methodRes, cacheExpire);
}
return methodRes;
}
} catch (BusinessException e) {
throw e;
} catch (Exception e) {
log.error("分布式锁运行错误!", e);
throw e;
} finally {
//只有是真正走了加锁流程的才需要解锁,从缓存取的无须解锁
if (getTheLock.equals(status)) {
unlock(lockKey, value);
}
}
return method.get();
}
/**
* 对无返回值的方法进行加锁
*
* @param lockKey 加锁key
* @param spinWaitTime 自旋等待时间 单位毫秒
* @param method 加锁方法
*/
@SneakyThrows
private void doLock(String lockKey, Long spinWaitTime, Runnable method) {
if (StringUtils.isAnyBlank(lockKey)) {
throw new IllegalArgumentException("lockKey is null!");
}
String value = UUID.randomUUID().toString() + System.currentTimeMillis();
try {
lockKey = RedisConstants.LockKey.LOCK_PREFIX + lockKey;
LockResult lockResult = tryLock(lockKey, "", value, spinWaitTime);
Integer status = lockResult.getStatus();
if (getTheLock.equals(status)) {
method.run();
}
} catch (BusinessException e) {
throw e;
} catch (Exception e) {
log.error("分布式锁运行错误!", e);
throw e;
} finally {
unlock(lockKey, value);
}
}
//释放锁
private void unlock(String lockKey, String value) {
Map<String, Integer> map = lockCount.get();
//释放也是同理,如果重入过只是count-1,而不是无意义的去redis做多次释放
if (map.getOrDefault(lockKey, 0) <= 1) {
lockCount.remove();
redisUtils.delLock(lockKey, value);
log.info("进入解锁流程! TheadId:{}", Thread.currentThread().getId());
} else {
map.put(lockKey, map.get(lockKey) - 1);
}
}
//尝试自旋获取锁
private LockResult tryLock(String lockKey, String valueKey, String value, Long spinWaitTime) throws InterruptedException {
Object obj;
if (!RedisConstants.LockKey.LOCK_VALUE_PREFIX.equals(valueKey) && (obj = redisUtils.getBean(valueKey)) != null) {
log.info("互斥条件通过,直接返回缓存结果,无需加锁!");
return getCache(obj);
}
boolean ret = lock(lockKey, value);
if (!ret) {
Thread.sleep(spinWaitTime);
return tryLock(lockKey, valueKey, value, spinWaitTime);
}
log.info("获取到锁!");
return getLock();
}
//真正的对redis加锁,如果重入,会记录在map中,不会访问redis,减少不必要io
private boolean lock(String key, String value) {
Map<String, Integer> map = lockCount.get();
//先判断锁是否已经重入,重入表示已加过锁,未重入过才用redis去setnx
if (map.get(key) != null || redisUtils.setEx(key, value, 30, TimeUnit.MINUTES)) {
map.compute(key, (k, v) -> {
if (null == v) {
v = 1;
} else {
v += 1;
}
return v;
});
return true;
}
return false;
}
@Data
private static class LockResult {
private Integer status;
private Object res;
}
private static LockResult getLock() {
LockResult lockResult = new LockResult();
lockResult.setStatus(getTheLock);
return lockResult;
}
private static LockResult getCache(Object res) {
LockResult lockResult = new LockResult();
lockResult.setStatus(getTheCache);
lockResult.setRes(res);
return lockResult;
}
}