spring-boot-lock-starter基于redis的分布式锁实现

贾飞鸿
2023-12-01

一、软件介绍

       spring-boot-lock-starter 是基于redis实现的简单分布式锁。软件面向接口编程,同时兼顾基于zk或者其他实现的方便扩展。基于redis的分布式锁实现,主要依赖以reids 的set命令 和get del 的lua脚本。同时对锁做了注解封装,预留是否启用分布式锁、和是否启用默认redis实现类完成分布式锁的扩展。

二、核心实现

1,加锁过程

    /**
	 * 加锁函数
	 * @param key 锁key
	 * @param value 锁值
	 * @param timeOut 超时时间
	 * @param tryNum  尝试重先加锁次数:tryNum==0(不尝试,直接返回),tryNum>0(尝试tryNum次),tryNum<0(只到尝试成功为止)
	 * @param sleep  每次尝试延时多少秒
	 * @return
	 */
	public boolean lock(String key,String value,long timeOut,int tryNum,long sleep){
		Jedis jedis = jedisPool.getResource();
		String result = jedis.set(key, value, "NX", "PX", timeOut);
		jedis.close();
		if("OK".equals(result)){
			return true;
		}else{
			if(tryNum<0){
				delayed(sleep);
				return lock(key,value,timeOut,tryNum,sleep);
			}else if(tryNum>0){
				delayed(sleep);
				tryNum--;
				return lock(key,value,timeOut,tryNum,sleep);
			}else{
				return false;
			}
		}
	}

        加锁过程主要是调用redis的set命令,其中NX和PX表示当key存在时设置失败,超过过期时间自动删除key。需要注意的是低版本的redis是不支持set 传多个参数的。

        方法做了个递归调用,允许尝试tryNum次加锁。

    2,解锁代码实现

    /**
	 * 解锁函数
	 */
	public boolean  unlock(String key, String value,int tryNum,long sleep){
		Jedis jedis = jedisPool.getResource();
		String script = "local lockVal=redis.call('get', KEYS[1]) if not lockVal then return 1 elseif  lockVal== ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
		Object result = jedis.eval(script, Collections.singletonList(key), Collections.singletonList(value));
		jedis.close();
		if(result.equals(1L)){
			return true;
		}else{
			if(tryNum<0){
				delayed(sleep);
				return unlock(key,value,tryNum,sleep);
			}else if(tryNum>0){
				delayed(sleep);
				tryNum--;
				return unlock(key,value,tryNum,sleep);
			}else{
				return false;
			}
		}
		
	}

        解锁代码中与redis官网给出的示例不同的是当key过期时间很短,做并发测试时,当key过期了再采取解锁时就会返回解锁失败。所以这里我做了个修改,当key不存在时直接返回解锁成功。

    3,注解封装

        3.1 定义一个注解

package com.github.dgw.lock.annotation;

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

