Redis分布式锁的正确用法
多个服务同时操作同一个资源时,需要一种协调机制。比如秒杀活动中扣减库存,或者多个用户抢同一个红包。Redis分布式锁就是解决这种问题的常用工具。它利用Redis的高性能和原子操作特性,确保同一时间只有一个客户端能执行关键操作。
一个可靠的分布式锁需要满足几个基本要求:
互斥性:同一时间只能有一个客户端持有锁
超时释放:锁必须有有效期,避免死锁
安全释放:只能由锁的持有者释放锁
高可用:锁服务要稳定可靠
下面介绍几种实现方式,从简单到复杂。
基础实现:SET命令
Redis的SET命令有个特殊用法:SET key value NX PX milliseconds。NX表示只在键不存在时设置,PX设置过期时间(毫秒)。这个命令是原子性的,很适合实现锁。
import redis.clients.jedis.Jedis;
import java.util.UUID;
public class RedisLock {
// 尝试获取锁
public static boolean tryLock(Jedis jedis, String lockKey, String uniqueId, int expireTime) {
String result = jedis.set(lockKey, uniqueId, "NX", "PX", expireTime);
return "OK".equals(result);
}
// 释放锁
public static void unlock(Jedis jedis, String lockKey, String uniqueId) {
String luaScript =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
jedis.eval(luaScript, 1, lockKey, uniqueId);
}
// 使用示例
public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);
String lockKey = "order_lock";
String clientId = UUID.randomUUID().toString(); // 唯一标识
try {
if (tryLock(jedis, lockKey, clientId, 30000)) {
// 这里执行需要加锁的业务逻辑
System.out.println("执行业务操作");
}
} finally {
unlock(jedis, lockKey, clientId);
}
}
}注意几个要点:
每个客户端使用唯一标识(如UUID),防止误删别人的锁
使用Lua脚本保证检查和删除是原子操作
一定要在finally中释放锁
Spring Boot项目中的实现
如果你用Spring Boot,可以用StringRedisTemplate,写起来更简洁。
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@Service
public class RedisLockService {
private final StringRedisTemplate redisTemplate;
public RedisLockService(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
public boolean tryLock(String lockKey, long expireTime) {
String uniqueId = UUID.randomUUID().toString();
Boolean result = redisTemplate.opsForValue()
.setIfAbsent(lockKey, uniqueId, expireTime, TimeUnit.MILLISECONDS);
return Boolean.TRUE.equals(result);
}
public void unlock(String lockKey, String uniqueId) {
// 使用Lua脚本保证原子性
String script =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
Collections.singletonList(lockKey),
uniqueId
);
}
}使用Redisson框架
手动实现分布式锁要考虑很多细节,比如锁续期、可重入等。Redisson是一个成熟的Redis客户端,内置了分布式锁的实现,推荐使用。
首先添加依赖:
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.27.2</version>
</dependency>配置Redis连接:
spring:
redis:
host: localhost
port: 6379使用Redisson的锁:
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 OrderService {
@Autowired
private RedissonClient redissonClient;
public void processOrder(String orderId) {
RLock lock = redissonClient.getLock("order:" + orderId);
try {
// 尝试获取锁,最多等待10秒,锁持有30秒
boolean locked = lock.tryLock(10, 30, TimeUnit.SECONDS);
if (locked) {
// 执行业务逻辑
System.out.println("处理订单: " + orderId);
} else {
System.out.println("获取锁失败");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
if (lock.isLocked() && lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}Redisson的优点:
自动续期:业务没执行完,锁快过期时会自动续期
可重入:同一个线程可以多次获取同一把锁
高可用:支持主从、哨兵、集群模式
实际应用案例:库存扣减
假设有个秒杀系统,多个用户同时抢购同一商品。
@Service
public class SeckillService {
@Autowired
private RedissonClient redissonClient;
@Autowired
private StockService stockService;
public String seckill(String productId, String userId) {
// 按商品ID加锁,不同商品可以并发处理
String lockKey = "seckill:" + productId;
RLock lock = redissonClient.getLock(lockKey);
try {
// 快速失败,等待不超过1秒
if (!lock.tryLock(1, 10, TimeUnit.SECONDS)) {
return "抢购太火爆,请重试";
}
// 检查库存
int stock = stockService.getStock(productId);
if (stock <= 0) {
return "商品已售完";
}
// 扣减库存
boolean success = stockService.reduceStock(productId, 1);
if (success) {
// 创建订单
createOrder(productId, userId);
return "抢购成功";
} else {
return "库存不足";
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return "系统异常";
} finally {
if (lock.isLocked() && lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}需要注意的问题
1. 锁过期时间设置
锁的过期时间不能太短,否则业务没执行完锁就释放了。也不能太长,否则客户端崩溃后锁要很久才能自动释放。一般设置在10-30秒,具体根据业务执行时间调整。
2. 避免死锁
一定要在finally块中释放锁。即使业务代码抛出异常,也要确保锁被释放。
3. 主从切换问题
在Redis主从架构中,如果主节点写入锁后宕机,从节点可能还没同步锁数据就升级为主节点,导致锁丢失。对一致性要求高的场景,可以考虑使用Redlock算法(多个独立的Redis实例),或者用ZooKeeper等CP系统。
4. 锁粒度要合适
锁的粒度越细,并发度越高。比如按用户ID加锁比全局一把锁性能更好。但也要避免锁太多消耗过多资源。
什么时候不用分布式锁
分布式锁不是万能解药。有些场景可以用更简单的方式:
数据库唯一约束:防重复提交可以用数据库唯一索引
乐观锁:更新数据时加版本号检查
消息队列:通过队列串行处理请求
原则是:能不用锁就不用锁,能用简单方案就不用复杂方案。
总结
Redis分布式锁是解决分布式系统并发问题的有效工具。对于简单场景,可以直接用SET命令实现。对于Spring Boot项目,StringRedisTemplate更方便。对于生产环境,推荐使用Redisson,它功能完善,稳定性好。
关键是要理解锁的原理,设置合理的过期时间,确保锁的安全释放。同时要考虑锁粒度、性能影响和替代方案。正确使用分布式锁,能让你的系统更稳定可靠。
本文内容仅供个人学习、研究或参考使用,不构成任何形式的决策建议、专业指导或法律依据。未经授权,禁止任何单位或个人以商业售卖、虚假宣传、侵权传播等非学习研究目的使用本文内容。如需分享或转载,请保留原文来源信息,不得篡改、删减内容或侵犯相关权益。感谢您的理解与支持!