🛒 从超市储物柜到分布式锁:原来核心技术就在身边!

52 阅读5分钟

当3000件商品在1秒内被超卖,当支付系统因并发扣款损失百万,当你的系统日志频繁出现诡异的数据覆盖——这都可能源于错误的分布式锁实现!本文将用超市储物柜的日常场景,揭秘分布式锁的6大隐藏陷阱与4大核心生存法则,带你从表象直击本质

一、深入生活场景:超市储物柜的智慧密码

1.1 存包全流程拆解(对照分布式锁)

当您使用超市储物柜时,其实正在参与一个精妙的分布式系统:

sequenceDiagram
    顾客->>储物柜: 1. 点击"存包"(SETNX检查)
    储物柜->>顾客: 2. 分配空闲柜号(返回锁Key)
    顾客->>储物柜: 3. 放入物品(业务操作)
    顾客->>储物柜: 4. 关闭柜门(锁获取成功)
    顾客->>打印机: 5. 获取条码(锁令牌)
    储物柜->>计时器: 6. 启动30分钟倒计时(锁超时)
    计时器->>储物柜: 7. 时间到自动弹开(锁释放)

技术映射清单

  • 每个柜子 = 一个锁资源
  • 条码 = 客户端唯一标识
  • 倒计时 = 锁超时机制
  • 多个柜组 = 分布式集群

二、分布式锁四大要素的终极解析

2.1 互斥性:你的包裹专属保险箱

经典错误案例:

// 危险的取包方式 - 类似直接删除Redis Key
public void 取包危险版(String 柜号) {
    储物柜.物理破坏门锁(); // 任何人都能拿走包裹!
}
正确实现方案:

public class 安全储物柜 {
    private Map<String, String> 柜号到条码 = new ConcurrentHashMap<>();
    
    public boolean 存包(String 柜号, String 条码) {
        synchronized(this) {
            if (!柜号到条码.containsKey(柜号)) {
                柜号到条码.put(柜号, 条码);
                new Timer().schedule(new 超时任务(柜号), 30*60*1000);
                return true;
            }
            return false;
        }
    }
    
    public void 安全取包(String 柜号, String 条码) {
        if (柜号到条码.getOrDefault(柜号, "").equals(条码)) {
            柜号到条码.remove(柜号);
            触发开柜(柜号);
        }
    }
}

2.2 容错性:连锁超市的生存法则

多柜组容灾策略: title 储物柜集群状态

  • "正常柜组" : 3
  • "故障柜组" : 1
  • "维护柜组" : 0 Redlock算法实操:
public class 连锁超市存包 {
    private List<储物柜组> 所有柜组 = Arrays.asList(新柜组A(), 新柜组B(), 新柜组C());
    
    public boolean 安全存包(物品 item) {
        int 成功数 = 0;
        long 开始时间 = System.currentTimeMillis();
        
        for (储物柜组 柜组 : 所有柜组) {
            if (柜组.尝试存包(item)) {
                成功数++;
            }
        }
        
        long 耗时 = System.currentTimeMillis() - 开始时间;
        boolean 超时未发生 = 耗时 < 30000; // 锁默认30秒超时
        
        return 成功数 >= 2 && 超时未发生;
    }
}

2.3 活性保障:智能续费背后的黑科技

看门狗机制全流程

  1. 初始锁定:设置30分钟有效期
  2. 启动守护线程:每隔10分钟检查
  3. 自动续期条件:
  4. 物品仍存在柜中(业务未完成)
  5. 剩余时间 < 1/3有效期
  6. 续期操作:重新设置为30分钟
public class 智能储物柜看门狗 {
    private ScheduledExecutorService 定时器 = Executors.newScheduledThreadPool(1);
    
    public void 开始监控(String 柜号, String 条码) {
        定时器.scheduleAtFixedRate(() -> {
            if (系统.检查物品存在(柜号) && 剩余时间(柜号) < 10分钟) {
                系统.续费柜号(柜号, 条码);
            }
        }, 0, 10, TimeUnit.MINUTES);
    }
}

