文章目录
- 前言
- 第一节 入门使用Redisson
- 第二节 注解形式的分布式锁
- 1. 分布式锁的注解实现
- 2. 分析MyRedissonLock注解和使用
前言
并发执行是比较场景的场景,单机情况下,我们可以利用锁机制来实现顺序执行。然而微服务时代,多节点运行,如何让某业务可以同一时刻只允许一个任务运行呢?
Redisson实现分布式锁的用法,可以很容易实现分布式锁的配置。
第一节 入门使用Redisson
- 导入依赖
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.16.6</version>
</dependency>
- 编写RedissonConfig
package com.it2.springbootredisson.config;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.io.IOException;
@Configuration
public class RedissonConfig {
@Value("${spring.redis.host:localhost}")
private String host;
@Value("${spring.redis.port:6379}")
private String port;
/**
* RedissonClient,单机模式
* @return
* @throws IOException
*/
@Bean(destroyMethod = "shutdown")
public RedissonClient redisson() throws IOException {
Config config = new Config();
config.useSingleServer().setAddress("redis://" + host + ":" + port);
return Redisson.create(config);
}
}
- 配置redis
RedissonConfig里已经包含了redis默认的localhost:6379了,如果redis的配置不是默认值,则需要在配置文件中配置。
spring:
redis:
host: xxx.xxx.xxx.xxx
port: 16379
- 编写测试的service业务实现
public interface TestService {
void hello(String name);
}
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
@Service
@Slf4j
public class TestServiceImpl implements TestService {
@Resource
RedissonClient redissonClient;
private int ticket=100;
private final static String LOCK_KEY = "TICKET_KEY";
@Override
public void hello(String name) {
//定义锁
RLock lock = redissonClient.getLock(LOCK_KEY);
try {
//尝试加锁,最大等待时间2秒(如果还没等到锁,则失败),最大持锁时间10秒(防止意外情况无法释放)
if (lock.tryLock(2000, 10000, TimeUnit.MILLISECONDS)) {
log.info("线程:" + Thread.currentThread().getName() + "获得了锁");
log.info("剩余数量:{}", --ticket);
}
} catch (Exception e) {
log.error("程序执行异常:{}", e);
} finally {
log.info("线程:" + Thread.currentThread().getName() + "准备释放锁");
//释放锁
lock.unlock();
}
}
}
- 编写测试用例,模拟并发抢票,并执行
@Autowired
private TestService testService;
@Test
public void testTicket() throws InterruptedException {
class TicketThread extends Thread{
private TestService testService;
public TicketThread(TestService testService,String name){
super(name);
this.testService=testService;
}
public void run(){
testService.hello("abc");
}
}
List<TicketThread> ticketThreads=new ArrayList<>();
for (int i=0;i<30;i++){
TicketThread ticketThread=new TicketThread(testService,"thread-"+i);
ticketThreads.add(ticketThread);
}
TimeUnit.SECONDS.sleep(2);
ticketThreads.forEach(t->{
t.start();
});
TimeUnit.HOURS.sleep(1);
}
可以看到各线程在竞争到锁后才能取得执行资格,保证了同一时刻跨服务也只能有一个线程拥有执行权限,并且是卖票是有序执行的。
第二节 注解形式的分布式锁
前面我们使用redisson做分布式锁,很显然我们需要在方法内进行改造,如何更加简单的实现分布式锁?我们可以利用注解的形式的无侵入实现分布式锁,对于业务的改造也就更加简单。
1. 分布式锁的注解实现
- 导入依赖
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.16.6</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
</dependency>
- spel解析器(用于支持spel表达式)
package com.it2.springbootredisson.util;
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 java.util.Map;
/**
* spel工具
*/
public class SpELUtil {
/**
* spel转换
* @param spelExpression
* @param variables
* @param clazz
* @param <T>
* @return
*/
public static <T> T parse(String spelExpression, Map<String, Object> variables, Class<T> clazz) {
ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
context.setVariables(variables);
Expression exp = parser.parseExpression(spelExpression);
return exp.getValue(context, clazz);
}
}
- 定义注解
package com.it2.springbootredisson.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyRedissonLock {
/**
* 空间
* @return
*/
String cacheName() default "";
/**
* 分布式锁key
*/
String key() default "";
/**
* 获取锁等待时间(默认2000毫秒,还没获取到锁即放弃)
*/
long waitTime() default 2000;
/**
* 锁的过期时间,默认60秒,超时自动失效
*/
long expire() default 60_000;
/**
* key的生成器
*
* @return
*/
String keyGenerator() default "";
}
- 定义RedissonConfig
package com.it2.springbootredisson.config;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.io.IOException;
@Configuration
public class RedissonConfig {
@Value("${spring.redis.host:localhost}")
private String host;
@Value("${spring.redis.port:6379}")
private String port;
/**
* RedissonClient,单机模式
* @return
* @throws IOException
*/
@Bean(destroyMethod = "shutdown")
public RedissonClient redisson() throws IOException {
Config config = new Config();
config.useSingleServer().setAddress("redis://" + host + ":" + port);
return Redisson.create(config);
}
}
- 定义key生成器
package com.it2.springbootredisson.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.lang.reflect.Method;
@Configuration
@Slf4j
public class MyRedissonKeyGenerator {
/**
* 默认的key生成器
*/
public static final String myKeyGeneratorByMethodName = "myKeyGeneratorByMethodName";
/**
* 类全名+方法名
* @return
*/
@Bean(myKeyGeneratorByMethodName)
public KeyGenerator myKeyGeneratorByMethodName() {
KeyGenerator keyGenerator = new KeyGenerator() {
@Override
public Object generate(Object target, Method method, Object... params) {
String className = target.getClass().getName();
String methodName = method.getName();
StringBuffer cacheKey = new StringBuffer();
cacheKey.append(className).append(".").append(methodName);
return cacheKey.toString();
}
};
return keyGenerator;
}
}
- 实现aop的核心
package com.it2.springbootredisson.config;
import com.it2.springbootredisson.annotation.MyRedissonLock;
import com.it2.springbootredisson.util.SpELUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import org.springframework.cache.interceptor.KeyGenerator;
import javax.annotation.Resource;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
@Component
@Aspect
@Slf4j
public class RedissonLockAspect implements ApplicationContextAware {
@Resource
private RedissonClient redissonClient;
@Around("@annotation(distributedLock)")
public Object lock(ProceedingJoinPoint proceedingJoinPoint, MyRedissonLock distributedLock) {
String key = distributedLock.key();//key
String keyGenerator = distributedLock.keyGenerator();//自定义的keyGenerator
String cacheName = distributedLock.cacheName();//缓存空间
long waitTime = distributedLock.waitTime(); //最大等待时间 毫秒
long expire = distributedLock.expire(); //失效时间(防止线程意外无法释放锁) 毫秒
waitTime = waitTime < 0 ? 0 : waitTime;
expire = expire < 0 ? 0 : expire;
StringBuffer rKey = new StringBuffer();
try {
Signature signature = proceedingJoinPoint.getSignature();
String className = proceedingJoinPoint.getTarget().getClass().getName();
String methodName = signature.getName();
Object[] objects = proceedingJoinPoint.getArgs();
Class[] parameterTypes = new Class[objects.length];
/**
* 打印输入的参数
*/
for (int i = 0; i < objects.length; i++) {
Object obj = objects[i];
// System.out.println("type:" + obj.getClass().getSimpleName() + ", value:" + obj);
parameterTypes[i] = obj.getClass();
}
Method method = signature.getDeclaringType().getMethod(methodName, parameterTypes);
Parameter[] parameters = method.getParameters();
// for (Parameter parameter : parameters) {
// System.out.println(parameter.getName() + "," + parameter.getType() + ",");
// }
if (!"".equals(cacheName)) {
rKey.append(cacheName).append("::");
}
if (!key.equals("")) {
//利用spel表达式转换key
Map<String, Object> variables = new HashMap<>();
for (int i = 0; i < parameters.length; i++) {
variables.put(parameters[i].getName(), objects[i]);
}
rKey.append(SpELUtil.parse(key, variables, String.class));
} else if (!keyGenerator.equals("")) {
//未设置key,则按照默认规则生成key
KeyGenerator kg = applicationContext.getBean(keyGenerator, KeyGenerator.class);
rKey.append(kg.generate(proceedingJoinPoint.getTarget(), method, objects).toString());
} else {//如果未设置key,也没有使用keyGenerator,则使用默认的key生成器
KeyGenerator kg = applicationContext.getBean(MyRedissonKeyGenerator.myKeyGeneratorByMethodName, KeyGenerator.class);
rKey.append(kg.generate(proceedingJoinPoint.getTarget(), method, objects).toString());
}
} catch (Exception e) {
throw new RuntimeException("运行时异常",e);
}
RLock lock = redissonClient.getLock(rKey.toString());
boolean hasLock = false;
try {
hasLock = lock.tryLock(waitTime, expire, TimeUnit.MILLISECONDS);
if (hasLock) {
log.info("获取分布式锁成功,key={}", rKey);
return proceedingJoinPoint.proceed();
} else {
log.info("获取锁失败");
}
} catch (Throwable e) {
log.error("切面分布式锁异常:{}", e);
} finally {
if (hasLock) {
lock.unlock();
log.info("解锁成功:{}", rKey);
}
}
return null;
}
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
- 使用注解实现redisson分布式锁
默认的分布式锁是方法锁,key为className.method,如果存在方法重载的情况,则对于同类的重载方法用的都是同一把锁。如果你希望对重载也做隔离锁,那么你可以通过修改key的生成器策略来处理,或者使用cacheName进行重载方法的隔离也是一样效果。
- 运行测试代码(前面的测试用例),可以看到使用注解简单的实现了分布式锁。
2. 分析MyRedissonLock注解和使用
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyRedissonLock {
/**
* 空间
* @return
*/
String cacheName() default "";
/**
* 分布式锁key
*/
String key() default "";
/**
* 获取锁等待时间(默认2000毫秒,还没获取到锁即放弃)
*/
long waitTime() default 2000;
/**
* 锁的过期时间,默认60秒,超时自动失效
*/
long expire() default 60_000;
/**
* key的生成器
*
* @return
*/
String keyGenerator() default "";
}
key: 支持spel表达式,例如
@MyRedissonLock(key = "#name") //获取参数
@MyRedissonLock(key = "'lock-'+#name") //拼接参数(下图为示例)
@MyRedissonLock(key = "mylock") //常量参数
cacheName: 缓存空间,不同名的cacheName ,相互之间无关联。这样即使key的规则一致,两个不同的cacheName也不会发生锁竞争。与上面的demo中@MyRedissonLock(key = “‘lock-’+#name”)方式相比而言,这里实际只是分离了key而已,但是好处是规则简单了,可以复用key规则。
#下面两个redissonLock,虽然key规则一致,但是cacheName不一致,它们竞争的不是同一把锁。
@MyRedissonLock(cacheName = "hello",key = "#name")
@MyRedissonLock(cacheName = "hello2",key = "#name")
waitTime: 最大等待多久,如果到了最大等待时间仍未获取锁则失败。单位毫秒。
expire:锁的失效时间,获取锁后,最大允许持有锁多久,超时会自动释放锁。单位毫秒。锁失效是为了避免因为某些意外锁长期占有无法释放导致线程堵塞。
waitTime和expire需根据业务具体情况设定
keyGenerator: key的生成器,前面的代码里已经包含了一个默认的生成器myKeyGeneratorByMethodName,它生成的规则是全类名+方法名,如果用户既未指定key,也没有指定key生成器,则使用默认的myKeyGeneratorByMethodName。用户可以根据自己的需求,自定义生成器。可参照demo定义自己的生成器策略,并使用。
@MyRedissonLock //默认为@MyRedissonLock(keyGenerator = "myKeyGeneratorByMethodName")
@MyRedissonLock(keyGenerator = "myKeyGeneratorByXXX") //自定义
redis分布式锁官方文档
http://www.redis.cn/topics/distlock.html