springboot中实现分布式锁有多种方法,一般采用Redis来实现分布式锁的比较多,它基于Redis的高性能和高可用特性,能够有效地协调多节点间的并发访问,防止竞争条件。在Spring Boot中使用Redis实现分布式锁,可以通过直接使用Jedis客户端或者集成第三方库如Redisson来简化操作。
原理如下:
基于Redis的分布式锁实现依赖于Redis的原子操作能力,核心是使用SET命令的NX和PX选项来实现锁的获取与自动过期。
当多个线程想要访问受保护的资源时,首先会循环尝试获取锁,这里可以设置获取时间,也可以设置获取次数,依据实际项目来定。(我们这里使用设置获取时间的方式)
获取锁之后它会尝试在Redis中设置一个带有唯一标识的键。如果设置成功,服务获得锁并可以继续执行;如果失败,则重试或回退。设置时间的意义在于如果因为其他因素导致redis宕机,也不会影响后续线程。
为确保锁不会被误删,释放锁时会检查键的值与设置时的标识是否一致。此外,通过引入锁续期机制和RedLock算法,可以进一步增强锁的稳定性和安全性。
1、基于Jedis客户端手动实现(不推荐)
1.添加依赖:首先确保你的pom.xml
中添加了Jedis客户端的依赖。
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.7.0</version> <!-- 使用最新的稳定版本 -->
</dependency>
2.分布式锁实现
下面的Java代码展示了如何实现一个基本的Redis分布式锁,包括获取锁、执行业务逻辑和释放锁的过程。此示例中,我们将使用Redis的SETNX
(Set if Not Exists)命令尝试获取锁,并且利用EX
参数设置锁的过期时间来防止死锁。
import redis.clients.jedis.Jedis;
import java.util.UUID;
public class DistributedLockWithJedis {
private Jedis jedis;
private static final String LOCK_SUCCESS = "OK";
private static final long LOCK_TIMEOUT = 5; // 锁超时时间,单位:秒
public DistributedLockWithJedis(Jedis jedis) {
this.jedis = jedis;
}
/**
* 尝试获取锁,允许设置获取锁的超时时间
* @param lockKey 锁的键名
* @return 成功时返回锁的唯一标识,失败则返回null
*/
public String tryLock(String lockKey) {
final String identifier = UUID.randomUUID().toString(); // 生成一个全局唯一的标识符
final long endTime = System.currentTimeMillis() + LOCK_TIMEOUT * 1000; // 计算尝试获取锁的结束时间点
// 在指定超时时间内循环尝试获取锁
while (System.currentTimeMillis() < endTime) {
// 使用SETNX命令尝试设置锁,同时设置锁的自动过期时间
String result = jedis.set(lockKey, identifier, "NX", "EX", LOCK_TIMEOUT);
if (LOCK_SUCCESS.equals(result)) {
System.out.println("锁已获得,标识符为: " + identifier);
return identifier; // 成功获取锁,返回标识符
}
try {
Thread.sleep(100); // 如果没有获取到锁,则短暂休眠后重试
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
}
}
return null; // 超时未获取到锁,返回null
}
/**
* 释放锁
* @param lockKey 锁的键名
* @param identifier 锁的唯一标识
* @return 是否成功释放了锁
*/
public boolean releaseLock(String lockKey, String identifier) {
// 使用Lua脚本确保删除操作的原子性,避免由于并发问题导致误删他人锁
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, 1, lockKey, identifier);
if ("1".equals(result.toString())) {
System.out.println("锁已释放,标识符为: " + identifier);
return true;
}
return false;
}
// 示例使用方法
public static void main(String[] args) {
Jedis jedis = new Jedis("localhost");
DistributedLockWithJedis locker = new DistributedLockWithJedis(jedis);
String lockKey = "myDistributedLock";
String lockIdentifier = locker.tryLock(lockKey);
if (lockIdentifier != null) {
try {
// 在此执行受保护的业务逻辑
System.out.println("正在执行关键部分...");
} finally {
locker.releaseLock(lockKey, lockIdentifier); // 使用正确的标识符释放锁
}
} else {
System.out.println("未能获取到锁。");
}
jedis.close(); // 关闭Jedis连接
}
}
2、使用Redisson实现
使用Redisson在Spring Boot中实现分布式锁是一个既高效又简便的方法。上边我们说过的实现原理在Redisson中有更简洁的体现,我们不需要再自己创建唯一键,删除键时也不用额外写逻辑去验证是否误删,Redisson默认在内部处理锁的标识验证,当你使用tryLock
方法获取锁时,它会在Redis中存储一个带有锁标识(内部由Redisson管理)的键值对,解锁时会校验这个标识。因此,直接使用Redisson的API,你不需要显式地添加额外的标识验证代码。也不用关心锁的过期时间,Redisson都在内部为我们实现了。
如果设置获取时间的话redissonClient中有方法可以直接设置:
boolean isLocked = lock.tryLock(10, 5, TimeUnit.SECONDS); // 尝试获取锁,等待10秒,锁的最大持有时间为5秒
这意味着,如果服务在持有锁的5秒内没有主动释放锁(比如因为处理逻辑未完成或服务崩溃),Redisson会自动在5秒后让锁失效,其他等待的线程就有机会获取锁并继续执行。
Redisson的锁实现了自动续期机制,只要客户端持有锁且会话有效,锁就会自动续期,避免因处理时间过长导致锁意外过期。你可以在创建RedissonClient
时设置锁的默认自动续期时间,或者在获取锁时指定续期参数。例如,如果你希望锁自动续期,可以这样设置:
// 配置RedissonClient时设置默认锁自动续期时间
Config config = new Config();
config.useSingleServer().setAddress("redis://localhost:6379")
.setLockWatchdogTimeout(30, TimeUnit.SECONDS); // 设置锁自动续期时间窗口为30秒
RedissonClient redisson = Redisson.create(config);
当然我们也可以使用DI注入的方式将上述配置写入配置类中:
@Configuration
public class RedissonConfig {
@Value("${spring.redis.host}")
private String redisHost;
@Value("${spring.redis.port}")
private int redisPort;
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://" + redisHost + ":" + redisPort)
.setDatabase(0)
.setLockWatchdogTimeout(30, TimeUnit.SECONDS);
return Redisson.create(config);
}
}
这里的配置从Spring Boot的默认配置中读取Redis服务器的地址和端口,确保这些属性已经在你的application.properties
或application.yml
文件中正确配置。我们在下文也使用配置类的方式,这样可以使我们编写更灵活、可测试的代码。
下面是详细的步骤指南,帮助你了解如何在Spring Boot应用中集成和使用Redisson来实现分布式锁:
1.在你的Spring Boot项目的pom.xml
文件中,添加Redisson的Spring Boot Starter依赖。确保使用与你的Spring Boot版本兼容的Redisson版本。
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>{latest-version}</version> <!-- 替换为最新稳定版本 -->
</dependency>
2.配置Redisson,在application.properties
或application.yml
中配置Redis服务器的连接信息。
spring:
redis:
host: your-redis-host
port: 6379
password: your-redis-password # 如果有密码的话
3.Spring Boot会自动配置RedissonClient,你可以在需要使用分布式锁的服务类中直接注入RedissonClient。
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class MyService {
private final RedissonClient redissonClient;
@Autowired
public BusinessService(RedissonClient redissonClient) {
this.redissonClient = redissonClient;
}
public void criticalSectionOperation(String businessId) {
RLock lock = redissonClient.getLock("myLock" + businessId)); // 锁名称可以加上业务ID来使我们更容易区分
try {
// 尝试获取锁,如果无法立即获取则等待直到获得锁或者达到最大等待时间
boolean isLocked = lock.tryLock(10, 5, TimeUnit.SECONDS); // 尝试获取锁,等待10秒,锁的最大持有时间为5秒
if (isLocked) {
// 执行临界区代码
System.out.println("已获取锁,正在执行关键部分...");
// 你的业务逻辑代码
} else {
System.out.println("在指定时间内未能获取到锁。");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println("线程在尝试获取锁时被中断。);
} finally {
// 无论是否执行成功,都要确保解锁
if (lock.isHeldByCurrentThread()) {
lock.unlock();
System.out.println("Lock released.");
}
}
}
}
通过引入锁续期机制和RedLock算法,可以进一步增强锁的稳定性和安全性。
需要注意的是:
- 确保Redis服务器的稳定性,避免因Redis故障导致的锁管理问题。
- 正确处理锁的超时和释放,避免死锁。
- 考虑使用lua脚本进行原子操作,以减少分布式环境下的竞态条件。