redis分布式锁的应用

前言:

项目需求,搞了搞

  1. 实现了锁的重入
  2. 参考了别人的博文实现了AOP注解形式的锁、统一配置

参考博文地址:

https://www.cnblogs.com/lijiasnong/p/9952494.html

这边看了下比较主流几个分布式锁的应用,最终选择的redis
原因是:
1、懒(服务器已有redis做缓存,不想再去安装zuukeeper)
2、评估认为redis的分布式锁已能满足当下应用

正文 - 摘录核心代码:

  1. RedisReentrantLock
import lombok.extern.log4j.Log4j2;
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分布式锁 - 使用 ThreadLocal 做线程隔离,实现锁的重入机制
 * @date 2020/12/11 9:13
 * @author wei.heng
 */
@Component
@Log4j2
public class RedisReentrantLock {

	/** 获取锁超时的时间 */
	private static final int ACQUIRE_LOCK_TIME_OUT_IN_MILLISECONDS = 3 * 1000;
	/** 自旋重试间隔 */
	private static final int WAIT_INTERVAL_IN_MILLISECONDS = 100;
	/** 锁的失效时间 */
	private static final Integer EXPIRE_SECONDS = 5;
	/** 锁的前缀,方便统一查看 */
	private static final String LOCK_PREFIX = "LOCK:";

	private ThreadLocal<Map<String, Integer>> lockers = new ThreadLocal<>();

	private RedisService redisService;

	@Autowired
	public RedisReentrantLock(RedisService redisService) {
		this.redisService = redisService;
	}

	/**
	 *
	 * 添加分布式锁
	 * @param key 键
	 * @return boolean 请求锁是否成功
	 * @date 2020/12/11 10:52
	 * @author wei.heng
	 */
	public boolean lock(String key){
		key = LOCK_PREFIX + key;
		Map<String, Integer> refs = currentLockers();
		Integer refCnt = refs.get(key);
		if(refCnt != null){
			// 当前线程已经加锁,这里属于锁的重入,计数器加1
			refs.put(key, refCnt + 1);
			return true;
		}
		boolean ok = this._lock(key);
		if(!ok){
			return false;
		}
		refs.put(key, 1);
		return true;
	}

	/**
	 *
	 * 释放锁
	 * @param key 键
	 * @return boolean 释放是否成功
	 * @date 2020/12/11 10:53
	 * @author wei.heng
	 */
	public boolean unlock(String key){
		key = LOCK_PREFIX + key;
		Map<String, Integer> refs = currentLockers();
		Integer refCnt = refs.get(key);
		if(refCnt == null){
			return false;
		}
		refCnt -= 1;
		if(refCnt > 0){
			refs.put(key, refCnt);
		} else {
			refs.remove(key);
			this._unlock(key);
		}
		return true;
	}

	private boolean _lock(String key){
		// 超时时间点
		long timeoutAt = System.currentTimeMillis() + ACQUIRE_LOCK_TIME_OUT_IN_MILLISECONDS;
		while(true){
			boolean isSuccess = redisService.setIfAbsent(key, "", EXPIRE_SECONDS);
			if(isSuccess){
				// 如果加锁成功,就返回
				return isSuccess;
 			}
			// 如果加锁失败,证明当前锁被其他线程占用,进入自旋
			if(System.currentTimeMillis() < timeoutAt){
				try {
					TimeUnit.MILLISECONDS.sleep(WAIT_INTERVAL_IN_MILLISECONDS);
				} catch (InterruptedException e) {
					log.error("redis锁自旋等待被打断...", e);
					e.printStackTrace();
				}
			} else {
				// 自旋等待时间超时,返回获取锁失败
				isSuccess = false;
				return isSuccess;
			}
		}
	}

	private void _unlock(String key){
		redisService.deleteObject(key);
	}

