项目中最近迫切的需要一套分布式锁来解决一些接口的并发问题,在网上搜集了各种资料。并结合自己的项目场景,依赖Redis实现了一套分布式锁,使用简单,支持注解。在这里分享一下实现过程,希望能对你们有一些帮助。
核心包:
org.springframework.boot
spring-boot-starter-data-redis
2.2.5.RELEASE
redis.clients
jedis
io.lettuce
lettuce-core
org.redisson
redisson-spring-boot-starter
3.13.0
无需多说
spring:
redis:
host: xxxxx
password: xxxxx
timeout: 3000
port: 6379
redisson配置项需要单独配置一份yaml文件,在springboot的yml中配置是无法生效的。
redisson配置有两种方式:编码形式和配置式
方式一:纯编码形式无需编写yaml文件,直接对所需配置项进行配置就好:如下
@Configuration
public class RedissonConfig {
@Bean
@ConditionalOnProperty("spring.redis.host")
public RedissonClient redissonClient(){
Config config = new Config();
config.setTransportMode(TransportMode.EPOLL);
config.useClusterServers()
// use "rediss://" for SSL connection
.addNodeAddress("perredis://127.0.0.1:7181");
return Redisson.create(config);
}
}
配置方式二 配置式:新增redission.yaml配置文件
clusterServersConfig:
idleConnectionTimeout: 10000
connectTimeout: 10000
timeout: 3000
retryAttempts: 3
retryInterval: 1500
failedSlaveReconnectionInterval: 3000
failedSlaveCheckInterval: 60000
password: null
subscriptionsPerConnection: 5
clientName: null
loadBalancer: ! {}
subscriptionConnectionMinimumIdleSize: 1
subscriptionConnectionPoolSize: 50
slaveConnectionMinimumIdleSize: 24
slaveConnectionPoolSize: 64
masterConnectionMinimumIdleSize: 24
masterConnectionPoolSize: 64
readMode: "SLAVE"
subscriptionMode: "SLAVE"
nodeAddresses:
- "redis://127.0.0.1:7004"
- "redis://127.0.0.1:7001"
- "redis://127.0.0.1:7000"
scanInterval: 1000
pingConnectionInterval: 0
keepAlive: false
tcpNoDelay: false
threads: 16
nettyThreads: 32
codec: ! {}
transportMode: "NIO"
之后编写配置类,将此配置文件引入即可(注意路径)。
Config config = Config.fromYAML(new File("redisson.yaml"));
RedissonClient redisson = Redisson.create(config);
配置参数根据不同的redis使用场景(单机、主备、cluster等)需要自行去选择,官方文档都做了详细的讲解,具体配置参数请移步官网进行详细查看:redisso官方文档
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DistributedLock {
/**
* 分布式锁的key值,非空。支持el表达式获取入参对象参数
*/
String[] keys();
/**
* 分布式锁key前缀,默认采用类全限定名:方法名
*/
String prefix() default "";
/**
* key与前缀的分隔符
*/
String separator() default ":";
/**
* 等待获取锁的时间
*/
long waitTime() default 8;
/**
* 锁的租期,超时后自动释放
*/
long leaseTime() default 3;
/**
* 时间单位
*/
TimeUnit timeUnit() default TimeUnit.SECONDS;
}
考虑到我们的业务场景,这里的keys我是用了数组形式,可以获取多个入参的参数。支持EL
@Aspect
@Component
@Slf4j
public class DistributedLockAspect {
@Resource
DistributedLocker distributedLocker;
@Pointcut("@annotation(xxx.lock.annotation.DistributedLock)")
public void pointCut(){}
/**
* 环绕增强,尝试获取锁/释放锁
*
* @param joinPoint 切面
* @return Object
* @throws Throwable
*/
@Around("pointCut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method targetMethod = methodSignature.getMethod();
DistributedLock annotation = AnnotationUtils.findAnnotation(targetMethod, DistributedLock.class);
assert annotation != null;
String key=getLockKey(targetMethod,joinPoint,annotation);
boolean lockFlag =false;
Object proceed =null;
try {
lockFlag=distributedLocker.tryLock(key,annotation.timeUnit(),annotation.waitTime(),annotation.leaseTime());
if (lockFlag) {
log.info("success to get distributed lock with key {}",key);
proceed = joinPoint.proceed();
}
} catch (Exception exception) {
log.error("exception occurred while getting distributed lock ",exception);
return null;
}finally {
if (lockFlag){
distributedLocker.unlock(key);
log.info("lock {} has been released",key);
}
}
return proceed;
}
/**
* 获取拦截到的方法,解析分布式锁key值(如果包含el表达式,则从中解析出内容)
*
* @param joinPoint 切点
* @return redisKey
*/
private String getLockKey(Method targetMethod,
ProceedingJoinPoint joinPoint, DistributedLock targetAnnotation) {
Object target = joinPoint.getTarget();
Object[] arguments = joinPoint.getArgs();
StringBuilder stringBuilder=new StringBuilder();
for (int i=0;i
public class SpelUtil {
public static String parse(String spel, 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上下文中
if (paraNameArr != null) {
for (int i = 0; i < paraNameArr.length; i++) {
context.setVariable(paraNameArr[i], args[i]);
}
}
return parser.parseExpression(spel).getValue(context, String.class);
}
/**
* 支持 #p0 参数索引的表达式解析
* @param rootObject 根对象,method 所在的对象
* @param spel 表达式
* @param method ,目标方法
* @param args 方法入参
* @return 解析后的字符串
*/
public static String parse(Object rootObject,String spel, Method method, Object[] args) {
//获取被拦截方法参数名列表(使用Spring支持类库)
LocalVariableTableParameterNameDiscoverer u =
new LocalVariableTableParameterNameDiscoverer();
String[] paraNameArr = u.getParameterNames(method);
//使用SPEL进行key的解析
ExpressionParser parser = new SpelExpressionParser();
//SPEL上下文
StandardEvaluationContext context = new MethodBasedEvaluationContext(rootObject,method,args,u);
//把方法参数放入SPEL上下文中
if (paraNameArr != null) {
for (int i = 0; i < paraNameArr.length; i++) {
context.setVariable(paraNameArr[i], args[i]);
}
}
return parser.parseExpression(spel).getValue(context, String.class);
}
}
接口DistributedLocker
/**
* 分布式锁
*
* @author 小菜瓜
* @since 2020/6/8
*/
public interface DistributedLocker {
/**
* 以默认配置获取锁,等待锁默认5s,锁租期默认3s
*
* @param lockKey lockKey
* @return RLock
*/
RLock lock(String lockKey) ;
/**
* 自定义超时时间的锁,最多等待timeout秒,
*
* @param lockKey lockKey
* @param waitTime 超时时间,单位为秒
* @return lock
*/
RLock lock(String lockKey, long waitTime);
/**
* 自定义超时时间和时间单位的锁,
*
* @param lockKey lockKey
* @param waitTime 超时时间
* @param unit 时间单位
* @return lock
*/
RLock lock(String lockKey, TimeUnit unit, long waitTime);
/**
* 尝试加锁。自定义时间单位和锁等待时间,租期默认3s
*
* @param lockKey lockKey
* @param unit 时间单位
* @param waitTime 超时时间
* @return boolean
*/
boolean tryLock(String lockKey, TimeUnit unit, long waitTime);
/**
* 尝试加锁,自定义时间单位和锁等待时间以及锁过期时间
*
* @param lockKey key
* @param timeUnit 时间单位
* @param waitTime 等待超时时间
* @param leaseTime 锁租期,超过leaseTime自动释放
* @return boolean
*/
boolean tryLock(String lockKey,TimeUnit timeUnit, long waitTime , long leaseTime);
/**
* 公平锁,多个Redisson客户端线程同时请求加锁时,优先分配给先发出请求的线程。
* 尝试加锁,最多等待waitTime后强制获取锁,上锁以后leaseTime自动解锁
*
* @param lockKey 锁key
* @param unit 锁时间单位
* @param waitTime 等到最大时间,强制获取锁
* @param leaseTime 锁自动时间,
* @return 如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false
*/
boolean fairLock(String lockKey, TimeUnit unit, long waitTime, long leaseTime);
/**
* 获取公平锁,单位为秒
*
* @param lockKey lockKey
* @param waitTime 等待时间
* @param leaseTime 租期
* @return boolean
*/
boolean fairLock(String lockKey, long waitTime,long leaseTime);
/**
* 解锁
*
* @param lockKey lockKey
*/
void unlock(String lockKey);
/**
* 解锁RLock
*
* @param lock
*/
void unlock(RLock lock);
}
实现类DistributedLockImpl :在这里我们依赖RedissonClient 进行锁的实现
@Slf4j
@Component
public class DistributedLockImpl implements DistributedLocker{
/**
* 默认时间单位:秒
*/
public static final TimeUnit DEFAULT_TIME_UNIT= TimeUnit.SECONDS;
/**
* 默认锁等待超时时间
*/
public static final int DEFAULT_TIMEOUT=8;
/**
* 默认锁过期时间
*/
public static final int DEFAULT_LEASE_TIME=3;
@Resource
private RedissonClient redissonClient;
@Override
public RLock lock(String lockKey) {
RLock lock =redissonClient.getLock(lockKey);
try {
lock.tryLock(DEFAULT_TIMEOUT,DEFAULT_LEASE_TIME,DEFAULT_TIME_UNIT);
} catch (InterruptedException e) {
log.error("get lock with key {} failed,cause ",lockKey,e);
return null;
}
return lock;
}
@Override
public RLock lock(String lockKey, long timeout) {
return lock(lockKey,DEFAULT_TIME_UNIT,timeout);
}
@Override
public RLock lock(String lockKey, TimeUnit unit, long timeout) {
RLock lock =redissonClient.getLock(lockKey);
try {
lock.tryLock(timeout,DEFAULT_LEASE_TIME,unit);
} catch (InterruptedException e) {
log.error("get lock with key {} failed. cause",lockKey,e);
return null;
}
return lock;
}
@Override
public boolean tryLock(String lockKey, TimeUnit unit, long timeout) {
return tryLock(lockKey,unit,timeout,DEFAULT_LEASE_TIME);
}
@Override
public boolean tryLock(String lockKey, TimeUnit timeUnit, long waitTime, long leaseTime) {
RLock lock=redissonClient.getLock(lockKey);
try {
return lock.tryLock(waitTime,leaseTime,timeUnit);
} catch (InterruptedException e) {
log.error("get lock with key {} failed. cause",lockKey,e);
return false;
}
}
@Override
public boolean fairLock(String lockKey, TimeUnit unit, long waitTime, long leaseTime) {
RLock lock=redissonClient.getFairLock(lockKey);
try {
return lock.tryLock(waitTime,leaseTime,unit);
} catch (InterruptedException e) {
log.error("get lock with key {} failed. cause",lockKey,e);
return false;
}
}
@Override
public boolean fairLock(String lockKey, long waitTime, long leaseTime) {
return fairLock(lockKey,DEFAULT_TIME_UNIT,waitTime,leaseTime);
}
@Override
public void unlock(String lockKey) {
RLock lock=redissonClient.getLock(lockKey);
lock.unlock();
}
@Override
public void unlock(RLock lock) {
lock.unlock();
}
}
以上这一套分布式锁可以拿来就用,具体的 waitTime() default 8; leaseTime() default 3;等参数还需要各位根据自己的业务场景去进行调整。
如果有更好的改进方案和不对的地方还请各位大佬纠正和指教,感兴趣也可以可以一起讨论一下。