极致性能的redis分布式锁工具

极致性能的redis分布式锁工具

设计思路

基于Redis实现,采用了setex命令来获取锁,并支持自旋等待和重入锁机制。同时,针对查询缓存场景,提供了cacheLock方法,利用先检查缓存再加锁的方式避免了缓存击穿问题,提高并发性能

缓存锁场景举例:

缓存击穿,当某一个热点缓存过期,突然大并发流量进来取数,在获取缓存的时候都没取到值,全部走数据库,
导致数据库崩溃,这种情况一般解决方案就是对整段代码加锁(先判断缓存,在查库,在添加至缓存),
但是同时也带来一个问题:加锁后,后面的线程都会依次排队加锁(即使已经有缓存,不需要加锁了!)
去取缓存,没取到的会自旋占用不必要cpu开销 **缓存锁就是对这个情况的统一优化** 

优势

  1. 采用ThreadLocal记录重入次数,避免不必要加锁解锁减少对Redis的访问,降低IO开销
  2. 缓存锁多线程取的时候,如果第一个线程已经获取了缓存,后续线程不需要自旋排队了,无需加锁解锁直接取缓存,减少不必要io,和cpu压力
  3. 简洁的代码调用,避免冗余沉重的加锁代码和屏蔽不必要的细节
  4. 能自己权衡性能权重自定义设置线程自旋等待时间 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;
    }
}

  • 7
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值