分布式锁(一)Redis分布式锁注解灵活实现

  • 前言

在日常开发中,为了防止高并发,在不依赖过多的中间件的情况下,最常使用的分布式锁之一是 Redis锁。使用Redis锁就不得不面临一个问题,就是在业务代码中要控制Redis加锁、释放锁等等,对代码的侵入性较强。本文将详细介绍Redis分布式锁的实现原理以及已注解的形式灵活控制锁。

  • 优点:

  1. 线程间锁互斥。在同一时间内,仅有一个线程持有锁,避免多个线程同时执行逻辑,出现并发情况。
  2. 可重试。若一个线程第一次没有拿到锁,将会等待N秒后,重新尝试获得锁,重试次数可自定义,避免某一线程没有第一时间获得锁直接失败。
  3. 无死锁。即使某一线程中断没能释放锁,在到达指定的时间后,程序会自动释放锁。
  4. 锁唯一独有。加锁和释放锁必须由同一线程执行,不会出现A线程加锁后,B线程将锁释放。
  5. 无侵入。通过注解实现加锁和释放锁,代码中只需关注业务实现,无须关心“锁”问题,避免代码侵入。
  6. 支持多种方式传参做key。通过注解指定参数名称和类型,通过反射,灵活获得指定的参数。
  • 代码实现

spring相关依赖不在具体粘代码了,操作Redis用到了 Jedis 组件,因用到了jedis 2.9版本的功能,引入组件时,请注意版本、

        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>2.9.0</version>
        </dependency>

RedisLock 注解类

代码解释:

  1. 注解需传入businessFlag,标识当前加锁的业务,保证锁在当前业务逻辑下有效。
  2. 使用业务唯一的参数,作为key,保证锁的唯一。
  3. 通过反射实现支持多种取参方式,如直接取值、json/map取值,以及业务封装的对象取值。使用对象的属性做key时,需通过fieldName指定属性名称
  4. 可根据业务自定锁时间,有默认值,保证即使线程中断,到达指定时间后会自动释放锁,避免出现死锁。

代码

package com.lsz.common.annotation;

import java.lang.annotation.*;

/**
 * Redis锁注解
 *
 * @author lishuzhen
 * @date 2021/6/1 21:12
 */
@Documented
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RedisLock {

    /**
     * key取值方式
     */
    enum KeyType {
        /**
         * 直接取值
         * <p>
         * 如 String,Integer...
         */
        VALUE,
        /**
         * 通过对象属性取值
         * <p>
         * 如 User,Order...
         */
        CLASS,
        /**
         * 序列化后通过 key:value 取值
         * <p>
         * 如 JSONObject,Map...
         */
        JSON
    }


    /**
     * 方法签名中,将要作为key的参数名称
     */
    String keyName();

    /**
     * key取值方式
     *
     * @return
     */
    KeyType keyType() default KeyType.VALUE;

    /**
     * key 参数类型
     *
     * @return
     */
    Class keyClass() default String.class;

    /**
     * 业务标识
     * <p>
     * 防止key重复
     * <p>
     * 如 订单锁 传入 ORDER 等等
     *
     * @return
     */
    String businessFlag();

    /**
     * 锁自动释放时间,默认30s自动释放锁
     *
     * @return
     */
    long lockTime() default 30L;

    /**
     * 若 KeyType = JSON, 通过 fieldName 作为 key 取 JSON 中的值,作为 LockKey
     * <p>
     * 若 KetType = ClASS, 通过获取对象的field,属性取值
     *
     * @return
     */
    String fieldName() default "";


}

RedisLockAspect 使用AOP切面处理注解,实现功能

