文章目录
1、背景
- 分布式系统开发中常常用到分布式锁,比如防止多个用户同时购买同一个商品,传统的
synchronized
就无法实现了,而基于数据库的乐观锁实现又可能会对数据库产生较大的压力 - 而分布式锁相对较轻量,对性能影响也较小
- 目前主流的分布式锁都是基于Redis或者Zookeeper实现
使用分布式锁的流程一般如下:
思考:
如果需要使用分布式锁的场景有多处,那么就需要写多个类似的代码片段了,就会形成很多冗余的代码了,那怎么办呢?
解决方案:
我们可以使用 AOP技术 把这段逻辑抽象出来,这样就避免了重复代码,极大减少了工作量
2、目标
我们希望这把分布式锁能帮我们达到这样的目标:
- 对业务代码无侵入(或侵入性较小)
- 使用起来非常方便,最好是打一个注解就可以了,可插拔式的
- 对性能影响尽可能的小
- 要便于后期维护
3、方案
此处我们选择的方案就是:AOP+自定义注解+Redis锁
- 自定义一个注解,声明锁的相关参数,如:锁存Redis的key值、锁的有效期、提示信息、是否自动释放等等
- 使用Spring AOP的环绕通知增强被自定义注解修饰的方法,把加锁和释放锁的代码片段抽取到这个切面中,这样就公用了
- 那么需要用到分布式锁的接口,只需要打一个注解即可,这样才灵活优雅
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,业务代码处理完毕,锁已释放
从上面结果我们梳理一下现象:
- 首先可以看到确实有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
- 首先是http-nio-8001-exec-4获取到了锁,开始扣减库存,其次是:http-nio-8001-exec-5和http-nio-8001-exec-2
- 最后线程http-nio-8001-exec-3和http-nio-8001-exec-1虽然拿到了锁,但是库存已经不足了,所以没有抢到商品
完全符合预期,测试成功
总结
- 本文主要是介绍了分布式锁利用注解的方式处理,方便使用和扩展
- 具体使用了AOP+自定义注解+Redis实现了基于注解实现分布式锁的目的
- 希望对大家有所帮助
最后本案例代码已全部提交到gitee中了,地址如下:
https://gitee.com/colinWu_java/spring-boot-base.git
本文新增的代码在RedisDistributedLock分支中
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/116628.html