版本
- SpringBoot:2.2.6.RELEASE
- Redisson:3.12.5
依賴
<!--核心依賴-->
<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);
}
}