代码解释:

  1. 生成uuid作为requestId,意为当前线程的请求ID,将其作为value存入redis中,在释放锁时通过LUA脚本检查 requestId 是否一致,保证谁加的锁由谁来释放,确保锁唯一独有,解铃还须系铃人。
  2. 对Redis进行操作时,设置SET_IF_NOT_EXIST = NX, 保证当key不存在时,才进行set操作;key存在则不执行任何操作。
  3. 对Redis进行操作时,设置 SET_WITH_EXPIRE_TIME = PX,给当前key设置过期时间,保证不会出现死锁,过期时间从RedisLock注解中获取,默认最长30s,可自定义。
  4. 定义 MAX_RETRY_GET_LOCK 和 WAIT_LOCK_TIME ,控制获得锁重试的次数和每次等待的时间。
  5. LUA脚本解释:KEYS[1]赋值为lockKey,ARGV[1]赋值为requestId;功能是先通过key获得value,再检查value和requestId是否一致,如何一致则删除当前key,也就是释放锁;不一致则表明加锁线程不是当前线程,不可以释放锁。使用LUA可以包直上诉操作是原子性的。简单来说就是在jedis.eval()方法执行LUA脚本时,会将其作为一条命令执行,并且直到命令执行完毕,Redis才会执行下一条命令。

代码

package com.lsz.common.aspect;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.lsz.common.annotation.RedisLock;
import com.lsz.common.exception.BusinessError;
import com.lsz.common.exception.BusinessRuntimeException;
import com.lsz.common.utils.JoinPointUtils;
import com.lsz.common.utils.IdUtils;
import com.lsz.common.redis.JedisPoolUtil;
import org.apache.commons.lang.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.lang.reflect.Field;
import java.util.Collections;


/**
 * Redis锁 切面
 *
 * @author lishuzhen
 * @date 2021/6/1 21:45
 */
@Aspect
@Component
public class RedisLockAspect {
    private static org.apache.logging.log4j.Logger logger = LogManager.getLogger();

    /**
     * redis key
     */
    private static final String KEY_PRE = "LOCK_";

    /**
     * 获取锁 最大重试次数
     */
    private static final Integer MAX_RETRY_GET_LOCK = 3;

    /**
     * 等待锁的时间 5s
     */
    private static final Long WAIT_LOCK_TIME = 5000L;

    /**
     * 当key不存在时,进行set操作
     */
    private static final String SET_IF_NOT_EXIST = "NX";

    /**
     * 过期的设置
     */
    private static final String SET_WITH_EXPIRE_TIME = "PX";

    /**
     * lua脚本 释放锁
     */
    private static final String REDIS_DEL_LOCK_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";


    @Autowired
    private JedisPoolUtil jedisPoolUtil;


    /**
     * redis 加锁切入点
     */
    @Pointcut(value = "@annotation(com.lsz.common.annotation.RedisLock) && args(..)")
    public void redisLockPointCut() {
    }

    /**
     * 使用环绕通知,控制加锁和释放锁
     *
     * @param joinPoint
     * @return
     * @throws Throwable
     */
    @Around("redisLockPointCut()")
    public Object redisLockAction(ProceedingJoinPoint joinPoint) throws Throwable {
        Object result = new Object();
        String lockKey = "";
        String requestId = IdUtils.uuid32();
        try {
            RedisLock redisLock = JoinPointUtils.getMethodAnnotation(joinPoint, RedisLock.class);
            Object obj = JoinPointUtils.getParamByName(joinPoint, redisLock.keyName(), redisLock.keyClass());
            lockKey = getRedisKey(redisLock, obj);

            // 检查key是否可以加锁
            if (checkingLock(lockKey)) {
                // 一直被锁,不可以上锁
                logger.info("目前此 lockKey => {} 一直被锁,无法获得锁,导致程序中断", lockKey);
                // 锁替换为随机码,防止将原有的锁释放
                lockKey = IdUtils.uuid();
                throw new BusinessRuntimeException(BusinessError.LOCK_WAIT_TIME_OUT);
            }

            jedisPoolUtil.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, redisLock.lockTime() * 1000);
            logger.info("此 lockKey => {} 已获得redis锁, requestId = {}", lockKey, requestId);

            result = joinPoint.proceed();
        } catch (Throwable e) {
            if (e instanceof BusinessRuntimeException) {
                freedLock(lockKey, requestId);
                throw e;
            }
            logger.error("服务器异常", e);
        } finally {
            freedLock(lockKey, requestId);
        }

