目录
一、前言
在日常使用springboot框架进行微服务开发过程中,遇到需要控制并发造成的问题时,比较常用的做法是使用分布式锁进行控制,基于分布式锁的实现,到目前为止也有很多开源实现,使用比较多的像基于redis的分布式锁,基于zookeeper的分布式锁,本文再来介绍另一种比较高效的分布式锁实现,即Lock4j ,将通过案例演示下如何使用lock4j做分布式锁。
二、Lock4j 概述
2.1 Lock4j 介绍
2.1.1 Lock4j 是什么
Lock4j 是一个基于 Spring Boot 的分布式锁框架,旨在简化分布式系统中锁的实现和管理。项目入口:lock4j: 基于Spring AOP 的声明式和编程式分布式锁,支持RedisTemplate、Redisson、Zookeeper
2.1.2 Lock4j 主要特征
Lock4j是一个轻量级的分布式锁框架,它支持多种锁实现,包括Redis、Zookeeper等,并通过Spring AOP进行集成,使得开发者可以轻松地在Spring应用中使用分布式锁。它的设计目标是简单易用,同时提供高性能和高可靠性。具体来说,包括下面的主要特征:
注解驱动:通过简单的注解即可实现方法级别的锁控制
多种锁实现:支持多种底层锁实现方式
可扩展:支持自定义锁的实现
与Spring生态集成:无缝集成Spring框架
2.1.3 Lock4j 技术特点
Lock4j 作为一款上手简单,使用高效的分布式缓存技术框架,具备如下特点和优势:
使用高效:
Lock4j采用了高效的锁机制,能够在高并发场景下保持稳定的性能。其底层实现充分利用了Redis和Zookeeper等存储系统的特性,确保锁操作的快速响应。
简单易用:
通过简单的API和Spring AOP集成,开发者可以快速上手并实现分布式锁。Lock4j提供了详细的文档和示例代码,帮助开发者快速理解和使用。
锁支持类型丰富:
支持多种底层存储,满足不同应用场景的需求。无论是高性能的Redis,还是强一致性的Zookeeper,Lock4j都能提供相应的支持。
提供监控和日志:
提供丰富的监控和日志功能,帮助开发者了解锁的使用情况和性能表现。通过监控界面,开发者可以实时查看锁的状态和性能指标。
2.2 Lock4j 支持的锁类型
Lock4j支持多种类型的锁,通过底层存储(如Redis或Zookeeper)来实现分布式锁。支持的锁类型包括:
Redis锁:基于Redis实现的分布式锁
Zookeeper锁:基于Zookeeper实现的分布式锁
数据库锁:基于数据库实现的锁
内存锁:本地JVM锁(适用于单机环境)
2.3 Lock4j 工作原理
什么是锁?锁是一种同步机制,用于控制多线程对共享资源的访问。在分布式系统中,锁的实现更加复杂,因为需要在多个节点之间进行协调。Lock4j 作为分布式锁框架,其核心原理是通过协调多个分布式节点对共享资源的访问,确保在分布式环境下同一时间只有一个节点能够执行受保护的代码块。Lock4j通过底层存储(如Redis、Zookeeper等)来实现分布式锁。不同的存储组件在具体的实现上稍有差别,但是其核心原理是类似的,下面是Lock4j 的基本工作原理:
获取锁:
当一个节点需要访问共享资源时,它会向底层存储发送请求以获取锁。请求中包含锁的唯一标识和节点信息。
锁的持有:
如果锁可用,节点将持有该锁,并可以安全地访问共享资源。持有锁的节点需要定期向存储系统发送心跳信号,以保持锁的有效性。
释放锁:
访问完成后,节点会释放锁,使得其他节点可以获取锁并访问资源。释放锁时,节点需要确保锁的状态已更新,以避免其他节点误认为锁仍然被持有。
2.4 Lock4j 应用场景
Lock4j 作为分布式锁框架,适用于各种需要协调分布式系统资源访问的场景。下面列举了几种常用的应用场景:
防止重复提交/重复请求处理
典型场景
用户快速多次点击提交按钮
消息队列消费者重复消费同一条消息
定时任务重复执行
分布式锁并发控制
典型场景
全局配置更新
账户余额变更
库存扣减(防止超卖)
定时任务防重复执行
典型场景
分布式环境下多个实例的定时任务
长时间执行的批处理任务
分布式缓存
典型场景
缓存击穿保护
缓存一致性维护
关键业务流程串行化处理
典型场景
支付订单处理
文件导入导出
数据迁移任务
分布式文件系统
在分布式文件系统中,多个节点可能会同时访问和修改文件。
通过使用Lock4j,可确保文件的一致性,避免数据损坏。例如,在云存储系统中,多个节点可能会同时上传和下载文件,通过分布式锁可以确保文件数据的完整性。
三、springboot 整合 lock4j
3.1 前置准备
3.1. 1 导入依赖
创建一个springboot工程,导入下面的依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.4</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
</dependency>
<!-- 使用lock4j实现分布式锁 https://gitee.com/baomidou/lock4j/-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>lock4j-core</artifactId>
<version>2.2.5</version>
</dependency>
<!--添加开源分布式锁Lock4j-->
<!--若使用redisTemplate作为分布式锁底层,则需要引入-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>lock4j-redis-template-spring-boot-starter</artifactId>
<version>2.2.5</version>
</dependency>
</dependencies>
3.2 基于Redis实现分布式锁
基于Redis的实现是很常用的整合使用方式,当使用 Redis 作为底层存储时,Lock4j 主要依赖 Redis 的以下特性:
SETNX 命令(或 SET 命令的 NX 选项):原子性地设置键值,只有键不存在时才会设置成功
过期时间:避免死锁
Lua 脚本:保证解锁操作的原子性
下面看具体的整合和使用过程。
3.2.1 添加配置文件信息
在工程的配置文件中添加下面的信息
需要注意的是,配置文件中设置的lock4j的几个核心参数信息是全局生效的,比如acquire-timeout,这里全局设置的是3秒,但如果你在注解中再次配置了,则会覆盖配置文件中的这个值;
server:
port: 8082
lock4j:
type: redis
acquire-timeout: 3000 # 获取锁超时时间(毫秒)
expire: 30000 # 锁过期时间(毫秒)
retry-interval: 100 # 获取锁失败重试间隔(毫秒)
#redis的链接配置信息
redis:
host: localhost
database: 1
port: 6379
3.2.2 添加测试接口
在工程中增加如下接口,方便测试
package com.congge.web;
import com.baomidou.lock.annotation.Lock4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class LockController {
//localhost:8082/testLock
@Lock4j(keys = {"'key'"}, acquireTimeout = 1000, expire = 10000)
@GetMapping("/testLock")
public Object testLock(){
long threadId = Thread.currentThread().getId();
System.out.println(threadId + " : 获取到了锁,准备执行业务逻辑");
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return "testLock";
}
}
在上面这段代码中,在接口上面增加了一个 @Lock4j的注解,使用该注解即可实现分布式锁的功能,并且设置了注解中其他的参数,其核心参数包括:
key:锁的键,支持SpEL表达式
expire:锁的过期时间(毫秒)
timeout:获取锁的超时时间(毫秒)
retry:获取锁失败后的重试间隔(毫秒)
3.2.3 效果测试
在postman中模拟一下多线程的并发测试
然后通过控制台的结果输出不难看出,多个线程请求过来的时候,由于分布式锁的存在,所以未获取到锁的请求将会进行排队,等待前面的线程释放锁,拿到锁之后才能执行
3.3 基于Redission 实现
使用Redission 的实现方式与Redis的实现差不多,首先需要导入下面的依赖,配置文件信息和接口代码不用动,然后启动工程后再次测试,可以得到相同的效果。
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>lock4j-redisson-spring-boot-starter</artifactId>
<version>2.2.5</version>
</dependency>
3.4 基于Zookeeper实现
基于 Zookeeper 的实现主要是利用了zk的临时顺序节点来实现,还记得在使用zk实现分布式锁的机制吗,主要流程如下:
在指定路径下创建临时顺序节点
判断当前节点是否为最小序号节点
如果是则获取锁,否则监听前一个节点的删除事件
下面来看如何在代码中集成和使用。
3.4.1 启动zk服务
为了在代码中集成并使用zookeeper,需要本地或服务器启动一个zookeeper服务,这里直接在本机启动
3.4.2 导入下面的依赖
在pom文件中导入下面的依赖
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>lock4j-zookeeper-spring-boot-starter</artifactId>
<version>2.2.5</version>
</dependency>
3.4.3 添加配置信息
在配置文件中添加下面的配置信息
#使用redis作为分布式锁
spring:
coordinate:
zookeeper:
zkServers: 127.0.0.1:2181
3.4.4 添加测试接口
增加一个测试接口方便测试看效果
//localhost:8082/testZkLock
@Lock4j(keys = {"'key'"}, acquireTimeout = 1000, expire = 10000, executor = ZookeeperLockExecutor.class)
@GetMapping("/testZkLock")
public Object testZkLock(){
long threadId = Thread.currentThread().getId();
System.out.println(threadId + " : 获取到了zk的锁,准备执行业务逻辑");
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return "testLock";
}
3.4.5 模拟并发测试
在postman中,使用并发调用接口的方式进行测试
通过控制台输出效果可以看到,有了分布式锁的控制,可以确保请求的有序执行
四、Lock4j 功能扩展
如果默认的配置还不能满足实际的需求场景时,还可以使用lock4j提供的一些扩展点做补充,下面介绍几个点。
4.1 自定义执行器作用
lock4j 的自定义执行器主要用于扩展和定制分布式锁的实现方式
4.1.1 支持不同的分布式锁实现
作用:允许集成各种分布式锁技术,而不仅限于框架默认提供的几种。
典型场景:
当项目使用非主流分布式协调服务时(如非Redis、Zookeeper等)
需要接入公司自研的分布式锁服务
使用云服务商特有的分布式锁服务(如AWS DynamoDB Lock Client)
4.1.2 定制锁的获取和释放逻辑
作用:完全控制锁的获取和释放过程,实现特殊业务需求。
典型场景:
需要实现特定等待策略(如指数退避)
需要添加额外的锁校验逻辑(如业务状态检查)
需要记录详细的锁竞争指标和日志
4.1.3 适配特殊业务需求
作用:解决标准分布式锁无法满足的特殊业务场景。
典型场景:
需要实现租约机制(lease-based locking)
需要支持不同级别的锁(如读锁/写锁)
需要实现锁的自动续期功能
4.1.4 性能优化
作用:针对特定环境优化锁的性能表现。
典型场景:
针对高并发场景优化锁实现
减少网络往返次数
实现本地缓存加速
4.1.5 增强可靠性
作用:提供更健壮的锁机制,防止极端情况下的问题。
典型场景:
处理时钟漂移问题
实现更安全的锁释放机制
防止锁过期但业务未完成的情况
4.1.6 统一锁管理
作用:在异构环境中提供一致的锁管理接口。
典型场景:
混合使用多种锁实现(如部分用Redis,部分用数据库)
需要统一的监控和管理界面
实现锁的降级策略
4.2 自定义执行器使用
接下来通过两个案例演示下如何使用自定义执行器
4.2.1 基于zk的自定义执行器
添加一个自定义类,继承抽象类AbstractLockExecutor并重写内部的几个核心方法,参考下面的代码
package com.congge.config;
import com.baomidou.lock.executor.AbstractLockExecutor;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.imps.CuratorFrameworkState;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
@Slf4j
@RequiredArgsConstructor
/**
* 基于zk的分布式锁执行器 , 使用CuratorFramework作为ZooKeeper客户端,实现锁的获取和释放。
*/
@Component
public class MyZookeeperLockExecutor extends AbstractLockExecutor<InterProcessMutex> {
private final CuratorFramework curatorFramework;
/**
* 尝试获取分布式锁
*
* @param lockKey 锁的关键字,用于在ZooKeeper中创建锁节点的路径。
* @param lockValue 锁的值,可以用于进一步标识锁。
* @param expire 锁的过期时间,未使用。
* @param acquireTimeout 获取锁的超时时间。
* @return 如果成功获取锁,返回InterProcessMutex实例;否则返回null。
*/
@Override
public InterProcessMutex acquire(String lockKey, String lockValue, long expire, long acquireTimeout) {
System.out.println("进入了zk的自定义锁执行器");
// 检查CuratorFramework实例是否已启动
if (!CuratorFrameworkState.STARTED.equals(curatorFramework.getState())) {
log.warn("instance must be started before calling this method");
return null;
}
// 构建锁节点的路径
String nodePath = "/curator/lock4j/%s";
try {
// 创建InterProcessMutex实例,并尝试获取锁
InterProcessMutex mutex = new InterProcessMutex(curatorFramework, String.format(nodePath, lockKey));
final boolean locked = mutex.acquire(acquireTimeout, TimeUnit.MILLISECONDS);
// 根据获取锁的结果,返回相应的锁实例或null
return obtainLockInstance(locked, mutex);
} catch (Exception e) {
// 获取锁过程中发生异常,返回null
return null;
}
}
/**
* 释放分布式锁。
*
* @param key 锁的关键字,与获取锁时使用的key相同。
* @param value 锁的值,与获取锁时使用的value相同。
* @param lockInstance 锁实例,用于释放锁。
* @return 如果成功释放锁,返回true;否则返回false。
*/
@Override
public boolean releaseLock(String key, String value, InterProcessMutex lockInstance) {
System.out.println("进入了zk的自定义锁执行器释放锁");
try {
// 直接释放锁
lockInstance.release();
} catch (Exception e) {
// 释放锁过程中发生异常,记录日志并返回false
log.warn("zookeeper lock release error", e);
return false;
}
// 成功释放锁,返回true
return true;
}
}
添加如下测试接口,在自定义注解中指定executor 为自定义的类即可
//localhost:8082/testZkLock
//@Lock4j(keys = {"'key'"}, acquireTimeout = 1000, expire = 10000, executor = ZookeeperLockExecutor.class)
@Lock4j(keys = {"#key"}, acquireTimeout = 1000, expire = 10000, executor = MyZookeeperLockExecutor.class)
@GetMapping("/testZkLock")
public Object testZkLock(){
long threadId = Thread.currentThread().getId();
System.out.println(threadId + " : 获取到了zk的锁,准备执行业务逻辑");
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return "testLock";
}
启动工程后,使用接口工具模拟并发测试
通过控制台输出日志可以看到获取锁的逻辑走到了自定义的zk执行器里面了
4.2.2 基于Redis的自定义执行器
同样,如果要使用基于Redis的自定义执行器,也需要首先自定义一个类继承AbstractLockExecutor抽象类,参考下面的代码,结合代码理解
package com.congge.config;
import com.baomidou.lock.executor.AbstractLockExecutor;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Component;
import java.util.Collections;
/**
* Redis模板锁执行器,实现基于Redis的分布式锁,使用StringRedisTemplate和Lua脚本实现锁的获取和释放,提高锁操作的原子性
*/
@Slf4j
@RequiredArgsConstructor
@Component
public class MyRedisTemplateLockExecutor extends AbstractLockExecutor<String> {
/**
* 获取锁的Lua脚本。
* 该脚本用于尝试以NX选项设置键值对,实现锁的获取。
*/
private static final RedisScript<String> SCRIPT_LOCK = new DefaultRedisScript<>("return redis.call('set',KEYS[1]," +
"ARGV[1],'NX','PX',ARGV[2])", String.class);
/**
* 释放锁的Lua脚本
* 首先验证锁是否由当前持有者释放,如果是,则删除锁
*/
private static final RedisScript<String> SCRIPT_UNLOCK = new DefaultRedisScript<>(
"if redis.call('get',KEYS[1]) " +
"== ARGV[1] then return tostring(redis.call('del', KEYS[1])==1) else return 'false' end",
String.class);
/**
* 表示锁获取成功的固定字符串。
*/
private static final String LOCK_SUCCESS = "OK";
/**
* Redis字符串模板,用于执行Redis操作。
*/
private final StringRedisTemplate redisTemplate;
/**
* 尝试获取锁
* 使用Lua脚本在Redis中执行SET命令,尝试获取锁
*
* @param lockKey 键
* @param lockValue 值,唯一的标识
* @param expire 锁的期时间,单位为毫秒
* @param acquireTimeout 获取锁的超时时间,单位为毫秒
* @return 一个表示锁实例的字符串,如果获取锁失败,则为null
*/
@Override
public String acquire(String lockKey, String lockValue, long expire, long acquireTimeout) {
System.out.println("尝试获取redis的锁");
String lock = redisTemplate.execute(SCRIPT_LOCK,
redisTemplate.getStringSerializer(),
redisTemplate.getStringSerializer(),
Collections.singletonList(lockKey),
lockValue, String.valueOf(expire));
final boolean locked = LOCK_SUCCESS.equals(lock);
return obtainLockInstance(locked, lock);
}
/**
* 释放锁
* 使用Lua脚本在Redis中执行验证和删除操作,以确保只有锁的持有者才能释放锁
*
* @param key 键
* @param value 值,用于验证锁的所有权。
* @param lockInstance 表示锁实例的字符串
* @return 表示释放锁是否成功的布尔值
*/
@Override
public boolean releaseLock(String key, String value, String lockInstance) {
System.out.println("释放锁");
String releaseResult = redisTemplate.execute(SCRIPT_UNLOCK,
redisTemplate.getStringSerializer(),
redisTemplate.getStringSerializer(),
Collections.singletonList(key), value);
return Boolean.parseBoolean(releaseResult);
}
}
再添加一个测试接口,如下:
//localhost:8082/testRedisLock
@Lock4j(keys = {"'key'"}, acquireTimeout = 1000, expire = 10000, executor = MyRedisTemplateLockExecutor.class)
@GetMapping("/testRedisLock")
public Object testRedisLock(){
long threadId = Thread.currentThread().getId();
System.out.println(threadId + " : 获取到了redis的锁,准备执行业务逻辑");
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return "testLock";
}
使用接口工具模拟一下并发请求,如下效果
通过控制台的输出可以看到自定义的执行器生效了
4.2.3 自定义锁的key生成策略
默认情况下,如果不做任何的设置,以redis为例,加锁时生成的key的前缀统一为:lock4j,这个在配置文件中也可以做如下显示的设置
加锁过程中,可以看到如下效果,key的格式为 lock4j开头
某些情况下,如果你想定制自己的key的生成策略,可以通过自定义一个类并实现LockKeyBuilder这个接口,如下:
package com.congge.config;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import com.baomidou.lock.LockKeyBuilder;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.context.expression.MethodBasedEvaluationContext;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.expression.BeanResolver;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
@Component
public class DefaultLockKeyBuilder implements LockKeyBuilder {
private static final ParameterNameDiscoverer NAME_DISCOVERER = new DefaultParameterNameDiscoverer();
private static final ExpressionParser PARSER = new SpelExpressionParser();
private BeanResolver beanResolver;
public DefaultLockKeyBuilder(BeanFactory beanFactory) {
this.beanResolver = new BeanFactoryResolver(beanFactory);
}
public String buildKey(MethodInvocation invocation, String[] definitionKeys) {
System.out.println("开始构建自定义的key");
Method method = invocation.getMethod();
return definitionKeys.length <= 1 && "".equals(definitionKeys[0]) ? "" : this.getSpelDefinitionKey(definitionKeys, method, invocation.getArguments());
}
protected String getSpelDefinitionKey(String[] definitionKeys, Method method, Object[] parameterValues) {
StandardEvaluationContext context = new MethodBasedEvaluationContext((Object)null, method, parameterValues, NAME_DISCOVERER);
context.setBeanResolver(this.beanResolver);
List<String> definitionKeyList = new ArrayList(definitionKeys.length);
String[] var6 = definitionKeys;
int var7 = definitionKeys.length;
for(int var8 = 0; var8 < var7; ++var8) {
String definitionKey = var6[var8];
if (definitionKey != null && !definitionKey.isEmpty()) {
String key = (String)PARSER.parseExpression(definitionKey).getValue(context, String.class);
definitionKeyList.add(key);
}
}
return StringUtils.collectionToDelimitedString(definitionKeyList, ".", "", "");
}
}
在buildkey的方法里面,你就可以根据自己的需求自定义key的生成策略了,再次请求接口,可以看到代码已经走到这里了
五、写在文末
本文详细的介绍了分布式锁技术组件Lock4j的使用,并通过案例代码演示了相关功能的使用,希望对看到的同学有用哦,本篇到此结束,感谢观看。