Redis锁与幂等性不得不说的故事

359 阅读3分钟

前言:

相信很多小伙伴对缓存锁都不陌生,但是简单的缓存锁想要用好还是需要一些功力。本文总结了笔者多年使用缓存所的一些心得,欢迎交流探讨~

幂等模型:

幂等场景一般由查重+写入两步操作组成,两步操作组成一个最小完整逻辑,再通过缓存锁保证原子性

实现:

实现redis分布式锁需要注意两个关键点:保证原子性、设置过期时间

保证原子性的目的:是为了保证同一时间只有一个请求能获取到锁

设置过期时间的目的:是为了防止解锁失败导致死锁

public class MethodLock {

    private static final Logger logger = LoggerFactory.getLogger(MethodLock.class);

    public static final String KEY_SEPARATOR = ":";
    public static final String PARAM_SEPARATOR = "_";
    public static final String KEY_PARAM_SEPARATOR = "#";
    public static final String SET_SUCCESS_RESULT = "OK";
    public static final String METHOD_LOCK = "doraemon:method:lock";


    private static Jedis jedis;

    private static Jedis getJedis() {
        if (jedis == null) {
            jedis = 自行实现;
        }
        return jedis;
    }


    /**
     * 获取锁
     *
     * @param methodName 方法名,类名加方法名(例:MethodLock.getLock)
     * @param timeout    锁过期时间(单位秒)
     * @param params     参数(通过参数控制锁的粒度)
     * @return
     */
    public static boolean getLock(String methodName, long timeout, String... params) {
        String method = "getLock|获取锁|";

        try {
            String key = getKey(methodName, params);
            String result = getJedis().set(key, methodName, "nx", "ex", timeout <= 0 ? 5 : timeout);
            if (Objects.equals(SET_SUCCESS_RESULT, result)) {
                return true;
            }
        } catch (Exception e) {
            logger.error(method + "执行失败,methodName={},timeout={},params={}", methodName, timeout, JSONObject.toJSONString(params), e);
        }

        return false;
    }

    /**
     * 释放锁
     *
     * @param methodName 方法名,类名加方法名(例:MethodLock.getLock)
     * @param params     参数(通过参数控制锁的粒度)
     * @return
     */
    public static boolean delLock(String methodName, String... params) {
        String method = "delLock|释放锁|";

        try {
            String key = getKey(methodName, params);
            long result = getJedis().del(key);
            if (result > 0) {
                return true;
            }
        } catch (Exception e) {
            logger.error(method + "执行失败,methodName={},params={}", methodName, JSONObject.toJSONString(params), e);
        }

        return false;
    }

    /**
     * 【私有方法】获取redis key
     *
     * @param methodName
     * @param params
     * @return
     */
    protected static String getKey(String methodName, String... params) {
        if (StringUtils.isBlank(methodName)) {
            return null;
        }
        String key = METHOD_LOCK + KEY_SEPARATOR + methodName;
        if (params != null && params.length > 0) {
            key += KEY_PARAM_SEPARATOR + Joiner.on(PARAM_SEPARATOR).useForNull("null").join(params);
        }
        return key;
    }

注意事项:

1、锁范围内的逻辑需要 完整+精简

锁范围内指的是:获取锁和释放锁中间的逻辑,只有必须保证原子性的关键的逻辑才能放入到锁范围内,其他的一些不相关逻辑完全可以放在锁范围外处理

完整:加锁的目的是为了解决并发问题,所以锁里面的逻辑要求完整,不完整的逻辑=没加锁

比如幂等场景下:一般是 查重+写入 两个操作需要保证原子性,这两个操作加起来就是一个完整逻辑

精简:精简的意思是在保证完整的前提下,不要有多余的逻辑,以免锁占用时间过长,进而影响性能

2、锁粒度选择要恰当

锁粒度指的是:锁对请求条件筛选的粗细程度,例如用户购买商品下单时可以 1、根据用户id加锁,2、也可以根据用户id + 商品id加锁,1和2就是两种粒度

锁的粒度太大会导致锁的范围过大,可能会影响当前逻辑之外的业务

锁的粒度太小会导致锁的范围过小,可能会导致锁失效

3、锁过期时间设置要合理

一般建议过期时间 = 逻辑执行时间 * 150%

过期时间太小可能导致在逻辑执行完成前,锁过期失效

过期时间太大+解锁失败,可能导致在锁过期失效之前重试的请求被拒之门外

4、加锁在try之前,锁中的逻辑使用try catch包围,解锁在finally里处理

若加锁操作在try catch中,并发场景下未获取锁的操作会执行到finally里将锁解除,影响正常逻辑

防止逻辑中出现异常,阻断流畅,导致解锁操作执行不到

5、基于redis的分布式锁并不100%可靠

在一些特殊情况下,比如redis宕机,数据丢失,可能会导致锁失效