基于spring aop 环绕通知实现 redis的分布式锁

人生之路坎坎坷坷,跌跌撞撞在所难免。但是,不论跌了多少次,你都必须坚强勇敢地站起来。任何时候,无论你面临着生命的何等困惑抑或经受着多少挫折,无论道路多艰难,希望变得如何渺茫,请你不要绝望,再试一次,坚持到底,成功终将属于勇不言败的你。

导读:本篇文章讲解 基于spring aop 环绕通知实现 redis的分布式锁,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com,来源:原文

        <!-- json -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.49</version>
        </dependency>

        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.3.7</version>
        </dependency>

        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.util.concurrent.TimeUnit;

@Component
public class RedisUtil {

    private RedisTemplate redisTemplate;

    @Autowired
    public void setRedisTemplate(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    /**
     * 指定缓存失效时间
     *
     * @param key  键
     * @param time 时间(秒)
     * @return
     */
    public boolean expire(String key, long time) {
        try {
            if (time > 0) {
                redisTemplate.expire(key, time, TimeUnit.SECONDS);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 给指定key存放 value,指定的key若已存在值 失败
     * redis分布式锁
     *
     * @param key
     * @param value
     * @return
     */
    public boolean setIfAbsent(String key, Object value) {
        try {
            return redisTemplate.opsForValue().setIfAbsent(key, value);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 删除缓存
     *
     * @param key 可以传一个值 或多个
     */
    @SuppressWarnings("unchecked")
    public void del(String... key) {
        if (key != null && key.length > 0) {
            if (key.length == 1) {
                redisTemplate.delete(key[0]);
            } else {
                redisTemplate.delete(CollectionUtils.arrayToList(key));
            }
        }
    }

    /**
     * 普通缓存放入并设置时间
     *
     * @param key   键
     * @param value 值
     * @param time  时间(秒) time要大于0 如果time小于等于0 将设置无限期
     * @return true成功 false 失败
     */
    public boolean set(String key, Object value, long time) {
        try {
            if (time > 0) {
                redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
            } else {
                set(key, value);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 普通缓存放入
     *
     * @param key   键
     * @param value 值
     * @return true成功 false失败
     */
    public boolean set(String key, Object value) {
        try {
            redisTemplate.opsForValue().set(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }

    }

    /**
     * 普通缓存获取
     *
     * @param key 键
     * @return 值
     */
    public Object get(String key) {
        return key == null ? null : redisTemplate.opsForValue().get(key);
    }


}


import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;


@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface RedisLock {

    /**
     * RedisKeyPrefix.LOCK + key
     * key 默认 class.method
     * @return
     */
    String key() default "";

    // 过期时长
    long duration() default 1000L;

    // key 是否加上入参
    String keyAddArgs() default "";

    // 并发策略,true 直接抛异常, false 重复获取 锁
    boolean isThrowMsg() default true;

}

方式一 自旋锁实现

import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
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.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * 并发redis分布式锁
 */
@Component
@Aspect
@Slf4j
public class RedisLockAop {

    @Autowired
    RedisUtil redisUtil;


    /**
     * 切点注解拦截
     */
    @Pointcut("@annotation(RedisLock)")
    public void aspect() {
    }


    /**
     * redis锁
     * key = RedisKeyPrefix.LOCK  + redisLock.key
     * redisLock.key 默认 class.method
     */
    @Around("@annotation(redisLock)")
    public Object around(ProceedingJoinPoint point, RedisLock redisLock) throws Throwable {
        log.info("redis锁执行开始========");
        // redis key
        String key = redisLock.key();
        if (StrUtil.isBlank(key)) {
            String targetClassName = point.getTarget().getClass().getName();
            String targetMethodName = point.getSignature().getName();
            key = targetClassName + "." + targetMethodName;
        }
        key = RedisKeyPrefix.LOCK + key;
        // key + 入参
        if (point.getArgs().length > 0) {
            RedisLock method = getMethod(point);
            String args = getAnnotationValue(point, method.keyAddArgs());
            key += ":args:" + args ;
        }
        log.info("key :{}", key);
        // 获取锁,若失败,判断是否死锁,是:释放并重新获取锁
        while (true) {
            boolean aBoolean = redisUtil.setIfAbsent(key, new Date(System.currentTimeMillis() + redisLock.duration()).getTime());
            if (!aBoolean) {
                Object o = redisUtil.get(key);
                if (o == null) {
                    // 不存在key 锁已释放,重新获取
                    continue;
                }
                long oldExpireTime = Long.parseLong((o.toString()));
                long currentTime = System.currentTimeMillis();
                // 判断锁是否过期
                if (currentTime >= oldExpireTime) {
                    // 锁已过期
                    redisUtil.del(key);
                    continue;
                }
            }
            if (aBoolean) {
                log.info("获取到锁");
                break;
            }
            if (redisLock.isThrowMsg()) {
                // 没有获取到锁 直接抛异常
                throw new RuntimeException("没有获取到锁 直接抛异常");
            } else {
                // 没有获取到锁 等待5秒尝试再去获取锁
                log.info("没有获取到锁 等待5秒尝试再去获取锁");
                TimeUnit.SECONDS.sleep(5);
            }
        }

        // 锁获取成功后,指定失效时间,防止死锁
        redisUtil.expire(key, redisLock.duration());
        Object proceed;
        try {
            proceed = point.proceed();
        } finally {
            redisUtil.del(key);
        }
        log.info("redis锁执行结束========");
        return proceed;
    }

    /**
     * 获取注解中对方法的描述信息
     */
    public static RedisLock getMethod(JoinPoint joinPoint) {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        return signature.getMethod().getAnnotation(RedisLock.class);
    }

    /**
     * 获取注解中传递的动态参数的参数值
     * 1.“#{参数1},#{参数2}”  多个参数使用逗号分割
     * 2.“#{对象.参数1},#{对象.参数2}” 复杂的参数类型:对象.参数名
     * 3. 非动态参数直接返回
     * @param joinPoint
     * @param paramNames
     * @return
     */
    public String getAnnotationValue(JoinPoint joinPoint, String paramNames) {
        String[] split = paramNames.split(",");
        StringBuffer key = new StringBuffer();
        for (String paramName : split) {
            // 获取方法中所有的参数
            Map<String, Object> params = getParams(joinPoint);
            // 参数是否是动态的:#{paramName}
            if (paramName.matches("^#\\{\\D*\\}")) {
                // 获取参数名
                paramName = paramName.replace("#{", "").replace("}", "");
                // 是否是复杂的参数类型:对象.参数名
                if (paramName.contains(".")) {
                    String[] paramNameSplit = paramName.split("\\.");
                    // 获取方法中对象的内容
                    Object object = getValue(params, paramNameSplit[0]);
                    // 转换为JsonObject
                    JSONObject jsonObject = (JSONObject) JSONObject.toJSON(object);
                    // 获取值
                    Object o = jsonObject.get(paramNameSplit[1]);
                    key.append(paramName).append(":").append(o).append(":");
                }else {
                    // 简单的动态参数
                    key.append(paramName).append(":").append(getValue(params, paramName)).append(":");
                }
            }
        }
        // 非动态参数直接返回
        return key.toString();
    }

    /**
     * 根据参数名返回对应的值
     *
     * @param map
     * @param paramName
     * @return
     */
    public Object getValue(Map<String, Object> map, String paramName) {
        for (Map.Entry<String, Object> entry : map.entrySet()) {
            if (entry.getKey().equals(paramName)) {
                return entry.getValue();
            }
        }
        return null;
    }

    /**
     * 获取方法的参数名和值
     *
     * @param joinPoint
     * @return
     */
    public Map<String, Object> getParams(JoinPoint joinPoint) {
        Map<String, Object> params = new HashMap<>(8);
        Object[] args = joinPoint.getArgs();
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        String[] names = signature.getParameterNames();
        for (int i = 0; i < args.length; i++) {
            params.put(names[i], args[i]);
        }
        return params;
    }



}


方式二 redisson 看门狗实现

链接: 分布式锁Redisson的使用,看门狗机制.

        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson-spring-boot-starter</artifactId>
            <version>3.15.3</version>
        </dependency>


import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
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.aspectj.lang.reflect.MethodSignature;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * 并发redis分布式锁
 */
@Component
@Aspect
@Slf4j
public class RedisLockAop {

    @Autowired
    RedisUtil redisUtil;

    @Autowired
    RedissonClient redisson;

    /**
     * 切点注解拦截
     */
    @Pointcut("@annotation(RedisLock)")
    public void aspect() {
    }


    /**
     * redis锁
     * key = RedisKeyPrefix.LOCK  + redisLock.key
     * redisLock.key 默认 class.method
     */
    @Around("@annotation(redisLock)")
    public Object around(ProceedingJoinPoint point, RedisLock redisLock) throws Throwable {
        // redis key
        String key = redisLock.key();
        if (StrUtil.isBlank(key)) {
            String targetClassName = point.getTarget().getClass().getName();
            String targetMethodName = point.getSignature().getName();
            key = targetClassName + "." + targetMethodName;
        }
        key = RedisKeyPrefix.LOCK + key;
        // key + 入参
        if (point.getArgs().length > 0) {
            RedisLock method = getMethod(point);
            String args = getAnnotationValue(point, method.keyAddArgs());
            key += ":args:" + args ;
        }
        // 获取一把锁,只要锁的名字一样就是同一把锁
        log.info("key :{}", key);
        RLock lock = redisson.getLock(key);
        // 加锁 自动解锁时间
        lock.lock(redisLock.duration(), TimeUnit.SECONDS);
        log.info("redis锁执行开始========");
        Object proceed;
        try {
            proceed = point.proceed();
        } finally {
            if(lock.isLocked()){ // 是否还是锁定状态
                if(lock.isHeldByCurrentThread()){ // 时候是当前执行线程的锁
                    lock.unlock(); // 释放锁
                }
            }
        }
        log.info("redis锁执行结束========");
        return proceed;
    }

    /**
     * 获取注解中对方法的描述信息
     */
    public static RedisLock getMethod(JoinPoint joinPoint) {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        return signature.getMethod().getAnnotation(RedisLock.class);
    }

    /**
     * 获取注解中传递的动态参数的参数值
     * 1.“#{参数1},#{参数2}”  多个参数使用逗号分割
     * 2.“#{对象.参数1},#{对象.参数2}” 复杂的参数类型:对象.参数名
     * 3. 非动态参数直接返回
     * @param joinPoint
     * @param paramNames
     * @return
     */
    public String getAnnotationValue(JoinPoint joinPoint, String paramNames) {
        String[] split = paramNames.split(",");
        StringBuffer key = new StringBuffer();
        for (String paramName : split) {
            // 获取方法中所有的参数
            Map<String, Object> params = getParams(joinPoint);
            // 参数是否是动态的:#{paramName}
            if (paramName.matches("^#\\{\\D*\\}")) {
                // 获取参数名
                paramName = paramName.replace("#{", "").replace("}", "");
                // 是否是复杂的参数类型:对象.参数名
                if (paramName.contains(".")) {
                    String[] paramNameSplit = paramName.split("\\.");
                    // 获取方法中对象的内容
                    Object object = getValue(params, paramNameSplit[0]);
                    // 转换为JsonObject
                    JSONObject jsonObject = (JSONObject) JSONObject.toJSON(object);
                    // 获取值
                    Object o = jsonObject.get(paramNameSplit[1]);
                    key.append(paramName).append(":").append(o).append(":");
                }else {
                    // 简单的动态参数
                    key.append(paramName).append(":").append(getValue(params, paramName)).append(":");
                }
            }
        }
        // 非动态参数直接返回
        return key.toString();
    }

    /**
     * 根据参数名返回对应的值
     *
     * @param map
     * @param paramName
     * @return
     */
    public Object getValue(Map<String, Object> map, String paramName) {
        for (Map.Entry<String, Object> entry : map.entrySet()) {
            if (entry.getKey().equals(paramName)) {
                return entry.getValue();
            }
        }
        return null;
    }

    /**
     * 获取方法的参数名和值
     *
     * @param joinPoint
     * @return
     */
    public Map<String, Object> getParams(JoinPoint joinPoint) {
        Map<String, Object> params = new HashMap<>(8);
        Object[] args = joinPoint.getArgs();
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        String[] names = signature.getParameterNames();
        for (int i = 0; i < args.length; i++) {
            params.put(names[i], args[i]);
        }
        return params;
    }



}


问题描述

当我们使用Ression中Lock.lock()方法之后,如果存在线程并发常见情况下,会出现如下异常:
java.lang.IllegalMonitorStateException: attempt to unlock lock, not locked by current thread by node id: 9f178836-f7e1-44fe-a89d-2db52f399c0d thread-id: 22

问题分析

在thread-1还没有结束的时候,也就是在thread-1在获得锁但是还没有释放锁的时候, `thread-2由于被别的线程中断停止了等待从lock.tryLock的阻塞状态中返回继续执行接下来的逻辑,并且由于尝试去释放一个属于线程thread-1的锁而抛出了一个运行时异常导致该线程thread-2结束了, 然而thread-2完成了一系列操作后,线程thread-1才释放了自己的锁. 所以thread-2并没有获得锁,却执行了需要同步的内容,还尝试去释放锁。那解决方式我们就知道了,当前线程加的锁由当前线程去解锁,也就是说当我们使用lock.unlock的时候加上线程的判断即可。

问题解决

 RLock lock = redissonClient.getLock(key);
    if(lock.isLocked()){ // 是否还是锁定状态
      if(lock.isHeldByCurrentThread()){ // 时候是当前执行线程的锁
        lock.unlock(); // 释放锁
      }
    }

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/133926.html

(0)
飞熊的头像飞熊bm

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!