一、什么是分布式锁?
在分布式系统中,多个服务实例可能同时访问共享资源(如数据库、缓存),分布式锁用于保证同一时刻只有一个服务实例执行关键操作,避免竞争条件(如超卖、数据不一致)。
Redis 分布式锁的核心思路:利用 Redis 的键值对存储特性,通过原子操作实现锁的获取和释放。
二、Redis 分布式锁的实现原理
1. 核心命令
- 获取锁(SETNX)
SET key value NX PX expire_time
NX
:仅当键不存在时设置,保证原子性(防止多节点同时获取锁)。PX expire_time
:设置锁的过期时间(ms),避免死锁(如服务崩溃未释放锁)。
- 释放锁(LUA 脚本)
使用 LUA 脚本保证释放操作的原子性,避免误删其他节点的锁:lua
if redis.call("GET",KEYS[1]) == ARGV[1] thenreturn redis.call("DEL",KEYS[1]) elsereturn 0 end
- 先验证锁的持有者(通过唯一客户端 ID),再删除锁。
2. 关键特性
- 互斥性:同一时刻只有一个客户端持有锁。
- 安全性:锁只能由持有者释放,不能被其他客户端误删。
- 容错性:Redis 节点崩溃后,锁需能在集群中重新分配(需结合 Redis 集群模式)。
三、若依框架中如何使用 Redis 分布式锁?
若依框架是基于 Spring Boot 的快速开发框架,集成了 Redis 作为缓存。以下是在若依中实现分布式锁的步骤:
1. 依赖准备
若依框架已默认集成 Redis,无需额外添加依赖。若需自定义锁逻辑,可引入 Redis 客户端工具(如 spring-data-redis
)。
2. 自定义分布式锁工具类
在若依项目中创建 RedisLock
工具类,封装获取锁和释放锁的逻辑:
java
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;
import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.TimeUnit;@Component
public class RedisLock {private final RedisTemplate<String, String> redisTemplate;public RedisLock(RedisTemplate<String, String> redisTemplate) {this.redisTemplate = redisTemplate;}// 锁的前缀(避免键冲突)private static final String LOCK_PREFIX = "ry_lock:";// 获取锁public boolean tryLock(String lockKey, long timeout, TimeUnit unit) {String clientId = UUID.randomUUID().toString(); // 唯一标识客户端long expireTime = unit.toMillis(timeout);// 使用 SET NX PX 命令获取锁Boolean result = redisTemplate.opsForValue().setIfAbsent(LOCK_PREFIX + lockKey, clientId, expireTime, TimeUnit.MILLISECONDS);return result != null && result;}// 释放锁public boolean releaseLock(String lockKey, String clientId) {String 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);Long result = redisTemplate.execute(redisScript, Collections.singletonList(LOCK_PREFIX + lockKey), clientId);return result != null && result > 0;}
}
3. 在业务中使用分布式锁
以若依的订单创建场景为例,防止重复下单:
java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;@Service
public class OrderService {@Autowiredprivate RedisLock redisLock;public void createOrder(String userId, String orderId) {String lockKey = "order:" + userId; // 以用户 ID 作为锁键String clientId = UUID.randomUUID().toString();try {// 尝试获取锁(超时时间 5 秒)boolean locked = redisLock.tryLock(lockKey, 5, TimeUnit.SECONDS);if (!locked) {throw new RuntimeException("获取锁失败,请勿重复操作!");}// 执行业务逻辑(如检查库存、创建订单)// ...} finally {// 释放锁(需传入客户端 ID)redisLock.releaseLock(lockKey, clientId);}}
}
4. 若依框架集成建议
- 配置 Redis 连接:在
application.yml
中确保 Redis 地址、密码等配置正确。 - 异常处理:在
try-finally
中释放锁,避免因业务异常导致锁未释放。 - 锁粒度控制:锁键(如
order:userId
)需根据业务场景设计,避免锁范围过大影响性能。
四、常见问题与解决方案
1. 锁过期时间设置不合理
- 问题:业务执行时间超过锁过期时间,导致锁提前释放,其他客户端获取锁,引发并发问题。
- 解决方案:
- 合理评估业务耗时,设置稍长的过期时间(如 30 秒)。
- 使用 看门狗(Watchdog) 机制:在持有锁的线程中启动定时任务,自动延长锁的过期时间,直到业务完成。
2. Redis 集群模式下的锁失效
- 问题:主从模式中,主节点未及时同步锁数据到从节点,主节点宕机后,从节点升级为主节点,锁数据丢失,导致多个客户端获取锁。
- 解决方案:
- 使用 RedLock 算法(Redis 官方推荐):通过多个独立 Redis 节点获取锁,多数节点成功则认为获取锁成功,提高可靠性。
- 若依框架若使用 Redis 集群,需引入 RedLock 实现(可通过
redisson
客户端简化开发)。
3. 误删其他客户端的锁
- 问题:客户端 A 获取锁后,因业务耗时较长,锁过期自动释放,客户端 B 获取锁,此时客户端 A 执行完业务后释放锁,可能误删客户端 B 的锁。
- 解决方案:
- 释放锁时必须通过 客户端 ID 校验(如工具类中的
clientId
),确保仅释放自己的锁。
- 释放锁时必须通过 客户端 ID 校验(如工具类中的
五、扩展:使用 Redisson 简化分布式锁开发
Redisson 是 Redis 的 Java 客户端,内置了分布式锁的完整实现,相比手动编写代码更简单可靠。在若依中集成 Redisson 的步骤:
1. 添加依赖
xml
<dependency><groupId>org.redisson</groupId><artifactId>redisson-spring-boot-starter</artifactId><version>3.20.0</version>
</dependency>
2. 配置 Redisson
在 application.yml
中配置 Redis 连接(与若依原有 Redis 配置兼容):
yaml
spring:redis:host: 127.0.0.1port: 6379password:
3. 使用 Redisson 锁
java
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;@Service
public class OrderService {private final RedissonClient redissonClient;public OrderService(RedissonClient redissonClient) {this.redissonClient = redissonClient;}public void createOrder(String userId, String orderId) {RLock lock = redissonClient.getLock("order:" + userId);try {// 尝试获取锁(5 秒过期,3 秒等待时间)boolean locked = lock.tryLock(3, 5, TimeUnit.SECONDS);if (!locked) {throw new RuntimeException("操作太频繁,请稍后再试!");}// 执行业务逻辑} catch (InterruptedException e) {Thread.currentThread().interrupt();} finally {lock.unlock(); // 自动校验客户端 ID,安全释放锁}}
}
六、总结
- 核心逻辑:通过 Redis 的原子操作实现互斥,结合唯一 ID 保证安全释放。
- 若依框架适配:利用现有 Redis 配置,编写工具类或集成 Redisson 实现分布式锁。
- 注意事项:避免死锁、控制锁粒度、处理 Redis 集群下的一致性问题。
如果需要进一步调试或优化,可在若依的日志中监控锁的获取和释放情况,或通过 Redis 客户端工具(如 RedisInsight)观察键的生命周期。