        return result;
    }


    /**
     * 检查锁,尝试检查 MAX_RETRY_GET_LOCK 此
     *
     * @param lockKey
     * @return 是否一直有锁
     * @throws InterruptedException
     */
    private Boolean checkingLock(String lockKey) throws InterruptedException {
        Boolean isLock = true;
        // 尝试 MAX_RETRY_GET_LOCK 次 检查锁
        for (int count = 1; count <= MAX_RETRY_GET_LOCK; count++) {
            isLock = isLock(lockKey);
            if (isLock) {
                logger.info("第 {} 检查,目前此 lockKey => {}已上锁,等待 {}ms 再次检查", count, lockKey, WAIT_LOCK_TIME);
                Thread.sleep(WAIT_LOCK_TIME);
            } else {
                logger.info("第 {} 检查,目前此 lockKey => {}未锁定,可以加锁", count, lockKey);
                return false;
            }
        }

        return isLock;
    }


    /**
     * 是否已加锁
     *
     * @param key
     * @return
     */
    private boolean isLock(String key) {
        String lock = jedisPoolUtil.get(key);
        if (StringUtils.isBlank(lock) || "null".equals(lock)) {
            return false;
        }
        return true;
    }

    /**
     * 释放锁
     *
     * @param key
     */
    private void freedLock(String key, String requestId) {
        jedisPoolUtil.eval(REDIS_DEL_LOCK_SCRIPT, Collections.singletonList(key), Collections.singletonList(requestId));
        logger.info("释放redis锁,lockKey = {}, requestId = {}", key, requestId);
    }



    /**
     * 根据key不同的参数类型,获取key值,并拼接返回 Redis Key
     *
     * @param redisLock
     * @param obj
     * @return
     */
    private String getRedisKey(RedisLock redisLock, Object obj) throws NoSuchFieldException, IllegalAccessException {
        StringBuffer key = new StringBuffer(KEY_PRE + redisLock.businessFlag() + "_");
        if (RedisLock.KeyType.VALUE.equals(redisLock.keyType())) {
            key.append(obj);
        } else if (RedisLock.KeyType.JSON.equals(redisLock.keyType())) {
            JSONObject json = JSONObject.parseObject(JSON.toJSONString(obj));
            key.append(json.getString(redisLock.fieldName()));
        } else if (RedisLock.KeyType.CLASS.equals(redisLock.keyType())) {
            Field field = obj.getClass().getDeclaredField(redisLock.fieldName());
            field.setAccessible(true);
            key.append(field.get(obj));
        }

        return key.toString();
    }



}

JoinPointUtils 操作Spring JoinPoint的工具类 。若是boot项目,无须配置,此工具类可直接使用。若是spring mvc项目,有可能会出现无法获取实现类方法签名等情况,请检查spring配置文件中是否有此配置  

<aop:aspectj-autoproxy proxy-target-class="true" />
package com.lsz.common.utils;

import org.apache.commons.lang3.ArrayUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.reflect.MethodSignature;

import java.lang.annotation.Annotation;

/**
 * JoinPoint 工具类
 *
 * @author lishuzhen
 * @date 2020/10/10 18:27
 */
public class JoinPointUtils {

    /**
     * 从 joinPoint 中 根据 参数名称 获取参数
     *
     * @param joinPoint
     * @param paramName
     * @return
     * @author Lishuzhen
     */
    public static <T> T getParamByName(JoinPoint joinPoint, String paramName, Class<T> clazz) {
        Object[] args = joinPoint.getArgs();
        MethodSignature methodSignature = getMethodSignature(joinPoint);
        String[] parameterNames = methodSignature.getParameterNames();
        int index = ArrayUtils.indexOf(parameterNames, paramName);

        if (index < 0) {
            return null;
        }

        Object obj = args[index];
        if (clazz.isInstance(obj)) {
            return clazz.cast(obj);
        }

        return (T) obj;
    }


    /**
     * 从 joinPoint 获取 方法上的注解
     *
     * @param joinPoint
     * @return
     */
    public static <T extends Annotation> T getMethodAnnotation(JoinPoint joinPoint, Class<T> annotationClass) throws NoSuchMethodException {
        return getMethodSignature(joinPoint).getMethod().getAnnotation(annotationClass);
    }


