AOP+自定义注解+Redis实现分布式锁

导读:本篇文章讲解 AOP+自定义注解+Redis实现分布式锁,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

1、背景

  1. 分布式系统开发中常常用到分布式锁,比如防止多个用户同时购买同一个商品,传统的synchronized就无法实现了,而基于数据库的乐观锁实现又可能会对数据库产生较大的压力
  2. 而分布式锁相对较轻量,对性能影响也较小
  3. 目前主流的分布式锁都是基于Redis或者Zookeeper实现

使用分布式锁的流程一般如下:

在这里插入图片描述

思考:

如果需要使用分布式锁的场景有多处,那么就需要写多个类似的代码片段了,就会形成很多冗余的代码了,那怎么办呢?

解决方案:

我们可以使用 AOP技术 把这段逻辑抽象出来,这样就避免了重复代码,极大减少了工作量

2、目标

我们希望这把分布式锁能帮我们达到这样的目标:

  1. 对业务代码无侵入(或侵入性较小)
  2. 使用起来非常方便,最好是打一个注解就可以了,可插拔式的
  3. 对性能影响尽可能的小
  4. 要便于后期维护

3、方案

此处我们选择的方案就是:AOP+自定义注解+Redis锁

  1. 自定义一个注解,声明锁的相关参数,如:锁存Redis的key值、锁的有效期、提示信息、是否自动释放等等
  2. 使用Spring AOP的环绕通知增强被自定义注解修饰的方法,把加锁和释放锁的代码片段抽取到这个切面中,这样就公用了
  3. 那么需要用到分布式锁的接口,只需要打一个注解即可,这样才灵活优雅

4、实战编码

4.1、环境准备

首先我们需要一个简单的SpringBoot项目环境,这里我写了一个基础Demo版本,地址如下:

https://gitee.com/colinWu_java/spring-boot-base.git

大家可以先下载下来,本文就是基于这份主干代码进行修改的

4.2、pom依赖

pom.xml中需要新增以下依赖:

<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.7.8</version>
</dependency>

<!-- 必须 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

<!-- 必须 -->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.5.0</version>
</dependency>

4.3、自定义注解

package org.wujiangbo.annotation;

import org.wujiangbo.constants.LockType;
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;

/**
 * @desc 用于标记Redis锁的自定义注解
 * @author 波波老师(微信:javabobo0513)
 */
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RedisLock {

    /**
     * 防止redis的key发生冲突,所以会对key加上一些统一的前缀,例如:insertXxx, DeleteXxx
     */
    String lockName() default "redisson_distributed_lock:";

    /**
     * 锁自动释放时间(默认30秒)
     **/
    int leaseTime() default 30000;

    /**
     * 获取锁等待时间(默认3秒)
     **/
    int waitTime() default 3000;

    /**
     * 尝试获取锁的次数
     **/
    int tryNum() default 5;

    /**
     * 时间单位
     **/
    TimeUnit timeUnit() default TimeUnit.MILLISECONDS;

    /**
     * 锁类型
     **/
    LockType LockType() default LockType.REENTRANT_LOCK;
}

4.4、切面处理类

package org.wujiangbo.aspect;

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.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.wujiangbo.annotation.RedisLock;
import org.wujiangbo.utils.RedissonUtil;
import java.lang.reflect.Method;

/**
 * <p>切面类</p>
 * 被 @RedisLock 所注解的方法,会被 RedisLockAspect 进行切面管理
 *
 * @author 波波老师(微信 : javabobo0513)
 */
@Slf4j
@Aspect
@Component
public class RedisLockAspect {

    @Autowired
    private RedissonUtil redissonUtil;

