欢迎光临
我们一直在努力

【架构实战】分布式锁实现:Redis还是ZooKeeper?

一、一次重复扣款让我丢了工作

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实例提高可靠性

最佳实践:

  • 预估业务执行时间,设置合理的锁过期时间
  • 释放锁时使用Lua脚本保证原子性
  • 高并发场景考虑分段锁
  • 做好监控和告警
  • 血的教训:

    分布式锁用对了是神器,用错了是灾难。用之前想清楚:真的需要锁吗?有没有更好的无锁方案?

    思考题: 你的系统用了哪种分布式锁?有没有遇到过问题?


    个人观点,仅供参考

    赞(0)
    未经允许不得转载:171主机测评 » 【架构实战】分布式锁实现:Redis还是ZooKeeper?
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址