    /**
     * 在 joinPoint 中获取 MethodSignature
     *
     * @param joinPoint
     * @return
     */
    public static MethodSignature getMethodSignature(JoinPoint joinPoint) {
        return (MethodSignature) joinPoint.getSignature();
    }
}

JedisPoolUtils Jedis工具类 ,这里就不在粘代码了,就是简单封装了一下获取连接和释放连接, 调用jedis方法直接透传。

Demo

手写Demo代码进行测试。

package com.lsz.common;

import com.lsz.common.annotation.RedisLock;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.Map;

/**
 * RedisLock Demo
 *
 * @author lishuzhen
 * @date 2021/6/1 22:07
 */
@ResponseBody
@RequestMapping("redisLock/test")
public class TestController {


    @RedisLock(businessFlag = "payOrder", keyName = "orderNo")
    @RequestMapping("testValue")
    public Object testValue(String orderNo) {
        System.out.println("Test Value This is my order " + orderNo);
        return "ok";
    }

    @RedisLock(businessFlag = "payOrder", keyName = "order",
            keyType = RedisLock.KeyType.CLASS,
            keyClass = Order.class,
            fieldName = "orderNo")
    @RequestMapping("testBean")
    public Object testBean(Order order) {
        System.out.println("Test Bean This is my order " + order.getOrderNo());
        return "ok";
    }


    @RedisLock(businessFlag = "payOrder", keyName = "paramMap",
            keyType = RedisLock.KeyType.JSON,
            fieldName = "orderNo")
    @RequestMapping("testJson")
    public Object testJson(Map<String, Object> paramMap) {
        System.out.println("Test json, This is my order " + paramMap.get("orderNo"));
        return "ok";
    }

