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

在這裏插入圖片描述

補充

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章