<!--核心依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.12.5</version>
</dependency>
0:定义一个注解
在需要使用分布式锁的地方打上该注解即可,注解中的lockParamExpression字段传入Spring的Spel表达式,用于取出方法中需要加锁的字段
/**
* @author liujiazhong
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RedissonLock {
/**
* 锁定参数表达式
*/
String lockParamExpression();
/**
* 锁自动释放时间(秒)
*/
int leaseTime() default 60;
}
1:AOP实现
这里我们需要对打了我们上面定义的注解的地方做增强,从注解中取出加锁参数的表达式,解析出需要加锁的参数,然后使用Redisson客户端进行加锁。这里有两个时间需要注意,一个是获取锁时的等待时间,一个是锁自动释放的时间,这两个需要根据业务实际情况设定
/**
* @see RedissonLock
*
* @author liujiazhong
*/
@Slf4j
@Aspect
@Order(1)
@Component
public class RedissonLockAspect {
private static final Integer LOCK_WAIT_TIME = 5;
private static final String KEY_PREFIX = "stock:quantity:update:lock:{%s}";
private final RedissonClient redissonClient;
public RedissonLockAspect(RedissonClient redissonClient) {
this.redissonClient = redissonClient;
}
@Around("@annotation(redissonLock)")
public Object around(ProceedingJoinPoint joinPoint, RedissonLock redissonLock) throws Throwable {
validRedissonLock(redissonLock);
Long productId = parseExpression(joinPoint, redissonLock);
String key = String.format(KEY_PREFIX, productId);
Object obj;
RLock lock = redissonClient.getLock(key);
boolean lockResult = lock.tryLock(LOCK_WAIT_TIME, redissonLock.leaseTime(), TimeUnit.SECONDS);
if (lockResult) {
log.info("acquire the lock:{}", key);
try {
obj = joinPoint.proceed();
} finally {
unlock(lock);
}
log.info("releases the lock:{}", key);
} else {
throw new RuntimeException(String.format("try lock fail:productId:%s", productId));
}
return obj;
}
private void validRedissonLock(RedissonLock redissonLock) {
if (StringUtils.isBlank(redissonLock.lockParamExpression())) {
throw new RuntimeException("no lock param expression.");
}
}
private Method getTargetMethod(ProceedingJoinPoint pjp) throws NoSuchMethodException {
Signature signature = pjp.getSignature();
MethodSignature methodSignature = (MethodSignature)signature;
Method agentMethod = methodSignature.getMethod();
return pjp.getTarget().getClass().getMethod(agentMethod.getName(),agentMethod.getParameterTypes());
}
private Long parseExpression(ProceedingJoinPoint joinPoint, RedissonLock redissonLock) throws NoSuchMethodException {
String lockParam = redissonLock.lockParamExpression();
Method targetMethod = getTargetMethod(joinPoint);
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = new MethodBasedEvaluationContext(new Object(), targetMethod, joinPoint.getArgs(),
new DefaultParameterNameDiscoverer());
Expression expression = parser.parseExpression(lockParam);
return expression.getValue(context, Long.class);
}
private void unlock(RLock lock) {
try {
lock.unlock();
} catch (Exception e) {
log.error("unlock exception.", e);
throw new RuntimeException("unlock exception.");
}
}
}
2:对业务进行加锁
在需要加锁的业务上打上注解即可
案例:查出当前商品的库存数量,处理完相关业务逻辑后库存数量加1操作
@RedissonLock(lockParamExpression = "#p0.productId")
@Transactional(rollbackOn = Exception.class)
public void doStock(UpdateStockReqBO request) {
StockPO stock = stockRepository.findByProductId(request.getProductId());
stock.setQuantity(stock.getQuantity() + 1);
stockRepository.save(stock);
}
初始数量为0,使用500个线程并发执行上面的业务方法,在不加分布式锁注解的情况下,得到的结果是82(每次可能不同);加了注解之后,即可得到正确的结果500
private static final Integer MAX_THREAD = 500;
private final StockService stockService;
public StockController(StockService stockService) {
this.stockService = stockService;
}
private CyclicBarrier cyclicBarrier = new CyclicBarrier(MAX_THREAD);
@GetMapping("do-stock")
public void doStock() {
for (int i = 0; i < MAX_THREAD; i++) {
new Thread(() -> {
try {
cyclicBarrier.await();
stockService.doStock(UpdateStockReqBO.builder().productId(1001L).build());
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}
).start();
}
}
RedisConfig
/**
* @author liujiazhong
*/
@Configuration
@EnableConfigurationProperties(RedisConfigProperties.class)
public class RedisConfig {
private final RedisConfigProperties properties;
public RedisConfig(RedisConfigProperties properties) {
this.properties = properties;
}
@Bean
public JedisConnectionFactory jedisConnectionFactory() {
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(properties.getMaxTotal());
poolConfig.setMaxIdle(properties.getMaxIdle());
poolConfig.setMaxWaitMillis(properties.getMaxWaitMillis());
poolConfig.setTimeBetweenEvictionRunsMillis(properties.getTimeBetweenEvictionRunsMillis());
poolConfig.setTestOnBorrow(properties.getTestOnBorrow());
poolConfig.setMinEvictableIdleTimeMillis(properties.getMinEvictableIdleTimeMillis());
RedisStandaloneConfiguration redisConfig = new RedisStandaloneConfiguration();
redisConfig.setHostName(properties.getHost());
redisConfig.setPort(properties.getPort());
redisConfig.setPassword(properties.getPassword());
redisConfig.setDatabase(properties.getDatabase());
JedisClientConfiguration clientConfig = JedisClientConfiguration.builder()
.usePooling()
.poolConfig(poolConfig)
.and().build();
return new JedisConnectionFactory(redisConfig, clientConfig);
}
@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(jedisConnectionFactory());
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer().setAddress("redis://" + properties.getHost() + ":" + properties.getPort());
return Redisson.create(config);
}
}
Gitee:https://gitee.com/liujiazhongg_admin/redisson-example