1. 简介
在分布式业务开发中,很多场景都需要添加分布式锁。在具体实践过程中,研发人员都需要自行实现,导致实现方式不统一,代码风格迥异,难以维护。
在Mybatis-Plus
生态中,Lock4j
提供了支持redission
、redisTemplate
、zookeeper
的分布式锁组件,简单易用,功能强大,扩展性强。
Gitee地址:https://gitee.com/baomidou/lock4j
2. 简单示例
以redisTemplate作为分布式锁底层实现编写示例代码。其他两种方案可参考官网文档实现。
- 创建项目
- 修改pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.c3stones</groupId>
<artifactId>spring-boot-lock4j-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.7</version>
</parent>
<dependencies>
<!--若使用redisTemplate作为分布式锁底层-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>lock4j-redis-template-spring-boot-starter</artifactId>
<version>2.2.4</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
- 配置redis信息
spring:
redis:
host: 127.0.0.1
port: 6379
password: 123456
# Lock4j配置
#lock4j:
# acquire-timeout: 3000 #等待时长。默认值3s,可不设置
# expire: 30000 #过期时间,防止死锁。默认值30s,可不设置
# primary-executor: com.baomidou.lock.executor.RedisTemplateLockExecutor #默认redisson>redisTemplate>zookeeper,可不设置
# lock-key-prefix: lock4j #锁key前缀。默认值lock4j,可不设置
- 统一响应类
发生异常时,由全局异常处理类将异常信息格式化为统一响应类输出。
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import lombok.experimental.Accessors;
import java.io.Serializable;
/**
* 响应工具类
*
* @param <T>
* @author CL
*/
@Data
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class R<T> implements Serializable {
private static final long serialVersionUID = 1L;
private Boolean code;
private String msg;
private T data;
public static <T> R<T> ok() {
return restResult(null, Boolean.TRUE, null);
}
public static <T> R<T> ok(T data) {
return restResult(data, Boolean.TRUE, null);
}
public static <T> R<T> ok(T data, String msg) {
return restResult(data, Boolean.TRUE, msg);
}
public static <T> R<T> failed() {
return restResult(null, Boolean.FALSE, null);
}
public static <T> R<T> failed(String msg) {
return restResult(null, Boolean.FALSE, msg);
}
public static <T> R<T> failed(T data) {
return restResult(data, Boolean.FALSE, null);
}
public static <T> R<T> failed(T data, String msg) {
return restResult(data, Boolean.FALSE, msg);
}
private static <T> R<T> restResult(T data, Boolean code, String msg) {
R<T> apiResult = new R<>();
apiResult.setCode(code);
apiResult.setData(data);
apiResult.setMsg(msg);
return apiResult;
}
}
- 全局异常处理类
/**
* 全局异常处理
*
* @author CL
*/
@Log4j2
@RestControllerAdvice
public class WebExceptionAdvice {
/**
* 异常处理
*
* @param ex 异常
* @return {@link R}
*/
@ExceptionHandler(value = Exception.class)
public R<?> errorHandler(Exception ex) {
log.error(ex);
return R.failed(ex.getMessage());
}
}
- 自定义分布式锁失败策略(可选)
import com.baomidou.lock.LockFailureStrategy;
import com.baomidou.lock.exception.LockFailureException;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
/**
* 自定义获取锁失败策略
*
* @author CL
*/
@Component
public class MyLockFailureStrategy implements LockFailureStrategy {
@Override
public void onLockFailure(String key, Method method, Object[] arguments) {
throw new LockFailureException("处理中,请稍后");
}
}
- 五种测试方法
import com.baomidou.lock.LockInfo;
import com.baomidou.lock.LockTemplate;
import com.baomidou.lock.annotation.Lock4j;
import com.baomidou.lock.executor.RedisTemplateLockExecutor;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
/**
* 测试Service
*
* @author CL
*/
@Service
@RequiredArgsConstructor
public class TestService {
/**
* 测试无锁
*
* @param processTime 业务处理时间
* @return {@link Long}
*/
public Long test(long processTime) {
try {
Thread.sleep(processTime * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return processTime;
}
/**
* 测试默认分布式锁
*
* @param processTime 业务处理时间
* @return {@link Long}
*/
@Lock4j
public Long test1(long processTime) {
return test(processTime);
}
/**
* 测试自定义配置分布式锁
* <p>
* keys: 锁名称,全局唯一<br/>
* expire: 过期时间,防止死锁<br/>
* acquireTimeout: 等待时间,获取不到则执行失败策略<br/>
* </p>
*
* @param processTime 业务处理时间
* @return {@link Long}
*/
@Lock4j(keys = {"#processTime"}, expire = 60000, acquireTimeout = 1000)
public Long test2(long processTime) {
return test(processTime);
}
private final LockTemplate lockTemplate;
/**
* 测试手动获取分布式锁
*
* @param processTime 业务处理时间
* @return {@link Long}
*/
public Long test3(long processTime) {
// 获取锁
final LockInfo lockInfo = lockTemplate.lock("custom:" + processTime, 30000L, 2000L, RedisTemplateLockExecutor.class);
if (null == lockInfo) {
throw new RuntimeException("处理中,请稍后");
}
// 获取锁成功
try {
test(processTime);
} finally {
//释放锁
lockTemplate.releaseLock(lockInfo);
}
return processTime;
}
/**
* 测试5秒内只能访问1次
*
* @param processTime 业务处理时间
* @return {@link Long}
*/
@Lock4j(keys = {"#processTime"}, acquireTimeout = 0, expire = 5000, autoRelease = false)
public Long test4(long processTime) {
return processTime;
}
}
- 测试接口
import com.c3stones.common.R;
import com.c3stones.service.TestService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 测试Controller
*
* @author CL
*/
@RestController
@RequestMapping()
@RequiredArgsConstructor
public class TestController {
private final TestService testService;
/**
* 测试无锁
*
* @param processTime 业务处理时间
* @return {@link R}
*/
@RequestMapping("/test")
public R<Long> test(long processTime) {
return R.ok(testService.test(processTime));
}
/**
* 测试默认分布式锁
*
* @param processTime 业务处理时间
* @return {@link R}
*/
@RequestMapping("/test1")
public R<Long> test1(long processTime) {
return R.ok(testService.test1(processTime));
}
/**
* 测试自定义配置分布式锁
*
* @param processTime 业务处理时间
* @return {@link R}
*/
@RequestMapping("/test2")
public R<Long> test2(long processTime) {
return R.ok(testService.test2(processTime));
}
/**
* 测试手动获取分布式锁
*
* @param processTime 业务处理时间
* @return {@link R}
*/
@RequestMapping("/test3")
public R<Long> test3(long processTime) {
return R.ok(testService.test3(processTime));
}
/**
* 测试5秒内只能访问1次
*
* @param processTime 业务处理时间
* @return {@link R}
*/
@RequestMapping("/test4")
public R<Long> test4(long processTime) {
return R.ok(testService.test4(processTime));
}
}
- 启动类
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 启动类
*
* @author CL
*/
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
3. 测试
测试方案:手动快速发送两次相同的请求,假设业务处理时长为5秒,查看响应结果是否满足分布式锁。
- 测试无锁
相同条件:第一次请求和第二次请求都不会加锁,等待业务处理完成返回结果。
- 测试默认分布式锁
相同条件:第一次请求会加锁,等待业务处理完成返回结果并释放锁;第二次请求默认等待3秒获取锁,若获取锁失败则执行失败策略抛出异常。
- 测试自定义配置分布式锁
相同条件:第一次请求会加锁,等待业务处理完成返回结果并释放锁;第二次请求自定义等待1秒获取锁,若获取锁失败则执行失败策略抛出异常。
- 测试手动获取分布式锁
相同条件:第一次请求会加锁,等待业务处理完成返回结果并释放锁;第二次请求自定义等待2秒获取锁,若获取锁失败则执行失败策略抛出异常。
- 测试5秒内只能访问1次
相同条件:第一次请求会加锁,等待业务处理完成返回结果但不释放锁;第二次请求直接获取锁,若获取锁失败则执行失败策略抛出异常,若获取成功执行业务并加锁。