    /**
     * 环绕通知
     */
    @Around(value = "@annotation(redisLock)", argNames = "joinPoint,redisLock")
    public Object around(ProceedingJoinPoint joinPoint, RedisLock redisLock) throws Throwable {
        log.info("线程{},进入切面", Thread.currentThread().getName());

        //获取锁的名称key
        String lockKey = getRedisName(joinPoint, redisLock.lockName());

        Object result = null;
        try {
            //尝试获取锁的次数
            int tryNum = redisLock.tryNum();
            // 尝试获取锁,等待5秒,自己获得锁后一直不解锁则在指定时间后自动解锁
            while (tryNum > 0) {
                /**
                 * 开始尝试获取锁了,返回true表示当前线程获取到了锁,返回false表示没有获取到锁
                 */
                boolean lock = redissonUtil.tryLock(redisLock.LockType(), lockKey, redisLock.timeUnit(), redisLock.waitTime(), redisLock.leaseTime());
                if (lock) {
                    log.info("线程:{},获取到了锁,开始处理业务", Thread.currentThread().getName());
                    //执行业务逻辑
                    result = joinPoint.proceed();
                    //代码运行到这,业务做完,需要释放锁了
                    redissonUtil.unlock(lockKey);  //释放锁
                    log.info("线程:{},业务代码处理完毕,锁已释放", Thread.currentThread().getName());
                    break;
                }
                log.info("XXXXX - 线程:{},没有获取到锁,开始自旋", Thread.currentThread().getName());
                /**
                 * 睡眠500毫秒,休息一小会,给其他拿到锁的线程一点时间去处理业务逻辑代码,再自旋
                 */
                Thread.sleep(500);
                tryNum --;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }

    /**
     * 获取Redis名称
     *
     * @param joinPoint 切点
     * @param lockName 锁名称
     * @return redisKey
     */
    private String getRedisName(ProceedingJoinPoint joinPoint, String lockName) {
        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method targetMethod = methodSignature.getMethod();
        return lockName + targetMethod.getName();
    }
}

4.5、工具类

RedissonUtil工具类:

package org.wujiangbo.utils;

import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.wujiangbo.constants.LockType;
import org.wujiangbo.exception.MyException;
import java.util.concurrent.TimeUnit;

/**
 * <p>Redisson工具类:加锁解锁</p>
 *
 * @author 波波老师(微信 : javabobo0513)
 */
@Component
public class RedissonUtil {

    // RedissonClient已经由配置类生成,这里自动装配即可
    @Autowired
    private RedissonClient redissonClient;

    /**
     * 锁住不设置超时时间(拿不到lock就不罢休,不然线程就一直block)
     * @param lockKey
     * @return org.redisson.api.RLock
     */
    public RLock lock(String lockKey) {
        RLock lock = redissonClient.getLock(lockKey);
        lock.lock();
        return lock;
    }

    /**
     * leaseTime为加锁时间,单位为秒
     * @param lockKey
     * @param leaseTime
     * @return org.redisson.api.RLock
     */
    public RLock lock(String lockKey, long leaseTime) {
        RLock lock = redissonClient.getLock(lockKey);
        lock.lock(leaseTime, TimeUnit.SECONDS);
        return null;
    }

    /**
     * timeout为加锁时间,时间单位由unit确定
     * @param lockKey
     * @param unit
     * @param timeout
     * @return org.redisson.api.RLock
     */
    public RLock lock(String lockKey, TimeUnit unit, long timeout) {
        RLock lock = redissonClient.getLock(lockKey);
        lock.lock(timeout, unit);
        return lock;
    }

    /**
     * 尝试获取锁
     * @param lockType 锁的类型
     * @param lockKey 锁的key
     * @param unit 锁的单位
     * @param waitTime 获取锁等待时间
     * @param leaseTime 锁自动释放时间
     * @return boolean
     */
    public boolean tryLock(LockType lockType, String lockKey, TimeUnit unit, long waitTime, long leaseTime) {
        RLock lock = null;
        switch (lockType){
            case REENTRANT_LOCK:
                lock=  redissonClient.getLock(lockKey);
                break;
            case FAIR_LOCK:
                lock=  redissonClient.getFairLock(lockKey);
                break;
            case READ_LOCK:
                lock=  redissonClient.getReadWriteLock(lockKey).readLock();
                break;
            case WRITE_LOCK:
                lock=  redissonClient.getReadWriteLock(lockKey).writeLock();
                break;
            default:
                throw new MyException("do not support lock type:" + lockType);
        }
        try {
            return lock.tryLock(waitTime, leaseTime, unit);
        } catch (InterruptedException e) {
            return false;
        }
    }

    /**
     * 通过lockKey解锁
     * @param lockKey
     * @return void
     */
    public void unlock(String lockKey) {
        RLock lock = redissonClient.getLock(lockKey);
        lock.unlock();
    }

    /**
     * 直接通过锁解锁
     * @param lock
     * @return void
     */
    public void unlock(RLock lock) {
        lock.unlock();
    }
}

LockType枚举类:

package org.wujiangbo.constants;

/**
 * @desc 锁类型
 * @author 波波老师(微信:javabobo0513)
 */
public enum LockType {
    REENTRANT_LOCK, //可重入锁
    FAIR_LOCK, //公平锁
    READ_LOCK, //读锁
    WRITE_LOCK; //写锁
}

4.6、配置类

package org.wujiangbo.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;

/**
 * <p>Redisson配置类</p>
 *
 * @author 波波老师(微信 : javabobo0513)
 */
@Configuration
public class RedissonConfig {

    @Value("${redisson.address}")
    private String addressUrl;

    @Value("${redisson.password}")
    private String password;

    /**
     * 将 RedissonClient 对象注入Spring容器中
     * @return
     * @throws Exception
     */
    @Bean
    public RedissonClient getRedisson() throws Exception{
        Config config = new Config();
        config.useSingleServer().setAddress(addressUrl);
        config.useSingleServer().setPassword(password);
        return Redisson.create(config);
    }
}

4.7、yml配置

server:
  port: 8001
spring:
  #配置数据库链接信息
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&rewriteBatchedStatements=true
    username: root
    password: 123456
    driver-class-name: com.mysql.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource
  application:
    name: springboot #服务名

#MyBatis-Plus相关配置
mybatis-plus:
  #指定Mapper.xml路径,如果与Mapper路径相同的话,可省略
  mapper-locations: classpath:org/wujiangbo/mapper/*Mapper.xml
  configuration:
    map-underscore-to-camel-case: true #开启驼峰大小写自动转换
#    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #开启控制台sql输出

#redisson配置
redisson:
  address: redis://127.0.0.1:6379
  password: 123456

4.8、表相关

数据库新建一张商品信息表:

DROP TABLE IF EXISTS `t_goods`;
CREATE TABLE `t_goods`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '商品ID',
  `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '商品名称',
  `count` int(11) NULL DEFAULT NULL COMMENT '库存剩余数量',
  `version` bigint(20) NULL DEFAULT 0 COMMENT '版本',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '商品表' ROW_FORMAT = Dynamic;


INSERT INTO `t_goods` VALUES (1, 'IPhoneX', 3, 1);

里面只有一条数据,有一个商品【IPhoneX】,库存数量只有3个了

然后新建实体类对象:

package org.wujiangbo.domain;

import com.baomidou.mybatisplus.annotations.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

/**
 * <p>商品表对应实体类</p>
 *
 * @author 波波老师(微信 : javabobo0513)
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
@TableName("t_goods")
public class Goods {

    private Long id;
    private String name;
    private Integer count;
    private Long version;
}

然后新建GoodsMapper类:

package org.wujiangbo.mapper;

import com.baomidou.mybatisplus.mapper.BaseMapper;
import org.wujiangbo.domain.Goods;

/**
 * <p>商品表的Mapper</p>
 *
 * @author 波波老师(微信 : javabobo0513)
 */
public interface GoodsMapper extends BaseMapper<Goods> {
}

然后新建GoodsMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.wujiangbo.mapper.GoodsMapper">

</mapper>

4.9、使用

新建一个OrderController,写一个下单接口做测试:

package org.wujiangbo.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.wujiangbo.annotation.RedisLock;
import org.wujiangbo.domain.Goods;
import org.wujiangbo.mapper.GoodsMapper;
import org.wujiangbo.result.JSONResult;

/**
 * @desc 订单接口
 * @author 波波老师(微信:javabobo0513)
 */
@RestController
@RequestMapping("/order")
@Slf4j
public class OrderController {

    @Autowired
    private GoodsMapper goodsMapper;

    //测试Redis分布式锁
    @PostMapping("/insertOrder")
    @RedisLock()
    public JSONResult insertOrder(){
        log.info("线程{},开始下单...", Thread.currentThread().getName());
        /**
         * 开始做业务
         * 现在我们假设每个请求过来都只买一个
         */
        Goods goods = goodsMapper.selectById(1);
        if(goods.getCount() > 0){
            //如果还有库存的话,就进行库存扣减操作
            goods.setCount(goods.getCount() - 1);
            goodsMapper.updateById(goods);
            log.info("线程{},下单成功", Thread.currentThread().getName());
            return JSONResult.success("insertOrder success");
        }
        else{
            log.info("线程{},下单失败,库存不足", Thread.currentThread().getName());
            return JSONResult.error("insertOrder fail");
        }
    }
}

4.10、测试

测试场景是这样的:

目前商品数量只剩3个了,但是现在有5个人同时发请求要买这个商品,假设每个人每次只能买一个,那结果必然是有三个人买到了,有两个人是买不到商品的(有点类似于秒杀场景,或者抢湖北消费券的场景)

我这里会使用JMeter压测工具进行测试,模拟1秒钟发5个请求

好,现在我们启动项目,然后用JMeter发请求:

在这里插入图片描述

然后运行,结果如下:

在这里插入图片描述

在这里插入图片描述

然后我们再看下控制台核心打印语句:

INFO  2022-10-21 15:51:28.851 [http-nio-8001-exec-1] org.wujiangbo.aspect.RedisLockAspect.around:34:线程http-nio-8001-exec-1,进入切面
INFO  2022-10-21 15:51:28.851 [http-nio-8001-exec-5] org.wujiangbo.aspect.RedisLockAspect.around:34:线程http-nio-8001-exec-5,进入切面
INFO  2022-10-21 15:51:28.851 [http-nio-8001-exec-3] org.wujiangbo.aspect.RedisLockAspect.around:34:线程http-nio-8001-exec-3,进入切面
INFO  2022-10-21 15:51:28.851 [http-nio-8001-exec-2] org.wujiangbo.aspect.RedisLockAspect.around:34:线程http-nio-8001-exec-2,进入切面
INFO  2022-10-21 15:51:28.851 [http-nio-8001-exec-4] org.wujiangbo.aspect.RedisLockAspect.around:34:线程http-nio-8001-exec-4,进入切面
INFO  2022-10-21 15:51:28.864 [http-nio-8001-exec-4] org.wujiangbo.aspect.RedisLockAspect.around:50:线程:http-nio-8001-exec-4,获取到了锁,开始处理业务
INFO  2022-10-21 15:51:28.868 [http-nio-8001-exec-4] org.wujiangbo.controller.OrderController.insertOrder:29:线程http-nio-8001-exec-4,开始下单...
INFO  2022-10-21 15:51:28.920 [http-nio-8001-exec-4] org.wujiangbo.controller.OrderController.insertOrder:39:线程http-nio-8001-exec-4,下单成功
INFO  2022-10-21 15:51:28.922 [http-nio-8001-exec-4] org.wujiangbo.aspect.RedisLockAspect.around:55:线程:http-nio-8001-exec-4,业务代码处理完毕,锁已释放
INFO  2022-10-21 15:51:28.925 [http-nio-8001-exec-5] org.wujiangbo.aspect.RedisLockAspect.around:50:线程:http-nio-8001-exec-5,获取到了锁,开始处理业务
INFO  2022-10-21 15:51:28.925 [http-nio-8001-exec-5] org.wujiangbo.controller.OrderController.insertOrder:29:线程http-nio-8001-exec-5,开始下单...
INFO  2022-10-21 15:51:28.931 [http-nio-8001-exec-5] org.wujiangbo.controller.OrderController.insertOrder:39:线程http-nio-8001-exec-5,下单成功
INFO  2022-10-21 15:51:28.946 [http-nio-8001-exec-5] org.wujiangbo.aspect.RedisLockAspect.around:55:线程:http-nio-8001-exec-5,业务代码处理完毕,锁已释放
INFO  2022-10-21 15:51:28.948 [http-nio-8001-exec-2] org.wujiangbo.aspect.RedisLockAspect.around:50:线程:http-nio-8001-exec-2,获取到了锁,开始处理业务
INFO  2022-10-21 15:51:28.949 [http-nio-8001-exec-2] org.wujiangbo.controller.OrderController.insertOrder:29:线程http-nio-8001-exec-2,开始下单...
INFO  2022-10-21 15:51:28.954 [http-nio-8001-exec-2] org.wujiangbo.controller.OrderController.insertOrder:39:线程http-nio-8001-exec-2,下单成功
INFO  2022-10-21 15:51:28.955 [http-nio-8001-exec-2] org.wujiangbo.aspect.RedisLockAspect.around:55:线程:http-nio-8001-exec-2,业务代码处理完毕,锁已释放
INFO  2022-10-21 15:51:28.956 [http-nio-8001-exec-3] org.wujiangbo.aspect.RedisLockAspect.around:50:线程:http-nio-8001-exec-3,获取到了锁,开始处理业务
INFO  2022-10-21 15:51:28.957 [http-nio-8001-exec-3] org.wujiangbo.controller.OrderController.insertOrder:29:线程http-nio-8001-exec-3,开始下单...
INFO  2022-10-21 15:51:28.958 [http-nio-8001-exec-3] org.wujiangbo.controller.OrderController.insertOrder:43:线程http-nio-8001-exec-3,下单失败,库存不足
INFO  2022-10-21 15:51:28.959 [http-nio-8001-exec-3] org.wujiangbo.aspect.RedisLockAspect.around:55:线程:http-nio-8001-exec-3,业务代码处理完毕,锁已释放
INFO  2022-10-21 15:51:28.964 [http-nio-8001-exec-1] org.wujiangbo.aspect.RedisLockAspect.around:50:线程:http-nio-8001-exec-1,获取到了锁,开始处理业务
INFO  2022-10-21 15:51:28.967 [http-nio-8001-exec-1] org.wujiangbo.controller.OrderController.insertOrder:29:线程http-nio-8001-exec-1,开始下单...
INFO  2022-10-21 15:51:28.970 [http-nio-8001-exec-1] org.wujiangbo.controller.OrderController.insertOrder:43:线程http-nio-8001-exec-1,下单失败,库存不足
INFO  2022-10-21 15:51:28.971 [http-nio-8001-exec-1] org.wujiangbo.aspect.RedisLockAspect.around:55:线程:http-nio-8001-exec-1,业务代码处理完毕,锁已释放

从上面结果我们梳理一下现象:

  1. 首先可以看到确实有5个线程进入切面类了,分别是:http-nio-8001-exec-1、http-nio-8001-exec-2、http-nio-8001-exec-3、http-nio-8001-exec-4、http-nio-8001-exec-5
  2. 首先是http-nio-8001-exec-4获取到了锁,开始扣减库存,其次是:http-nio-8001-exec-5和http-nio-8001-exec-2
  3. 最后线程http-nio-8001-exec-3和http-nio-8001-exec-1虽然拿到了锁,但是库存已经不足了,所以没有抢到商品

完全符合预期,测试成功

总结

  1. 本文主要是介绍了分布式锁利用注解的方式处理,方便使用和扩展
  2. 具体使用了AOP+自定义注解+Redis实现了基于注解实现分布式锁的目的
  3. 希望对大家有所帮助

最后本案例代码已全部提交到gitee中了,地址如下:

https://gitee.com/colinWu_java/spring-boot-base.git

本文新增的代码在RedisDistributedLock分支中

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/116628.html

(0)
seven_的头像seven_bm

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!