基于redis的多资源分布式公平锁的设计与实践

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端全流程系统。

相关推荐
今天没有盐1 小时前
Scala Map集合完全指南:从入门到实战应用
后端·scala·编程语言
LSTM971 小时前
如何使用 C# 将 RTF 转换为 PDF
后端
Jing_Rainbow1 小时前
【AI-7 全栈-2 /Lesson16(2025-11-01)】构建一个基于 AIGC 的 Logo 生成 Bot:从前端到后端的完整技术指南 🎨
前端·人工智能·后端
7***53341 小时前
Rust错误处理模式
开发语言·后端·rust
16_one2 小时前
autoDL安装Open-WebUi+Rag本地知识库问答+Function Calling
人工智能·后端·算法
StockPP2 小时前
印度尼西亚股票多时间框架K线数据可视化页面
前端·javascript·后端
h***34632 小时前
Redis安装教程(Windows版本)
数据库·windows·redis
3***g2052 小时前
如何使用Spring Boot框架整合Redis:超详细案例教程
spring boot·redis·后端
狂奔小菜鸡2 小时前
Day18 | 深入理解Object类
java·后端·java ee