	private Map<String, Integer> currentLockers(){
		Map<String, Integer> refs = lockers.get();
		if(refs != null){
			return refs;
		}
		lockers.set(new HashMap<>());
		return lockers.get();
	}

}


  1. RedisService 核心代码
    /**
     *
     * 如果key不存在,则设值
     * @param key 键
     * @param value 值
     * @param exptime 过期时间 - 单位:秒
     * @return boolean 设值是否成功(如果已有该key存在,则返回false)
     * @date 2020/12/11 10:42
     * @author wei.heng
     */
    public boolean setIfAbsent(final String key, final Serializable value, final long exptime) {
        Boolean result = (Boolean) redisTemplate.execute((RedisCallback<Boolean>) connection -> {
            RedisSerializer valueSerializer = redisTemplate.getValueSerializer();
            RedisSerializer keySerializer = redisTemplate.getKeySerializer();
            Object obj = connection.execute("set", keySerializer.serialize(key),
                valueSerializer.serialize(value),
                "NX".getBytes(StandardCharsets.UTF_8),
                "EX".getBytes(StandardCharsets.UTF_8),
                String.valueOf(exptime).getBytes(StandardCharsets.UTF_8));
            return obj != null;
        });
        return result;
    }

到这里,redis的分布式锁已经可以使用了。
看到网上别人的帖子,为了方便,
下面继续做了下抄袭、修改
再次重申,抄袭的地址:

https://www.cnblogs.com/lijiasnong/p/9952494.html
  1. RedisLockable 新建注解

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

/**
 * <pre>
 * redis分布式锁
 * 使用示例(可参见 AppUserController 里被注释掉的 test 函数示例):
 * 1、按对象属性加锁@RedisLockable("#ObjectName.attributeName")
 * 2、按函数入参加锁@RedisLockable("#functionParamName")
 * 3、按对象参数 + 函数参数加锁:@RedisLockable("#ObjectName.attributeName.concat('#').concat(#functionParamName)
 * 4、非String的多参数加锁 @RedisLockable(value = {"businessId", "businessType"})
 * </pre>
 * @date 2020/12/11 11:48
 * @author wei.heng
 */
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface RedisLockable {
    /** redis锁的关键字(keys),可使用SpEL表达式代表参数值 */
    String[] value() default "";
}
  1. ReflectParamNames
    这个类需要引入jar包,我这里搜了个最新的
        <dependency>
            <groupId>javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.12.1.GA</version>
        </dependency>

import javassist.*;
import javassist.bytecode.CodeAttribute;
import javassist.bytecode.LocalVariableAttribute;
import javassist.bytecode.MethodInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 反射获取参数名
 * @date 2020/12/11 14:03
 * @author wei.heng
 */
public class ReflectParamNames {

	private static Logger log = LoggerFactory.getLogger(ReflectParamNames.class);
	private  static ClassPool pool = ClassPool.getDefault();

	static{
		ClassClassPath classPath = new ClassClassPath(ReflectParamNames.class);
		pool.insertClassPath(classPath);
	}

	public static String[] getNames(String className,String methodName) {
		CtClass cc = null;
		try {
			cc = pool.get(className);
			CtMethod cm = cc.getDeclaredMethod(methodName);
			// 使用javaassist的反射方法获取方法的参数名
			MethodInfo methodInfo = cm.getMethodInfo();
			CodeAttribute codeAttribute = methodInfo.getCodeAttribute();
			LocalVariableAttribute attr = (LocalVariableAttribute) codeAttribute.getAttribute(LocalVariableAttribute.tag);
			if (attr == null) return new String[0];

			int begin = 0;

			String[] paramNames = new String[cm.getParameterTypes().length];
			int count = 0;
			int pos = Modifier.isStatic(cm.getModifiers()) ? 0 : 1;

			for (int i = 0; i < attr.tableLength(); i++){
				//  为什么 加这个判断,发现在windows 跟linux执行时,参数顺序不一致,通过观察,实际的参数是从this后面开始的
				if (attr.variableName(i).equals("this")){
					begin = i;
					break;
				}
			}

			for (int i = begin+1; i <= begin+paramNames.length; i++){
				paramNames[count] = attr.variableName(i);
				count++;
			}
			return paramNames;
		} catch (Exception e) {
			e.printStackTrace();
		}finally{
			try {
				if(cc != null) cc.detach();
			} catch (Exception e2) {
				log.error(e2.getMessage());
			}


		}
		return new String[0];
	}
}
  1. RedisLockAspect
