synchronized
就无法实现了,而基于数据库的乐观锁实现又可能会对数据库产生较大的压力使用分布式锁的流程一般如下:
思考:
如果需要使用分布式锁的场景有多处,那么就需要写多个类似的代码片段了,就会形成很多冗余的代码了,那怎么办呢?
解决方案:
我们可以使用 AOP技术 把这段逻辑抽象出来,这样就避免了重复代码,极大减少了工作量
我们希望这把分布式锁能帮我们达到这样的目标:
此处我们选择的方案就是:AOP+自定义注解+Redis锁
首先我们需要一个简单的SpringBoot项目环境,这里我写了一个基础Demo版本,地址如下:
https://gitee.com/colinWu_java/spring-boot-base.git
大家可以先下载下来,本文就是基于这份主干代码进行修改的
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>
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;
}
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();
}
}
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; //写锁
}
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);
}
}
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
数据库新建一张商品信息表:
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>
新建一个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");
}
}
}
测试场景是这样的:
目前商品数量只剩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,业务代码处理完毕,锁已释放
从上面结果我们梳理一下现象:
完全符合预期,测试成功
最后本案例代码已全部提交到gitee中了,地址如下:
https://gitee.com/colinWu_java/spring-boot-base.git
本文新增的代码在RedisDistributedLock分支中