使用Spring AOP基于Redisson快速实现分布式锁

版本

  • 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();
    }
}

使用Spring AOP基于Redisson快速实现分布式锁_第1张图片

补充

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

你可能感兴趣的:(Spring,Boot)