package com.applet.common.security.aspect;



import com.applet.common.core.exception.GetRedisLockFailureException;
import com.applet.common.redis.util.RedisReentrantLock;
import com.applet.common.security.annotation.RedisLockable;
import com.applet.common.security.utils.ReflectParamNames;
import com.google.common.base.Joiner;
import com.alibaba.fastjson.JSON;
import lombok.extern.log4j.Log4j2;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;

import java.io.Serializable;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * 注解形式的redis分布式锁
 * @date 2020/12/11 14:10
 * @author wei.heng
 */
@Aspect
@Log4j2
@Component
public class RedisLockAspect {

	private RedisReentrantLock redisLock;
	/** 释放锁的超时毫秒数 - 超过该时间则不进行锁的释放(锁已失效,此时再进行释放,会导致其他持有该锁的线程异常) */
	private static int EXPIRE_MILLISECONDS = RedisReentrantLock.EXPIRE_SECONDS * 1000;

	@Autowired
	public RedisLockAspect(RedisReentrantLock redisLock) {
		this.redisLock = redisLock;
	}

	@Around("@annotation(com.applet.common.security.annotation.RedisLockable)")
	public Object around(ProceedingJoinPoint point) throws Throwable {
		Signature signature = point.getSignature();
		MethodSignature methodSignature = (MethodSignature) signature;
		Method method = methodSignature.getMethod();
		String targetName = point.getTarget().getClass().getName();
		String methodName = point.getSignature().getName();
		Object[] arguments = point.getArgs();

		RedisLockable redisLockable = method.getAnnotation(RedisLockable.class);
		String[] values = redisLockable.value();
		String redisKey;
		if(values.length > 1) {
			//获取参数名
			String[] parameterNames = methodSignature.getParameterNames();
			redisKey = getLockKeyMap(targetName, methodName,redisLockable.value(), parameterNames, arguments);
		} else {
			redisKey = getLockKey(targetName, methodName, redisLockable.value(), arguments);
		}
		long timeOutAt = System.currentTimeMillis() + EXPIRE_MILLISECONDS;
		boolean isLock = redisLock.lock(redisKey);
		if(isLock) {
			try {
				return point.proceed();
			} finally {
				if(System.currentTimeMillis() < timeOutAt){
					// 当前时间超过超时时间点,则不进行锁的释放(锁已失效,此时再进行释放,会导致其他持有该锁的线程异常)
					redisLock.unlock(redisKey);
				}
			}
		} else {
			log.info("未获取到锁:" + redisKey);
			throw new GetRedisLockFailureException();
		}
	}

	private String getLockKey(String targetName, String methodName, String[] keys, Object[] arguments) {

		StringBuilder sb = new StringBuilder();
		sb.append(targetName).append(".").append(methodName);

		if(keys != null) {
			String keyStr = Joiner.on(".").skipNulls().join(keys);
			String[] parameters = ReflectParamNames.getNames(targetName, methodName);
			ExpressionParser parser = new SpelExpressionParser();
			Expression expression = parser.parseExpression(keyStr);
			EvaluationContext context = new StandardEvaluationContext();
			int length = parameters.length;
			if (length > 0) {
				for (int i = 0; i < length; i++) {
					Object argument = arguments[i];
					context.setVariable(parameters[i], argument);
					if(argument instanceof Long){
						Long keysValue = expression.getValue(context, Long.class);
						sb.append("#").append(keysValue);
					} else if (argument instanceof Integer){
						Integer keysValue = expression.getValue(context, Integer.class);
						sb.append("#").append(keysValue);
					} else {
						String keysValue = expression.getValue(context, String.class);
						sb.append("#").append(keysValue);
					}
				}
			}
		}
		return sb.toString();
	}
	
	
	
