一、一次重复扣款让我丢了工作
2017年,我负责的优惠券系统出了一个Bug:用户领券时没有加锁,导致同一个用户同时领取了多张券。
虽然代码里加了if判断"每个用户只能领一张",但并发情况下,两个请求同时通过了判断,都去扣款了。
后来查出来,被薅了10万张券,公司损失50万。
我被开除了。
从那以后,我对分布式锁有了深刻的理解:并发问题不是小问题,用不好就是钱和工作的代价。
二、分布式锁概述
2.1 为什么需要分布式锁
单机锁 vs 分布式锁:
单机环境:
– Java synchronized
– ReentrantLock
– 只能保证单个JVM内有效
分布式环境:
– 多个应用实例
– 多个数据库实例
– 多个缓存实例
– 需要跨JVM的锁机制
场景:
1. 库存扣减
2. 优惠券领取
3. 幂等控制
4. 任务调度
5. Leader选举
2.2 分布式锁特性
分布式锁必须满足:
1. 互斥性
– 同一时刻只能有一个客户端持有锁
2. 安全性
– 锁只能被持有它的客户端释放
3. 死锁避免
– 即使客户端崩溃,锁也要能自动释放
4. 容错性
– 集群部分节点故障时,锁服务仍可用
5. 性能
– 获取/释放锁的延迟要尽量小
三、Redis分布式锁
3.1 基础实现
/**
* Redis分布式锁
*/
@Service
@Slf4j
public class RedisLockService {
@Autowired
private StringRedisTemplate redisTemplate;
private static final String LOCK_PREFIX = "lock:";
private static final Duration DEFAULT_EXPIRE = Duration.ofSeconds(30);
/**
* 获取锁(基础版)
*/
public boolean lock(String key, String value) {
String lockKey = LOCK_PREFIX + key;
// SET key value NX EX 30
Boolean result = redisTemplate.opsForValue()
.setIfAbsent(lockKey, value, DEFAULT_EXPIRE);
return Boolean.TRUE.equals(result);
}
/**
* 释放锁
*/
public void unlock(String key, String value) {
String lockKey = LOCK_PREFIX + key;
// Lua脚本:只有value相等才删除
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),
value
);
}
/**
* 获取锁(带重试)
*/
public boolean lockWithRetry(String key, String value, int maxRetries, long waitMs) {
for (int i = 0; i < maxRetries; i++) {
if (lock(key, value)) {
return true;
}
try {
Thread.sleep(waitMs);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
return false;
}
}
3.2 Redisson实现
/**
* Redisson分布式锁
*/
@Service
@Slf4j
public class RedissonLockService {
@Autowired
private RedissonClient redissonClient;
/**
* 获取锁
*/
public <T> T executeWithLock(String key, int waitTime, int leaseTime,
Callable<T> callable) {
RLock lock = redissonClient.getLock(key);
try {
// 尝试获取锁
boolean acquired = lock.tryLock(waitTime, leaseTime, TimeUnit.SECONDS);
if (!acquired) {
throw new LockException("获取锁失败: " + key);
}
// 执行业务逻辑
return callable.call();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new LockException("获取锁被中断", e);
} catch (Exception e) {
throw new LockException("业务执行失败", e);
} finally {
// 释放锁
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
/**
* 库存扣减(使用分布式锁)
*/
public boolean deductStock(Long productId, Integer quantity) {
String lockKey = "stock:lock:" + productId;
return executeWithLock(lockKey, 10, 30, () -> {
// 查询当前库存
Integer stock = getStock(productId);
if (stock < quantity) {
throw new BusinessException("库存不足");
}
// 扣减库存
int newStock = stock – quantity;
updateStock(productId, newStock);
log.info("库存扣减成功: productId={}, quantity={}, newStock={}",
productId, quantity, newStock);
return true;
});
}
}
3.3 Redlock算法
/**
* Redlock实现(多Redis实例)
*/
@Service
@Slf4j
public class RedlockService {
private List<RedisClient> redisClients;
/**
* Redlock获取锁
*/
public boolean lockRedlock(String resource, String value,
int ttl, int retryTimes) {
int n = redisClients.size();
int successCount = 0;
List<Long> acquisitionTimes = new ArrayList<>();
// 向N个Redis实例获取锁
for (RedisClient client : redisClients) {
long startTime = System.currentTimeMillis();
try {
if (client.tryLock(resource, value, ttl)) {
successCount++;
acquisitionTimes.add(System.currentTimeMillis() – startTime);
}
} catch (Exception e) {
log.warn("获取锁失败: {}", client, e);
}
}
// 计算锁的有效时间
long minAcquisitionTime = acquisitionTimes.stream()
.mapToLong(Long::longValue)
.min()
.orElse(0L);
// 超过半数成功
if (successCount > n / 2) {
// 锁的有效时间 = TTL – 平均获取时间
long validTime = ttl – (minAcquisitionTime / 2);
log.info("Redlock获取成功: resource={}, validTime={}ms", resource, validTime);
return true;
}
// 释放所有获取到的锁
for (RedisClient client : redisClients) {
try {
client.unlock(resource, value);
} catch (Exception e) {
log.warn("释放锁失败: {}", client, e);
}
}
log.warn("Redlock获取失败: resource={}, success={}/{}", resource, successCount, n);
return false;
}
}
四、ZooKeeper分布式锁
4.1 原理
ZooKeeper分布式锁原理:
1. 临时顺序节点
– 每个获取锁的客户端创建一个临时顺序节点
– 节点路径:/lock/guid-XXXXX
2. 判断最小
– 检查自己的节点是否是序号最小的
– 如果是,获取锁
3. 监听前一个节点
– 如果不是最小,监听前一个节点的删除事件
– 当前一个节点删除时,重新判断
4. 释放锁
– 删除自己的临时节点
– 下一个节点变为最小,获得锁
4.2 实现
/**
* ZK分布式锁
*/
@Service
@Slf4j
public class ZkLockService {
@Autowired
private CuratorFramework curatorFramework;
private static final String LOCK_PATH = "/locks";
/**
* 获取锁
*/
public String lock(String lockName, int waitTime, int leaseTime)
throws Exception {
// 创建锁根节点
createRootIfNotExists();
// 创建临时顺序节点
String lockPath = curatorFramework.create()
.creatingParentsIfNeeded()
.withMode(CreateMode.EPHEMERAL_SEQUENTIAL)
.forPath(LOCK_PATH + "/" + lockName + "-");
log.info("创建锁节点: {}", lockPath);
// 尝试获取锁
return tryAcquireLock(lockPath, waitTime, leaseTime);
}
/**
* 尝试获取锁
*/
private String tryAcquireLock(String lockPath, int waitTime, int leaseTime)
throws Exception {
long startTime = System.currentTimeMillis();
while (true) {
// 获取所有子节点
List<String> children = curatorFramework.getChildren()
.forPath(LOCK_PATH);
// 排序
Collections.sort(children);
// 获取当前节点序号
String currentNode = lockPath.substring(lockPath.lastIndexOf("/") + 1);
// 判断是否是第一个
if (children.get(0).equals(currentNode)) {
log.info("获取锁成功: {}", lockPath);
return lockPath;
}
// 监听前一个节点
String prevNode = children.get(children.indexOf(currentNode) – 1);
String prevPath = LOCK_PATH + "/" + prevNode;
// 设置监听
CountDownLatch latch = new CountDownLatch(1);
CuratorWatcher watcher = event -> {
if (EventType.NodeDeleted.equals(event.getType())) {
latch.countDown();
}
};
curatorFramework.getData().usingWatcher(watcher).forPath(prevPath);
// 等待被唤醒或超时
if (latch.await(waitTime, TimeUnit.MILLISECONDS)) {
// 被唤醒,重新尝试
continue;
} else {
// 超时,获取锁失败
throw new LockException("获取锁超时: " + lockPath);
}
}
}
/**
* 释放锁
*/
public void unlock(String lockPath) {
try {
curatorFramework.delete().forPath(lockPath);
log.info("释放锁成功: {}", lockPath);
} catch (Exception e) {
log.error("释放锁失败: {}", lockPath, e);
}
}
}
五、Redis vs ZooKeeper对比
5.1 对比表
┌─────────────────────────────────────────────────────────────────┐
│ Redis vs ZooKeeper 对比 │
│ │
│ 指标 │ Redis │ ZooKeeper │
│ ───────────────────────────────────────────────────────────── │
│ 性能 │ 高(单线程) │ 中(ZAB协议) │
│ 可靠性 │ 中(主从) │ 高(ZAB + 多数派) │
│ 复杂度 │ 低 │ 中 │
│ 支持锁类型 │ 互斥锁、读写锁 │ 互斥锁(需扩展) │
│ 故障恢复 │ 依赖TTL过期 │ 临时节点自动删除 │
│ 脑裂问题 │ 存在(Redlock解决) │ 不存在 │
│ 适用场景 │ 性能要求高 │ 可靠性要求高 │
│ 运维成本 │ 低 │ 中 │
│ │
└──────────────────────────────────────────────────────────────────┘
5.2 选型建议
/**
* 分布式锁选型决策
*/
public class LockSelection {
/**
* 根据场景选择合适的锁
*/
public static String selectLockType(LockScenario scenario) {
// 场景1:库存扣减(高频、对性能要求高)
if (scenario == LockScenario.STOCK_DEDUCT) {
// 推荐:Redis
return "Redis";
}
// 场景2:Leader选举(可靠性要求高)
if (scenario == LockScenario.LEADER_ELECTION) {
// 推荐:ZooKeeper
return "ZooKeeper";
}
// 场景3:分布式事务(需要协调者)
if (scenario == LockScenario.TRANSACTION) {
// 推荐:ZooKeeper
return "ZooKeeper";
}
// 场景4:防重复提交(性能要求高)
if (scenario == LockScenario.IDEMPOTENT) {
// 推荐:Redis
return "Redis";
}
// 默认:Redis(通用)
return "Redis";
}
}
六、实战案例
6.1 优惠券领取
/**
* 优惠券服务
*/
@Service
@Slf4j
public class CouponService {
@Autowired
private RedisLockService redisLockService;
/**
* 领取优惠券(使用分布式锁)
*/
public void receiveCoupon(Long userId, Long couponId) {
String lockKey = "coupon:lock:" + couponId + ":" + userId;
String lockValue = UUID.randomUUID().toString();
try {
// 获取分布式锁
boolean acquired = redisLockService.lockWithRetry(
lockKey, lockValue, 3, 100);
if (!acquired) {
throw new BusinessException("系统繁忙,请稍后重试");
}
// 检查是否已领取
if (hasReceived(userId, couponId)) {
throw new BusinessException("您已领取过该优惠券");
}
// 扣减库存
boolean deducted = couponStockService.deductStock(couponId, 1);
if (!deducted) {
throw new BusinessException("优惠券已领完");
}
// 记录领取
saveReceiveRecord(userId, couponId);
log.info("优惠券领取成功: userId={}, couponId={}", userId, couponId);
} finally {
// 释放锁
redisLockService.unlock(lockKey, lockValue);
}
}
}
6.2 订单创建防重
/**
* 订单服务
*/
@Service
@Slf4j
public class OrderService {
@Autowired
private RedissonLockService redissonLockService;
/**
* 创建订单(分布式锁防重)
*/
public Order createOrder(CreateOrderRequest request) {
String lockKey = "order:create:" + request.getUserId();
return redissonLockService.executeWithLock(
lockKey, 10, 30, () -> {
// 检查是否有未支付的订单
if (hasUnpaidOrder(request.getUserId())) {
throw new BusinessException("您有待支付订单,请先处理");
}
// 检查商品库存
checkInventory(request.getItems());
// 创建订单
Order order = doCreateOrder(request);
// 锁定库存
lockInventory(order.getItems());
return order;
}
);
}
}
七、踩坑实录
坑1:锁过期了但业务没完成
业务执行时间太长,锁自动过期了,被其他线程获取了锁。
解决:使用watchdog自动续期,或预估业务执行时间设置合理的TTL。
坑2:非原子性释放锁
判断和删除不是原子操作,导致释放了别人的锁。
解决:使用Lua脚本保证原子性。
坑3:Redis主从切换丢锁
主库宕机,从库升级为主库,锁丢失了。
解决:使用Redlock,或使用ZooKeeper。
坑4:大量客户端竞争
锁竞争激烈,大部分客户端都在等待。
解决:使用分段锁,或改用无锁设计。
坑5:运维复杂
需要维护Redis集群或ZooKeeper集群,增加了运维成本。
解决:评估是否真的需要分布式锁,或使用云服务。
八、总结
分布式锁是分布式系统的基石:
- Redis锁:性能高,但可靠性稍差
- ZooKeeper锁:可靠性高,但性能稍差
- Redlock:多Redis实例提高可靠性
最佳实践:
血的教训:
分布式锁用对了是神器,用错了是灾难。用之前想清楚:真的需要锁吗?有没有更好的无锁方案?
思考题: 你的系统用了哪种分布式锁?有没有遇到过问题?
个人观点,仅供参考



