1.背景
在分布式系统架构中,多节点并发访问共享资源是常见场景。当业务涉及多个关联资源的协同操作时(如订单系统中的商品与库存等),传统单资源锁无法满足业务需求,而多资源锁的实现又面临以下挑战:
- 死锁风险:不同节点以不同顺序获取多个锁时易导致死锁
- 公平性问题:先到请求可能因资源获取顺序不公导致长时间等待
- 性能瓶颈:分布式锁的获取与释放涉及网络通信,需优化重试机制
2.核心问题
2.1 典型业务场景
以电商库存扣减为例:
- 操作1:扣减商品库存(需锁定商品库存资源)
- 操作2:扣减仓库库存(需锁定仓库库存资源)
- 复合操作:同时扣减商品库存和仓库库存(需同时锁定商品库存和仓库库存资源)
2.2 技术痛点
- 顺序敏感:若操作1先锁商品库存再锁仓库库存,操作2反之,可能形成循环等待
- 饥饿现象:后到的请求可能抢占先到的请求资源
- 原子性保障:多资源锁定需保证要么全部成功,要么全部失败
3.问题分析
3.1.现有方案对比
| 方案类型 | 优点 | 缺点 |
| 单资源锁 | 实现简单 | 无法处理多资源关联场景 |
| 顺序锁 | 避免死锁 | 需严格约定加锁顺序,灵活性差 |
| RedLock算法 | 高可用 | 不保证公平性,时钟漂移问题 |
| 队列等待机制 | 天然公平 | 实现复杂,需处理超时和重试 |
3.2.关键需求
- 公平性:保证先到请求优先获取资源
- 原子性:多资源锁定/释放的原子操作
- 灵活性:支持单/双资源混合锁定
- 容错性:超时自动释放避免死锁
4.解决方案:多资源分布式顺序锁
4.1.核心设计思想
通过Redis实现基于等待队列的公平锁机制,结合Lua脚本保证原子性操作,主要包含:
- 多Key设计:队列Key(维护请求顺序) + 锁Key(实际资源锁定)
- 多阶段验证:先确认队列头部位置,再执行原子加锁
- 多资源支持:通过统一资源标识处理多资源场景
4.2.技术实现详解
4.2.1 关键组件
java
@Component
@Slf4j
public class DynamicResourceLock {
@Resource
private RedisTemplate<String, String> redisTemplate;
private final String lockPrefix = "mall:Lock:"; //锁前缀
private final String queuePrefix = "mall:Queue:"; //队列前缀
private final long defaultTimeout = 5; // 默认超时时间(秒)
private final long lockTTL = 10; // 锁持有时间(秒)
}
4.2.2核心流程
1.资源注册
- 将有效资源ID(r1,r2)统一处理为字符串列表
- 示例:
getValidResources("C123", "W456")→ ["C123", "W456"]
typescript
private List<String> getValidResources(String cId, String wId) {
List<String> resources = new ArrayList<>();
// 添加原有的uocId和jshId资源
Arrays.stream(new String[]{cId, wId})
.filter(Objects::nonNull)
.filter(s -> !s.isEmpty())
.forEach(resources::add);
return resources;
}
2.入队操作
- 使用Redis List的rightPush保证FIFO顺序
- 设置队列TTL防止死队列
- 设置lockId ****锁标识符,确保只有合法的持有者才能释放锁
typescript
/**
* 尝试获取锁(自动处理单/双资源)
* @param r1 资源1(可为null)
* @param r2 资源2(可为null)
* @return 锁ID(失败返回null)
*/
public String tryLock(String r1, String r2) {
List<String> resources = getValidResources(r1, r2);
if (resources.isEmpty()) {
throw new IllegalArgumentException("至少需要一个有效资源ID");
}
String lockId = UUID.randomUUID().toString();
return tryLock(resources, lockId, defaultTimeout) ? lockId : null;
}
/**
* 获取锁
* @param resources 资源
* @param lockId 锁唯一标识
* @param timeout 是获取锁的最大等待时间
* @return 是否成功获取到锁
*/
private boolean tryLock(List<String> resources, String lockId, long timeout) {
long start = System.currentTimeMillis();
// 1. 加入所有资源的等待队列
resources.forEach(res -> enqueue(res, lockId));
log.info("已加入资源队列,资源:{},锁ID:{}", resources, lockId);
....
}
/**
* 资源加入队列
* @param resource 资源
* @param lockId 锁唯一标识
*/
private void enqueue(String resource, String lockId) {
redisTemplate.opsForList().rightPush(queuePrefix + resource, lockId);
redisTemplate.expire(queuePrefix + resource, lockTTL, TimeUnit.SECONDS);
}
3.原子加锁
lua脚本分析
lua
-- 检查所有资源队列的队首是否都是当前请求
local all_ready = 1
local queue_key_count = #KEYS / 2 -- 计算队列key的数量
for i = 1, queue_key_count do
local queue_key = KEYS[i]
-- 检查数据结构类型是否为list
if redis.call('type', queue_key)['ok'] ~= 'list' then
return -1 -- 数据结构错误
end
-- 检查队首元素是否是当前lockId
if redis.call('lindex', queue_key, 0) ~= ARGV[1] then
all_ready = 0 -- 不是队首,设置标志位
break -- 跳出循环
end
end
-- 如果所有队列的队首都是当前请求
if all_ready == 1 then
-- 遍历所有锁key(KEYS的后半部分)
for i = queue_key_count + 1, #KEYS do
local lock_key = KEYS[i]
-- 尝试设置锁(带过期时间和NX条件)
redis.call('set', lock_key, ARGV[1], 'EX', ARGV[2], 'NX')
-- 验证锁是否设置成功
if redis.call('get', lock_key) ~= ARGV[1] then
return 0 -- 获取锁失败
end
end
return 1 -- 所有锁获取成功
else
return 0 -- 不是所有队列的队首
end
获取锁
- 100ms间隔检查是否到达队列头部、是否能获取到锁
- 超时后自动清理残留请求
swift
//--------------- 核心实现 ---------------//
/**
* 获取锁
* @param resources 资源
* @param lockId 锁唯一标识
* @param timeout 是获取锁的最大等待时间
* @return 是否成功获取到锁
*/
private boolean tryLock(List<String> resources, String lockId, long timeout) {
long start = System.currentTimeMillis();
// 1. 加入所有资源的等待队列
resources.forEach(res -> enqueue(res, lockId));
log.info("已加入资源队列,资源:{},锁ID:{}", resources, lockId);
try {
while (System.currentTimeMillis() - start < timeout * 1000) {
// 2. 检查是否在所有资源队列头部
if (checkAndLock(resources, lockId)) {
return true;
}
TimeUnit.MILLISECONDS.sleep(100); // 轮询间隔
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
// 3. 超时后清理队列
if (!isLockAcquired(resources, lockId)) {
resources.forEach(res -> removeFromQueue(res, lockId));
}
}
return false;
}
/**
* 检查是否达到队列头部并获取锁
* @param resources 资源
* @param lockId 锁唯一标识
* @return 是否成功获取到锁
*/
private boolean checkAndLock(List<String> resources, String lockId) {
String luaScript =
"local all_ready = 1\n" +
"local queue_key_count = #KEYS / 2\n" +
"for i = 1, queue_key_count do\n" +
" local queue_key = KEYS[i]\n" +
" if redis.call('type', queue_key)['ok'] ~= 'list' then\n" +
" return -1\n" +
" end\n" +
" if redis.call('lindex', queue_key, 0) ~= ARGV[1] then\n" +
" all_ready = 0\n" +
" break\n" +
" end\n" +
"end\n\n" +
"if all_ready == 1 then\n" +
" for i = queue_key_count + 1, #KEYS do\n" +
" local lock_key = KEYS[i]\n" +
" redis.call('set', lock_key, ARGV[1], 'EX', ARGV[2], 'NX')\n" +
" if redis.call('get', lock_key) ~= ARGV[1] then\n" +
" return 0\n" +
" end\n" +
" end\n" +
" return 1\n" +
"else\n" +
" return 0\n" +
"end";
List<String> keys = new ArrayList<>();
// 队列Key
resources.forEach(res -> keys.add(queuePrefix + res));
// 锁Key
resources.forEach(res -> keys.add(lockPrefix + res));
DefaultRedisScript<Long> script = new DefaultRedisScript<>(luaScript, Long.class);
log.info("尝试获取锁,keys:{},lockId:{}",keys,lockId);
Long result = redisTemplate.execute(script, keys, lockId, lockTTL);
log.info("获取锁结果:{},keys:{},lockId:{}",result == 1L,keys,lockId);
return result == 1L;
}
/**
* 该组资源是否被lockId持有
* @param resources 资源
* @param lockId 锁唯一标识
* @return true:全部被lockId持有 false:不被lockId持有
*/
private boolean isLockAcquired(List<String> resources, String lockId) {
return resources.stream()
.allMatch(res -> lockId.equals(
redisTemplate.opsForValue().get(lockPrefix + res)
));
}
-
参数说明:
- KEYS[1..n]:队列Key列表
- KEYS[n+1..2n]:对应锁Key列表
- ARGV[1]:lockId
- ARGV[2]:TTL
4.解锁流程
- 验证锁归属后同时删除锁和出队
typescript
/**
* 释放锁、资源出队列
*/
public void unlock(String r1, String r2, String lockId) {
List<String> resources = getValidResources(r1, r2);
resources.forEach(res -> releaseLock(res, lockId));
resources.forEach(res -> removeFromQueue(res, lockId));
}
/**
* 释放锁
* @param resource 资源
* @param lockId 锁唯一标识
*/
private void releaseLock(String resource, String lockId) {
String luaScript =
"if redis.call('get', KEYS[1]) == ARGV[1] then\n" +
" redis.call('del', KEYS[1])\n" +
" redis.call('lpop', KEYS[2])\n" +
" return 1\n" +
"else\n" +
" return 0\n" +
"end";
List<String> keys = Arrays.asList(
lockPrefix + resource,
queuePrefix + resource
);
redisTemplate.execute(
new DefaultRedisScript<>(luaScript, Long.class),
keys,
lockId
);
}
/**
* 移除队列资源
* @param resource 资源
* @param lockId 锁唯一标识
*/
private void removeFromQueue(String resource, String lockId) {
redisTemplate.opsForList().remove(queuePrefix + resource, 0, lockId);
log.info("清理资源队列:{},lockId:{}",resource,lockId);
}
4.2.3 示例
typescript
@Slf4j
@Service
public class MallServiceImpl {
@Resource
private DynamicResourceLock lockUtil;
@Override
public Boolean deductInventory(String productId, String warehouseId, int quantity) {
log.info("库存扣减开始");
String requestId = null;
try {
requestId = lockUtil.tryLock(productId, warehouseId);
if (StringUtils.isNotEmpty(requestId)) {
throw new BizException(DefaultErrorCode.EXECUTE_FAIL,"库存扣减获取锁失败,请重试");
}
log.info("deductInventory-,库存扣减获取锁成功");
// 执行业务操作
...
return Boolean.TRUE;
} finally {
if (StringUtils.isNotEmpty(requestId)) {
lockUtil.unlock(productId, warehouseId,requestId);
log.info("deductInventory-库存扣减释放锁成功");
}
}
return Boolean.TRUE;
}
}
4.3技术亮点
4.3.1公平性保障
- 通过Redis List的FIFO特性实现天然排队
- 只有队列头部请求可获取锁
4.3.2原子性操作
- 使用Lua脚本将多个Redis命令打包为原子操作
- 避免竞态条件
4.3.3多资源支持
csharp
lock.tryLock("C123", "W456");
4.3.4容错设计
- 超时自动释放机制
typescript
private boolean tryLock(List<String> resources, String lockId, long timeout) {
long start = System.currentTimeMillis();
// 1. 加入所有资源的等待队列
resources.forEach(res -> enqueue(res, lockId));
log.info("已加入资源队列,资源:{},锁ID:{}", resources, lockId);
try {
while (System.currentTimeMillis() - start < timeout * 1000) {
// 2. 检查是否在所有资源队列头部
if (checkAndLock(resources, lockId)) {
return true;
}
TimeUnit.MILLISECONDS.sleep(100); // 轮询间隔
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
// 3. 超时后清理队列
if (!isLockAcquired(resources, lockId)) {
resources.forEach(res -> removeFromQueue(res, lockId));
}
}
return false;
}
private boolean isLockAcquired(List<String> resources, String lockId) {
return resources.stream()
.allMatch(res -> lockId.equals(
redisTemplate.opsForValue().get(lockPrefix + res)
));
}
- 锁TTL略大于业务操作时间
5. 团队介绍
「三翼鸟数字化技术平台-应用软件框架开发」主要负责设计工具的研发,包括营销设计工具、家电VR设计和展示、水电暖通前置设计能力,研发并沉淀素材库,构建家居家装素材库,集成户型库、全品类产品库、设计方案库、生产工艺模型,打造基于户型和风格的AI设计能力,快速生成算量和报价;同时研发了门店设计师中心和项目中心,包括设计师管理能力和项目经理管理能力。实现了场景全生命周期管理,同时为水,空气,厨房等产业提供商机管理工具,从而实现了以场景贯穿的B端C端全流程系统。