	/**
	 * 改方法获取 参数值必须只有一级,转换map获取 如:keys = {"name1","name2"}
	 * @param targetName
	 * @param methodName
	 * @param keys
	 * @param parameterNames
	 * @param arguments
	 * @return
	 * @author liugang
	 */
	private String getLockKeyMap(String targetName, String methodName, String[] keys,String[] parameterNames, Object[] arguments) {
		StringBuilder sb = new StringBuilder();
		sb.append(targetName).append(".").append(methodName).append(":");
		if(arguments != null && arguments.length > 0) {
			Map<String, Object> map = new HashMap<String, Object>();
			for(int i = 0; i < arguments.length; i++) {
				Object ob = arguments[i];
				if(ob == null) {
					continue;
				}
				if(ob instanceof String || ob instanceof Integer || ob instanceof Long
						|| ob instanceof Number || ob instanceof Float || ob instanceof Double 
						|| ob instanceof BigDecimal || ob instanceof Date || ob instanceof Boolean ) {
					map.put(parameterNames[i], ob);
				} else if(ob instanceof Serializable) {
					String json = JSON.toJSONString(ob);
					map.putAll(JSON.parseObject(json, Map.class));
				} else if(ob instanceof LinkedHashMap || ob instanceof HashMap || ob instanceof Map) {
					map.putAll((Map)ob);
				} else {
					map.put(parameterNames[i], ob);
				}
			}
			for(String key : keys) {
				if(map.get(key) != null) {
					sb.append("#").append(map.get(key));
				}
			}
		}
		return sb.toString();
	}

}

到这里全部结束,可以直接使用@RedisLockable 进行分布式锁的同步控制了()

测试:

在这里插入图片描述
发出两个postman请求,参数相同,第一个OK,第二个返回409
修改其中某个参数、导致不是同一把锁,两个请求均通过(通过的场景就不截图了)
在这里插入图片描述

PS:

2020年12月14日某同事反馈这个锁有BUG,多次请求发现ThreadLocal里的计数器在累加,
所以提问:按照线程隔离的字面意思,每次请求都是一个新的线程,计数器应该都从0开始才对

这里有个知识点是,ThreadLocal的线程隔离,是根据线程名来的
经过测试,发现请求N次后,会再次循环之前的线程名,这里程序就识别为和之前的请求是同一个线程了(线程名称一样),于是出现了计数器的累加

这个是ThreadLocal实现原理的问题了,不属于BUG范畴,我们只要正确使用就好了
该同事非正常使用锁导致的问题,既没有使用这里写的注解方式做锁的同步操作,在显式地使用锁后又未进行释放
我们知道Java所有的手动锁,都是使用后需要手动释放资源的,在正确使用的情况下,不存在这个问题

2021年01月21日有朋友反馈 redis锁安全问题

鉴于上文中讨论的问题,个人理解,问题三描述的是真实存在的BUG(这个也是我在使用之前就已知的)
简单描述下:进程1超时导致锁自动释放,进程2拿到锁,这时进程1突然又响应了、把进程2的锁释放掉了
这里我想了一种补偿机制:在获取到锁资源的时候,生成时间戳,在释放锁之前通过时间戳判断是否超过了redis的超时时间X秒,若超过了则认为锁已自动释放、不再进行锁的释放(redis unlock)操作
虽然这个极限情况下,BUG仍然存在的,因为生成时间戳和获取redis锁资源,不是原子操作,会有间隔时间(N纳秒或M微秒),但已能极大地规避该问题的发生。对于极小概率就等于不存在这句话我还是不赞同,但根据自身的业务需求评估,认为已能满足当下需求(这里业务出错率零容忍的可以止步了)
现在redis还有个什么redlock,暂时没有去研究,有兴趣的朋友可以去了解下

文中描述的问题2和问题4,我理解是不存在的:
我们在加锁的时候,使用了redisTemplate.execute,Spring Data Redis 提供的 SessionCallback 的接口支持多个操作的执行都在同一个连接中,所以文中描述的SETNX和EXPIRE操作,实际上是原子的
而锁的重入是在本地LocalThread中实现的,由于线程隔离,所以也不存在交叉释放的问题
在这里插入图片描述

在这里插入图片描述

生产中的应用

@RedisLockable 配合 @Cacheable 优雅地解决缓存穿透的问题
用 CyclicBarrier barrier 写了段50个线程的并发测试,只会查询一次数据库,后面的49次直接返回缓存信息 -> 自测OK
在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值