/**
 * 自定义注解,基于redis的分布式锁
 * @author dgw
 *
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RedisLock {

	//key的前缀
	String prefix() default "lock_";
	
	//需要加锁的字段
	String key() default "";
	
	//加锁超时时间默认10秒
	long timeOut() default 10000;
	
	//尝试加锁次数
	int tryNum() default 5;
	
	//每次尝试重新加锁或解锁延时时间
	long sleep() default 500;
	
}

    3.2 定义注解的切面

    

package com.github.dgw.lock.aspect;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
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 org.springframework.util.StringUtils;

import com.github.dgw.lock.annotation.RedisLock;
import com.github.dgw.lock.core.LockContextService;
import com.github.dgw.lock.exception.LockEnum;
import com.github.dgw.lock.exception.LockException;
import com.github.dgw.lock.util.StringUtil;


@Aspect
@Component
@SuppressWarnings("all")
public class RedisLockAspect {
	 
	private final static Logger logger=LoggerFactory.getLogger(RedisLockAspect.class);
	  //保存解锁时间
	 private final static ConcurrentHashMap<String,String>  unLockTimes=new ConcurrentHashMap<String,String>();
	 
	 @Autowired
	 private LockContextService lockContextService;

	 //只切带RedisLock注解的方法
	 @Pointcut("@annotation(com.github.dgw.lock.annotation.RedisLock)")
	 public void executeService(){
		 
     }
	 
	 /**
	  * 环绕通知
	 * @return 
	 * @throws Throwable 
	  */
	 @Around("executeService()")
	 public Object doAroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
		//1,获取方法参数,作为redis存储时的key 
		Map<String, Object> methodParam = getMethodParam(proceedingJoinPoint);
		
		//2,用方法参数作为key,对方法加锁
		if(methodParam!=null){
			boolean lockMethed=lockMethed(methodParam);
			//加锁成功调用目标方法
			if(lockMethed){
				return proceedingJoinPoint.proceed();
			}else{
				logger.info(LockEnum.LOCK_FAIL.getMsg());
				throw new LockException(LockEnum.LOCK_FAIL);
			}
		}
		return null;
	 }
	 
	 private boolean lockMethed(Map<String, Object> methodParam) {
		//调用redis,进行加锁
		String prefix =(String)methodParam.get("prefix");
		String key =prefix+(String)methodParam.get("key");
		long id = Thread.currentThread().getId();
		String value=StringUtil.uuid(prefix+id);
		long timeOut=(Long)methodParam.get("timeOut");
		int tryNum=(Integer)methodParam.get("tryNum");
		long sleep=(Long)methodParam.get("sleep");
		boolean lock = lockContextService.lock(key, value, timeOut, tryNum, sleep);
		if(lock){
			//如果加锁成功,则把过期时间放到ConcurrentHashMap中,用来解锁
			unLockTimes.put(key+id, value);
		}
		return lock;
	 }

	//获取注解方法参数
	 public  Map<String, Object> getMethodParam(JoinPoint joinPoint){
		
		 Class target = joinPoint.getTarget().getClass();
		 Method[] methods = target.getMethods();
		 String methodName = joinPoint.getSignature().getName();
		 
		 for(Method method:methods){
			 if(method.getName().equals(methodName)){
				 Map<String, Object> result = new HashMap<String, Object>();
				 RedisLock annotation = method.getAnnotation(RedisLock.class);
				 String parseKey = parseKey(annotation.key(), method, joinPoint.getArgs());
				 result.put("prefix", annotation.prefix());
				 result.put("key", parseKey);
				 result.put("timeOut", annotation.timeOut());
				 result.put("tryNum", annotation.tryNum());
				 result.put("sleep", annotation.sleep());
				 return result;
			 }
		 }
		 return null;
	 }
	 /**
      *    获取缓存的key
      *    key 定义在注解上,支持SPEL表达式
      * @param pjp
      * @return
      */
     private String parseKey(String key,Method method,Object [] args){
         
         //获取被拦截方法参数名列表(使用Spring支持类库)
         LocalVariableTableParameterNameDiscoverer u =new LocalVariableTableParameterNameDiscoverer();  
         String [] paraNameArr=u.getParameterNames(method);
         
         //使用SPEL进行key的解析
         ExpressionParser parser = new SpelExpressionParser(); 
         //SPEL上下文
         StandardEvaluationContext context = new StandardEvaluationContext();
         //把方法参数放入SPEL上下文中
         for(int i=0;i<paraNameArr.length;i++){
             context.setVariable(paraNameArr[i], args[i]);
         }
         return parser.parseExpression(key).getValue(context,String.class);
     }
	 
	 /**
	  * 前置通知,方法调用前被调用
	  */
	 @Before("executeService()")
	 public void doBeforeAdvice(JoinPoint joinPoint){
	 }
	 
	 /**
	  * 后置通知,方法调用后被调用
	  */
	 @After("executeService()")
	 public void doAfterAdvice(JoinPoint joinPoint){
		 unlockMethod(joinPoint);
	 }
	 
	 /**
	  * 后置异常通知
	  */
	 @AfterThrowing(value="executeService()",throwing="exception")
	 public void doAfterThrowingAdvice(JoinPoint joinPoint,Throwable exception){
		  unlockMethod(joinPoint);
	 }
	 
	 /**
	  * 对方法进行解锁
	  * @param joinPoint
	  */
	 public void unlockMethod(JoinPoint joinPoint){
		 Map<String, Object> methodParam = getMethodParam(joinPoint);
		//调用redis,进行加锁
		 String prefix =(String)methodParam.get("prefix");
		 String key =prefix+(String)methodParam.get("key");
		 int tryNum = (Integer) methodParam.get("tryNum");
		 long sleep=(Long)methodParam.get("sleep");
		 long id = Thread.currentThread().getId();
		 String value = unLockTimes.get(key+id);
		 if(!StringUtils.isEmpty(value)){
			 boolean unlock = lockContextService.unlock(key, value, tryNum, sleep);
			 if(unlock){
				 unLockTimes.remove(key+id);
			 }
		 }
	 }
	 
	 /**
	  * 后置返回通知
	  */
	 @AfterReturning("executeService()")
	 public void doAfterReturningAdvice(JoinPoint joinPoint){
		 
	 }
}

三、扩展测试

    1,使用过程

	 
	//秒杀
	@GetMapping("/misosha/{prodId}")
	@RedisLock(key="#prodId",timeOut=3000,tryNum=-1)
	public String miaosha(@PathVariable("prodId")String prodId){
		order.put(UUID.randomUUID().toString(), "商品");
		Integer stock = prod.get(prodId);
		stock-=1;
		prod.put(prodId, stock);
		return "剩余库存为:"+prod.get(prodId)+",成功下单数:"+order.size();
	}

       使用过程很简单,直接在方法上加该注解即可。但是在高并发的场景下,从锁的细腻度来考虑,个人不建议直接使用注解。那样会在非核心目标上持有锁的时间过长,导致吞吐量降低。而是直接采用@Autowired注入LockContextService来掉用方法。

     2,为了方便使用和方便扩展,我把该事项封装成了一个jar包。可以很方便的和spring boot 集成。

之所以采用LockContextService类而不是RedisLockService,是为了方便扩展直接用LockContextService来切换使用哪一个实现类。通过动态从spring bean池中获取具体的实现类来调用。

完整项目已经上传github:https://github.com/baishuirouqing/spring-boot-lock-starter

 

转载于:https://my.oschina.net/zhaomin/blog/2101321

 类似资料: