Spring Boot 微服务分布式锁实战:从 Redis 到 Redisson 的选型与落地经验

Spring Boot 微服务分布式锁实战:从 Redis 到 Redisson 的选型与落地经验

技术主题:Java 编程语言
内容方向:实际使用经验分享(工具/框架选型、项目落地心得)

引言

在微服务架构下,分布式锁是解决并发安全问题的关键技术。我们团队在构建电商系统时,面临典型的库存超卖问题:多个服务实例同时处理订单,导致库存扣减不准确。经过半年的实践,我们从最初的 Redis SETNX 手动实现,到最终选择 Redisson 作为生产方案,积累了不少经验教训。本文将分享这个完整的技术选型和落地过程。

一、业务背景与技术挑战

业务场景

我们的电商系统采用微服务架构:

  • 订单服务:处理用户下单逻辑
  • 库存服务:管理商品库存
  • 支付服务:处理支付流程

在促销活动中,经常出现以下问题:

  • 同一商品被多个用户同时购买
  • 库存为1,但成功创建了3个订单
  • 数据库层面的行锁无法解决跨服务的并发问题

技术要求

基于业务需求,我们对分布式锁提出了以下要求:

  1. 互斥性:同一时刻只有一个服务实例能获取锁
  2. 防死锁:锁必须有过期时间,避免服务宕机导致死锁
  3. 可重入:同一线程可以多次获取同一把锁
  4. 高性能:锁的获取和释放要足够快,不能成为性能瓶颈
  5. 高可用:锁服务本身要稳定可靠

二、技术选型历程

第一阶段:原生 Redis 实现

最初我们使用 Redis 的 SETNX 命令手动实现分布式锁:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@Component
public class RedisDistributedLock {

@Autowired
private StringRedisTemplate redisTemplate;

private static final String LOCK_PREFIX = "distributed_lock:";
private static final int DEFAULT_EXPIRE_TIME = 30; // 30秒过期

/**
* 获取分布式锁 - 第一版实现
*/
public boolean tryLock(String key, String requestId, int expireTime) {
String lockKey = LOCK_PREFIX + key;

// 使用 SET 命令的 NX 和 EX 参数实现原子操作
String result = redisTemplate.opsForValue().setIfAbsent(
lockKey, requestId, Duration.ofSeconds(expireTime)
) ? "OK" : null;

return "OK".equals(result);
}

/**
* 释放分布式锁 - 第一版实现
*/
public boolean releaseLock(String key, String requestId) {
String lockKey = LOCK_PREFIX + key;

// Lua脚本保证原子性:先判断是否是自己的锁,再删除
String luaScript =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";

Long result = redisTemplate.execute(
new DefaultRedisScript<>(luaScript, Long.class),
Collections.singletonList(lockKey),
requestId
);

return result != null && result == 1L;
}
}

使用示例和问题发现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Service
public class InventoryService {

@Autowired
private RedisDistributedLock distributedLock;

public boolean reduceInventory(Long productId, int quantity) {
String lockKey = "inventory:" + productId;
String requestId = UUID.randomUUID().toString();

// 尝试获取锁
boolean locked = distributedLock.tryLock(lockKey, requestId, 30);
if (!locked) {
return false; // 获取锁失败
}

try {
// 业务逻辑:检查库存并扣减
Integer currentStock = getCurrentStock(productId);
if (currentStock >= quantity) {
updateStock(productId, currentStock - quantity);
return true;
}
return false;
} finally {
// 释放锁
distributedLock.releaseLock(lockKey, requestId);
}
}

// ... 其他方法
}

第一阶段遇到的问题:

  1. 锁续期困难:业务执行时间超过锁过期时间时,锁被自动释放
  2. 不支持可重入:同一线程无法多次获取同一把锁
  3. 代码复杂:每次使用都要手动管理 requestId 和异常处理
  4. 监控困难:缺乏锁状态的可观测性

第二阶段:改进版 Redis 实现

针对第一阶段的问题,我们进行了改进:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
@Component
public class ImprovedRedisLock {

@Autowired
private StringRedisTemplate redisTemplate;

private static final ThreadLocal<Map<String, LockInfo>> LOCK_MAP =
ThreadLocal.withInitial(HashMap::new);

/**
* 锁信息
*/
@Data
private static class LockInfo {
private String requestId;
private int reentrantCount;
private ScheduledFuture<?> renewTask;
}

public boolean tryLock(String key, int expireTime) {
Map<String, LockInfo> lockMap = LOCK_MAP.get();
String lockKey = "lock:" + key;

// 检查可重入
LockInfo existingLock = lockMap.get(lockKey);
if (existingLock != null) {
existingLock.setReentrantCount(existingLock.getReentrantCount() + 1);
return true;
}

// 尝试获取新锁
String requestId = Thread.currentThread().getId() + ":" + UUID.randomUUID();
Boolean success = redisTemplate.opsForValue().setIfAbsent(
lockKey, requestId, Duration.ofSeconds(expireTime)
);

if (Boolean.TRUE.equals(success)) {
LockInfo lockInfo = new LockInfo();
lockInfo.setRequestId(requestId);
lockInfo.setReentrantCount(1);

// 启动锁续期任务
lockInfo.setRenewTask(startRenewTask(lockKey, requestId, expireTime));

lockMap.put(lockKey, lockInfo);
return true;
}

return false;
}

public boolean releaseLock(String key) {
Map<String, LockInfo> lockMap = LOCK_MAP.get();
String lockKey = "lock:" + key;

LockInfo lockInfo = lockMap.get(lockKey);
if (lockInfo == null) {
return false;
}

// 处理可重入
lockInfo.setReentrantCount(lockInfo.getReentrantCount() - 1);
if (lockInfo.getReentrantCount() > 0) {
return true;
}

// 释放锁
try {
// 停止续期任务
if (lockInfo.getRenewTask() != null) {
lockInfo.getRenewTask().cancel(true);
}

// 删除 Redis 中的锁
String luaScript =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else return 0 end";

redisTemplate.execute(
new DefaultRedisScript<>(luaScript, Long.class),
Collections.singletonList(lockKey),
lockInfo.getRequestId()
);

} finally {
lockMap.remove(lockKey);
if (lockMap.isEmpty()) {
LOCK_MAP.remove();
}
}

return true;
}

/**
* 启动锁续期任务
*/
private ScheduledFuture<?> startRenewTask(String lockKey, String requestId, int expireTime) {
return Executors.newScheduledThreadPool(1).scheduleAtFixedRate(() -> {
try {
String luaScript =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('expire', KEYS[1], ARGV[2]) " +
"else return 0 end";

redisTemplate.execute(
new DefaultRedisScript<>(luaScript, Long.class),
Collections.singletonList(lockKey),
requestId, String.valueOf(expireTime)
);
} catch (Exception e) {
log.error("锁续期失败: {}", lockKey, e);
}
}, expireTime / 3, expireTime / 3, TimeUnit.SECONDS);
}
}

第二阶段的问题:

虽然解决了可重入和续期问题,但代码变得非常复杂,维护成本高,而且仍然存在一些边界情况的 bug。

第三阶段:Redisson 最终方案

经过调研,我们决定使用 Redisson,它是一个成熟的 Redis Java 客户端,内置了完善的分布式锁实现。

1. 添加依赖和配置

1
2
3
4
5
6
<!-- pom.xml -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.24.3</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# application.yml
spring:
redis:
redisson:
config: |
singleServerConfig:
address: "redis://localhost:6379"
password: null
database: 0
connectionPoolSize: 64
connectionMinimumIdleSize: 10
subscriptionConnectionPoolSize: 50
subscriptionConnectionMinimumIdleSize: 1
connectTimeout: 10000
timeout: 3000
retryAttempts: 3
retryInterval: 1500

