1. 分布式锁的原理
在分布式系统中,多个进程或服务可能同时访问共享资源,为了避免资源竞争,需要一种机制来确保同一时间只有一个进程可以执行某个关键操作。Redis 提供了实现分布式锁的能力,其核心原理如下:
- 原子性操作:Redis 的
SET
命令支持NX
(不存在时设置)和PX
(设置过期时间)参数,可以确保在设置锁时是原子的。 - 唯一标识:每个请求生成一个唯一的标识符(如 UUID),用于标识锁的持有者,避免误删其他进程的锁。
- 锁的释放:使用 Lua 脚本确保只有锁的持有者才能释放锁,避免误删锁。
2. 实现步骤
- 获取锁:
- 使用
SET
命令尝试设置一个键值对,如果键不存在则设置成功,返回OK
,表示获取锁成功。 - 设置锁的过期时间,避免锁被永久占用。
- 使用
- 释放锁:
- 使用 Lua 脚本检查锁的持有者是否与当前请求的标识符一致,如果一致则删除锁。
3. 示例代码
以下是一个使用 Spring Boot 和 Redis 实现分布式锁的完整示例。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.TimeUnit;@Service
public class DistributedLockService {@Autowiredprivate StringRedisTemplate redisTemplate; // 注入 Redis 客户端private static final String LOCK_KEY = "resource_lock"; // 锁的键名private static final int LOCK_EXPIRE_TIME = 30000; // 锁的过期时间,单位毫秒/*** 获取分布式锁** @param requestId 请求的唯一标识符(如 UUID)* @return true 表示获取锁成功,false 表示获取锁失败*/public boolean acquireLock(String requestId) {// 使用 setIfAbsent 方法尝试设置锁,如果锁不存在则设置成功// 参数说明:// LOCK_KEY: 锁的键名// requestId: 锁的值,用于标识锁的持有者// LOCK_EXPIRE_TIME: 锁的过期时间// TimeUnit.MILLISECONDS: 时间单位(毫秒)Boolean result = redisTemplate.opsForValue().setIfAbsent(LOCK_KEY, requestId, LOCK_EXPIRE_TIME, TimeUnit.MILLISECONDS);return result != null && result; // 返回是否成功获取锁}/*** 释放分布式锁** @param requestId 请求的唯一标识符(如 UUID)* @return true 表示释放锁成功,false 表示释放锁失败*/public boolean releaseLock(String requestId) {// 使用 Lua 脚本确保只有锁的持有者才能释放锁// 脚本逻辑:// 1. 检查锁的值是否与当前请求的标识符一致// 2. 如果一致,则删除锁;否则返回 0String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);// 执行 Lua 脚本// 参数说明:// redisScript: Lua 脚本// Collections.singletonList(LOCK_KEY): 脚本的键参数(锁的键名)// requestId: 脚本的参数值(锁的持有者标识符)Long result = redisTemplate.execute(redisScript, Collections.singletonList(LOCK_KEY), requestId);return result != null && result == 1; // 返回是否成功释放锁}// 业务逻辑示例public void performTask() {String requestId = UUID.randomUUID().toString();try {if (acquireLock(requestId)) {// 获取锁成功,执行业务逻辑System.out.println("Lock acquired, executing critical section...");// 模拟业务逻辑Thread.sleep(1000);} else {// 获取锁失败System.out.println("Failed to acquire lock.");}} catch (InterruptedException e) {e.printStackTrace();} finally {// 释放锁if (releaseLock(requestId)) {System.out.println("Lock released.");} else {System.out.println("Failed to release lock.");}}}}
4. 代码分析
- 获取锁:
- 使用
setIfAbsent
方法尝试设置锁,如果锁不存在则设置成功,并设置过期时间。 - 返回
true
表示获取锁成功,false
表示获取锁失败。
- 使用
- 释放锁:
- 使用 Lua 脚本检查锁的持有者是否与当前请求的标识符一致,如果一致则删除锁。
- 返回
true
表示释放锁成功,false
表示释放锁失败。
- 业务逻辑:
- 在获取锁成功后执行业务逻辑,确保同一时间只有一个进程可以执行关键操作。
- 在
finally
块中释放锁,确保锁一定会被释放。
5. 注意事项
- 锁的过期时间:设置合理的过期时间,避免锁被永久占用。
- 锁的唯一标识:每个请求生成唯一的标识符,避免误删其他进程的锁。
- 锁的释放:使用 Lua 脚本确保只有锁的持有者才能释放锁,避免误删锁。
6. 总结
通过 Redis 实现分布式锁是一种简单有效的方式,可以确保在分布式系统中同一时间只有一个进程可以执行关键操作。结合 Spring Boot 和 Redis,可以轻松实现分布式锁,并通过设置合理的过期时间和唯一标识来管理锁的生命周期。