    @RedisLock(businessFlag = "payOrder", keyName = "orderNo", lockTime = 20)
    @RequestMapping("testRetry")
    public Object testRetry(String orderNo) {
        System.out.println("start pay order" + orderNo);
        try {
            // 模拟业务逻辑用时
            Thread.sleep(6000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("end pay order" + orderNo);

        return "ok";
    }

}

自定义一bean,demo使用

package com.lsz.common;

import java.io.Serializable;

/**
 * Demo bean
 * @author lishuzhen
 * @date 2021/6/1 22:11
 */
public class Order implements Serializable {
    private static final long serialVersionUID = 6213874569224877786L;


    private String orderNo;
    private String userNo;

    // ...

    public String getOrderNo() {
        return orderNo;
    }

    public void setOrderNo(String orderNo) {
        this.orderNo = orderNo;
    }

    public String getUserNo() {
        return userNo;
    }

    public void setUserNo(String userNo) {
        this.userNo = userNo;
    }
}

测试注解值取参方式,访问 testValue ,查看日志

2021-06-01 22:26:11.364 INFO  com.lsz.common.aspect.RedisLockAspect 134 checkingLock - 第 1 检查,目前此 lockKey => LOCK_payOrder_Test001未锁定,可以加锁
2021-06-01 22:26:11.382 INFO  com.lsz.common.aspect.RedisLockAspect 102 redisLockAction - 此 lockKey => LOCK_payOrder_Test001 已获得redis锁, requestId = ff36ccdf29e849cca5b5d565939f7c09
Test Value This is my order Test001
2021-06-01 22:26:11.416 INFO  com.lsz.common.aspect.RedisLockAspect 163 freedLock - 释放redis锁,lockKey = LOCK_payOrder_Test001, requestId = ff36ccdf29e849cca5b5d565939f7c09

测试注解对象取参方式,访问 testBean

2021-06-01 22:28:09.936 INFO  com.lsz.common.aspect.RedisLockAspect 134 checkingLock - 第 1 检查,目前此 lockKey => LOCK_payOrder_Test002未锁定,可以加锁
2021-06-01 22:28:09.960 INFO  com.lsz.common.aspect.RedisLockAspect 102 redisLockAction - 此 lockKey => LOCK_payOrder_Test002 已获得redis锁, requestId = db2189b9702349b889b334027ce4e6d3
Test Bean This is my order Test002
2021-06-01 22:28:09.984 INFO  com.lsz.common.aspect.RedisLockAspect 163 freedLock - 释放redis锁,lockKey = LOCK_payOrder_Test002, requestId = db2189b9702349b889b334027ce4e6d3

测试注解JSON/Map取参方式,访问TestJson

2021-06-01 22:31:25.791 INFO  com.lsz.common.aspect.RedisLockAspect 134 checkingLock - 第 1 检查,目前此 lockKey => LOCK_payOrder_Test003未锁定,可以加锁
2021-06-01 22:31:25.812 INFO  com.lsz.common.aspect.RedisLockAspect 102 redisLockAction - 此 lockKey => LOCK_payOrder_Test003 已获得redis锁, requestId = 6d142ded58bb46cb868a72dc0f174a4f
Test json, This is my order Test003
2021-06-01 22:31:25.846 INFO  com.lsz.common.aspect.RedisLockAspect 163 freedLock - 释放redis锁,lockKey = LOCK_payOrder_Test003, requestId = 6d142ded58bb46cb868a72dc0f174a4f

测试重试获得锁,两个客户端访问 testRetry

2021-06-01 23:02:04.558 INFO  com.lsz.common.aspect.RedisLockAspect 134 checkingLock - 第 1 检查,目前此 lockKey => LOCK_payOrder_Test003未锁定,可以加锁
2021-06-01 23:02:04.587 INFO  com.lsz.common.aspect.RedisLockAspect 102 redisLockAction - 此 lockKey => LOCK_payOrder_Test003 已获得redis锁, requestId = 89a4b362896d4f64a8ff9191d27a953d
start pay orderTest003
2021-06-01 23:02:05.897 INFO  com.lsz.common.aspect.RedisLockAspect 131 checkingLock - 第 1 检查,目前此 lockKey => LOCK_payOrder_Test003已上锁,等待 5000ms 再次检查
end pay orderTest003
2021-06-01 23:02:10.646 INFO  com.lsz.common.aspect.RedisLockAspect 163 freedLock - 释放redis锁,lockKey = LOCK_payOrder_Test003, requestId = 89a4b362896d4f64a8ff9191d27a953d
2021-06-01 23:02:10.924 INFO  com.lsz.common.aspect.RedisLockAspect 134 checkingLock - 第 2 检查,目前此 lockKey => LOCK_payOrder_Test003未锁定,可以加锁
2021-06-01 23:02:10.948 INFO  com.lsz.common.aspect.RedisLockAspect 102 redisLockAction - 此 lockKey => LOCK_payOrder_Test003 已获得redis锁, requestId = 51057fa3168d452d97f4f36e9078ebb7
start pay orderTest003
end pay orderTest003
2021-06-01 23:02:16.986 INFO  com.lsz.common.aspect.RedisLockAspect 163 freedLock - 释放redis锁,lockKey = LOCK_payOrder_Test003, requestId = 51057fa3168d452d97f4f36e9078ebb7

总结:

以上记录博主在是用Redis做分布式锁的考虑到问题及解决方案,实现了博客开头提及的几个优点,目前满足大部分的业务场景。同时对业务代码零侵入,开发人员无需关心“锁”问题,一个注解轻松解决。

 

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Redis通过自定义注解实现分布式锁。在代码中,我们可以看到使用了一个名为`@RedisLockAnnotation`的注解\[3\]。这个注解被用于标记需要加锁的方法。在注解的定义中,可以指定锁的类型和锁的过期时间。通过在切面中定义一个`@Pointcut`,来拦截使用了`@RedisLockAnnotation`注解的方法\[2\]。在拦截到这些方法后,会执行分布式锁的逻辑。具体的实现是通过Redisson来获取分布式锁对象,并使用`tryLock`方法来尝试获取锁\[1\]。如果成功获取到锁,就可以执行业务逻辑;如果获取锁失败,则需要等待或执行其他逻辑。这样,通过注解和切面的配合,我们可以方便地在需要的地方实现分布式锁的功能。 #### 引用[.reference_title] - *1* [尚品汇之通过自定义注解实现分布式锁](https://blog.csdn.net/weixin_65627952/article/details/128188867)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* *3* [使用注解实现REDIS分布式锁](https://blog.csdn.net/LinkSLA/article/details/130419280)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值