一、软件介绍
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 unLockTimes=new ConcurrentHashMap();
@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 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 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 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 result = new HashMap();
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 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