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

文章目录

  • 1、背景
  • 2、目标
  • 3、方案
  • 4、实战编码
    • 4.1、环境准备
    • 4.2、pom依赖
    • 4.3、自定义注解
    • 4.4、切面处理类
    • 4.5、工具类
    • 4.6、配置类
    • 4.7、yml配置
    • 4.8、表相关
    • 4.9、使用
    • 4.10、测试
  • 总结

1、背景

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

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

AOP+自定义注解+Redis实现分布式锁_第1张图片

思考:

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

解决方案:

我们可以使用 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.hutoolgroupId>
    <artifactId>hutool-allartifactId>
    <version>5.7.8version>
dependency>


<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-aopartifactId>
dependency>


<dependency>
    <groupId>org.redissongroupId>
    <artifactId>redissonartifactId>
    <version>3.5.0version>
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;

/**
 * 

切面类

* 被 @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;

/**
 * 

Redisson工具类:加锁解锁

* * @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;

/**
 * 

Redisson配置类

* * @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;

/**
 * 

商品表对应实体类

* * @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;

/**
 * 

商品表的Mapper

* * @author 波波老师(微信 : javabobo0513) */
public interface GoodsMapper extends BaseMapper<Goods> { }

然后新建GoodsMapper.xml


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发请求:

AOP+自定义注解+Redis实现分布式锁_第2张图片

然后运行,结果如下:

AOP+自定义注解+Redis实现分布式锁_第3张图片

AOP+自定义注解+Redis实现分布式锁_第4张图片

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

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分支中

你可能感兴趣的:(面试宝典,Redis,redis,分布式,java,spring,boot)