2.4 可重入性:临时取物的正确姿势

场景模拟

  1. 存入笔记本电脑
  2. 临时取出电源适配器
  3. 再次放入并继续使用

代码实现

public class 可重入储物柜 {
    private Map<String, Integer> 使用次数 = new ConcurrentHashMap<>();
    
    public void 临时取物(String 柜号, String 条码) {
        synchronized(this) {
            if (验证条码(柜号, 条码)) {
                int 次数 = 使用次数.getOrDefault(柜号, 0);
                使用次数.put(柜号, 次数 + 1);
            }
        }
    }
    
    public void 完成存取(String 柜号) {
        synchronized(this) {
            int 次数 = 使用次数.getOrDefault(柜号, 0);
            if (次数 > 0) {
                使用次数.put(柜号, 次数 - 1);
            }
            if (次数 == 1) {
                标记为空闲(柜号);
            }
        }
    }
}

三、Redis锁实现全流程(含异常处理)

3.1 完整生命周期演示

    客户端->>Redis: SET lock:柜号1 uuid123 NX EX 300
    Redis-->>客户端: OK(获锁成功)
    客户端->>业务系统: 执行存包操作
    业务系统->>数据库: 记录存取日志
    客户端->>Redis: EVAL解锁脚本
    Redis-->>客户端: 1(删除成功)
    alt 网络抖动
        客户端->>Redis: 未收到响应
        客户端->>客户端: 重试机制(最多3次)
    end

3.2 增强版代码实现

public class 增强版储物柜锁 {
    private JedisPool jedisPool;
    private String clientId = UUID.randomUUID().toString();
    
    public boolean 存包(String 柜号, int 超时秒) {
        try (Jedis jedis = jedisPool.getResource()) {
            String result = jedis.set(柜号, clientId, 
                SetParams.setParams().nx().ex(超时秒));
            return "OK".equals(result);
        }
    }
    
    public boolean 取包(String 柜号) {
        String script = 
            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
            "   return redis.call('del', KEYS[1]) " +
            "else " +
            "   return 0 " +
            "end";
        
        try (Jedis jedis = jedisPool.getResource()) {
            Object result = jedis.eval(script, 
                Collections.singletonList(柜号), 
                Collections.singletonList(clientId));
            return (Long)result == 1L;
        }
    }
    
    public boolean 安全存包(String 柜号, Runnable 业务操作) {
        if (!存包(柜号, 30)) return false;
        try {
            业务操作.run();
            return true;
        } finally {
            取包(柜号);
        }
    }
  }

四、故障应对手册(含真实监控指标)

4.1 常见故障处理清单

image.png 4.2 监控看板关键指标

public class 储物柜监控 {
    @GetMapping("/metrics")
    public Map<String, Object> 获取监控数据() {
        return Map.of(
            "当前使用柜数", redis.scard("active_locks"),
            "平均锁定时间", calculateAvgLockTime(),
            "锁冲突率", calculateLockContentionRate(),
            "集群健康度", checkClusterHealth(),
            "异常解锁次数", getAbnormalUnlockCount()
        );
    }
}

五、实战压力测试方案

测试场景设计: 模拟双十一抢购场景

wrk -t20 -c100 -d300s --script=存包压力测试.lua

测试脚本核心逻辑

-- 存包压力测试.lua
request = function()
    local 柜号 = math.random(1, 1000)
    local 条码 = uuid()
    
    -- 1. 获取锁
    local resp = wrk.format("PUT", "/lock", {["柜号"]=柜号}, 条码)
    local res = wrk.execute(resp)
    
    if res.status == 200 then
        -- 2. 执行存包操作
        wrk.execute(wrk.format("POST", "/store", 包裹数据))
        
        -- 3. 释放锁
        wrk.execute(wrk.format("DELETE", "/unlock", {["柜号"]=柜号}, 条码))
    end
end

请立即采取行动

  • 点赞收藏,构建个人知识库
  • 分享到技术群,帮助团队避坑