2. 封装 Redisson 分布式锁工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
@Component
@Slf4j
public class RedissonDistributedLock {

@Autowired
private RedissonClient redissonClient;

private static final String LOCK_PREFIX = "redisson:lock:";

/**
* 尝试获取锁
*/
public boolean tryLock(String key, long waitTime, long leaseTime, TimeUnit unit) {
RLock lock = redissonClient.getLock(LOCK_PREFIX + key);
try {
return lock.tryLock(waitTime, leaseTime, unit);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}

/**
* 获取锁(阻塞)
*/
public void lock(String key, long leaseTime, TimeUnit unit) {
RLock lock = redissonClient.getLock(LOCK_PREFIX + key);
lock.lock(leaseTime, unit);
}

/**
* 释放锁
*/
public void unlock(String key) {
RLock lock = redissonClient.getLock(LOCK_PREFIX + key);
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}

/**
* 使用锁执行业务逻辑
*/
public <T> T executeWithLock(String key, long waitTime, long leaseTime,
TimeUnit unit, Supplier<T> supplier) {
RLock lock = redissonClient.getLock(LOCK_PREFIX + key);
try {
boolean acquired = lock.tryLock(waitTime, leaseTime, unit);
if (!acquired) {
throw new RuntimeException("获取锁失败: " + key);
}

return supplier.get();

} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("获取锁被中断: " + key, e);
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}

3. 业务代码使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@Service
public class OptimizedInventoryService {

@Autowired
private RedissonDistributedLock distributedLock;

/**
* 扣减库存 - 使用 Redisson
*/
public boolean reduceInventory(Long productId, int quantity) {
String lockKey = "inventory:" + productId;

return distributedLock.executeWithLock(
lockKey,
100, // 等待100毫秒
30, // 锁30秒后自动释放
TimeUnit.MILLISECONDS,
() -> {
// 业务逻辑
Integer currentStock = getCurrentStock(productId);
if (currentStock >= quantity) {
updateStock(productId, currentStock - quantity);
log.info("库存扣减成功: 商品={}, 扣减={}, 剩余={}",
productId, quantity, currentStock - quantity);
return true;
}
log.warn("库存不足: 商品={}, 需要={}, 当前={}",
productId, quantity, currentStock);
return false;
}
);
}

/**
* 批量操作示例 - 可重入锁
*/
@Transactional
public void batchReduceInventory(List<OrderItem> items) {
for (OrderItem item : items) {
reduceInventory(item.getProductId(), item.getQuantity());
// 由于 Redisson 支持可重入,这里可以安全调用
}
}
}

三、生产实践经验

1. 性能调优建议

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Configuration
public class RedissonConfig {

@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://localhost:6379")
.setConnectionPoolSize(100) // 连接池大小
.setConnectionMinimumIdleSize(20) // 最小空闲连接
.setConnectTimeout(5000) // 连接超时
.setTimeout(3000) // 命令超时
.setRetryAttempts(3); // 重试次数

return Redisson.create(config);
}
}

2. 监控和可观测性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Component
@Slf4j
public class LockMonitor {

@Autowired
private RedissonClient redissonClient;

@EventListener
@Async
public void handleLockEvent(LockEvent event) {
// 记录锁的获取和释放
log.info("Lock event: type={}, key={}, thread={}, timestamp={}",
event.getType(), event.getKey(),
event.getThreadId(), event.getTimestamp());
}

/**
* 定期检查锁状态
*/
@Scheduled(fixedRate = 60000) // 每分钟检查一次
public void checkLockStatus() {
// 可以通过 Redisson 的管理接口获取锁信息
// 这里简化处理
log.debug("定期锁状态检查完成");
}
}

3. 常见问题和解决方案

问题1:锁粒度过粗导致性能问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 错误:锁粒度太粗
public void updateUserProfile(Long userId, UserProfile profile) {
distributedLock.executeWithLock("user_update", 1000, 30000, TimeUnit.MILLISECONDS, () -> {
// 所有用户更新都被串行化了
return updateProfile(userId, profile);
});
}

// 正确:细粒度锁
public void updateUserProfile(Long userId, UserProfile profile) {
distributedLock.executeWithLock("user_update:" + userId, 1000, 30000, TimeUnit.MILLISECONDS, () -> {
// 只有同一用户的更新才会被串行化
return updateProfile(userId, profile);
});
}

问题2:死锁风险

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 有死锁风险的代码
public void transferInventory(Long fromProductId, Long toProductId, int quantity) {
String lockKey1 = "inventory:" + fromProductId;
String lockKey2 = "inventory:" + toProductId;

// 始终按 ID 大小顺序获取锁,避免死锁
String firstLock = fromProductId < toProductId ? lockKey1 : lockKey2;
String secondLock = fromProductId < toProductId ? lockKey2 : lockKey1;

distributedLock.executeWithLock(firstLock, 100, 30000, TimeUnit.MILLISECONDS, () -> {
return distributedLock.executeWithLock(secondLock, 100, 30000, TimeUnit.MILLISECONDS, () -> {
// 执行转移逻辑
return doTransfer(fromProductId, toProductId, quantity);
});
});
}

四、方案对比总结

方案 优点 缺点 适用场景
原生Redis 简单直接,性能好 功能不完整,代码复杂 简单场景,对功能要求不高
改进Redis 功能相对完整 代码复杂,维护成本高,bug风险大 不推荐使用
Redisson 功能完整,稳定可靠,代码简洁 引入额外依赖,学习成本 生产环境推荐方案

五、最佳实践建议

  1. 选择合适的锁粒度:既要避免锁竞争,又要保证数据一致性
  2. 设置合理的超时时间:根据业务执行时间设置锁的lease time
  3. 避免长时间持锁:将耗时操作移到锁外执行
  4. 做好监控和告警:监控锁的获取失败率和持锁时间
  5. 考虑降级方案:当锁服务不可用时的备选方案

总结

经过三个阶段的实践,我们深刻体会到:不要重复造轮子,选择成熟的解决方案更重要。Redisson 不仅解决了我们的技术需求,还提供了丰富的功能和稳定的性能。

在微服务架构下,分布式锁是必不可少的基础设施。选择合适的工具和方案,能够大大降低开发和维护成本,提高系统的稳定性。希望我们的经验能够帮助其他团队少走弯路,快速构建稳定可靠的分布式系统。