引言
在分布式系统中,我们经常需要对共享资源进行互斥访问。比如:
- 防止商品超卖
- 避免重复下单
- 确保任务只被处理一次
- 保护共享资源不被并发修改
这就需要一个分布式锁机制。与单机环境下的线程锁不同,分布式锁需要在多个服务实例间生效,这带来了新的挑战。
图片
分布式锁的核心要求
一个可靠的分布式锁必须满足以下要求:
- 互斥性
在任意时刻,只能有一个客户端持有锁
不能出现多个客户端同时持有锁的情况
- 可重入性
同一个客户端可以多次获取同一把锁
需要维护锁的重入计数
- 防死锁
客户端崩溃时,锁必须能自动释放
锁必须有过期机制
- 高可用
锁服务不能成为系统瓶颈
锁服务必须保证高可用
基于数据库的实现
图片
1. 悲观锁实现
最简单的方式是利用数据库的行锁:
-- 创建锁表
CREATE TABLE distributed_lock (
lock_key VARCHAR(50) PRIMARY KEY,
lock_value VARCHAR(50),
version INT,
expire_time TIMESTAMP
);
-- 获取锁
SELECT * FROM distributed_lock
WHERE lock_key = 'order_lock'
FOR UPDATE;
Java 实现示例:
@Service
public class DatabaseDistributedLock {
@Autowired
private JdbcTemplate jdbcTemplate;
@Transactional
public boolean acquireLock(String lockKey, String lockValue, long expireSeconds) {
try {
// 使用 FOR UPDATE 加锁查询
String sql = "SELECT * FROM distributed_lock " +
"WHERE lock_key = ? FOR UPDATE";
List<Map<String, Object>> result = jdbcTemplate.queryForList(
sql, lockKey
);
if (result.isEmpty()) {
// 锁不存在,创建锁
jdbcTemplate.update(
"INSERT INTO distributed_lock " +
"(lock_key, lock_value, version, expire_time) " +
"VALUES (?, ?, 1, ?)",
lockKey,
lockValue,
LocalDateTime.now().plusSeconds(expireSeconds)
);
return true;
}
// 检查锁是否过期
Map<String, Object> lock = result.get(0);
LocalDateTime expireTime = ((Timestamp) lock.get("expire_time"))
.toLocalDateTime();
if (expireTime.isBefore(LocalDateTime.now())) {
// 锁已过期,更新锁
jdbcTemplate.update(
"UPDATE distributed_lock " +
"SET lock_value = ?, version = version + 1, expire_time = ? " +
"WHERE lock_key = ?",
lockValue,
LocalDateTime.now().plusSeconds(expireSeconds),
lockKey
);
return true;
}
return false;
} catch (Exception e) {
return false;
}
}
}
2. 乐观锁实现
使用版本号实现乐观锁:
@Service
public class OptimisticLock {
@Autowired
private JdbcTemplate jdbcTemplate;
public boolean acquireLock(String lockKey, String lockValue, int version) {
int updated = jdbcTemplate.update(
"UPDATE distributed_lock " +
"SET lock_value = ?, version = version + 1 " +
"WHERE lock_key = ? AND version = ?",
lockValue,
lockKey,
version
);
return updated > 0;
}
}
数据库实现的优缺点:
- 优点:
实现简单
容易理解
不需要额外组件
- 缺点:
性能较差
数据库压力大
无法优雅处理锁超时
基于 Redis 的实现
1. 单节点实现
使用 Redis 的 SETNX 命令:
@Service
public class RedisDistributedLock {
@Autowired
private StringRedisTemplate redisTemplate;
public boolean acquireLock(String lockKey, String lockValue, long expireSeconds) {
return redisTemplate.opsForValue()
.setIfAbsent(lockKey, lockValue, expireSeconds, TimeUnit.SECONDS);
}
public boolean releaseLock(String lockKey, String lockValue) {
// 使用 Lua 脚本确保原子性
String script =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) " +
"else " +
"return 0 " +
"end";
return redisTemplate.execute(
new DefaultRedisScript<>(script, Boolean.class),
Collections.singletonList(lockKey),
lockValue
);
}
}
2. RedLock 算法
在 Redis 集群环境下,使用 RedLock 算法:
public class RedLock {
private final List<StringRedisTemplate> redisList;
private final int quorum; // 大多数节点数
public boolean acquireLock(String lockKey, String lockValue, long expireMillis) {
int acquiredLocks = 0;
long startTime = System.currentTimeMillis();
// 尝试在每个节点上获取锁
for (StringRedisTemplate redis : redisList) {
if (tryAcquireLock(redis, lockKey, lockValue, expireMillis)) {
acquiredLocks++;
}
}
// 计算获取锁消耗的时间
long elapsedTime = System.currentTimeMillis() - startTime;
long remainingTime = expireMillis - elapsedTime;
// 判断是否获取到足够的锁
if (acquiredLocks >= quorum && remainingTime > 0) {
return true;
} else {
// 释放所有获取的锁
releaseLocks(lockKey, lockValue);
return false;
}
}
private boolean tryAcquireLock(
StringRedisTemplate redis,
String lockKey,
String lockValue,
long expireMillis
) {
try {
return redis.opsForValue()
.setIfAbsent(lockKey, lockValue, expireMillis, TimeUnit.MILLISECONDS);
} catch (Exception e) {
return false;
}
}
}
Redis 实现的优缺点:
- 优点:
性能高
实现相对简单
支持自动过期
- 缺点:
需要额外维护 Redis 集群
RedLock 算法实现复杂
时钟依赖问题
基于 ZooKeeper 的实现
图片
利用 ZooKeeper 的临时节点机制:
public class ZookeeperDistributedLock {
private final CuratorFramework client;
private final String lockPath;
public boolean acquireLock(String lockKey) throws Exception {
// 创建临时节点
String path = lockPath + "/" + lockKey;
try {
client.create()
.creatingParentsIfNeeded()
.withMode(CreateMode.EPHEMERAL)
.forPath(path);
return true;
} catch (NodeExistsException e) {
return false;
}
}
public void releaseLock(String lockKey) throws Exception {
String path = lockPath + "/" + lockKey;
client.delete().forPath(path);
}
// 实现可重入锁
public class ReentrantZookeeperLock {
private final ThreadLocal<Integer> lockCount = new ThreadLocal<>();
public boolean acquire() throws Exception {
Integer count = lockCount.get();
if (count != null && count > 0) {
// 入
lockCount.set(count + 1);
return true;
}
if (acquireLock("lock")) {
lockCount.set(1);
return true;
}
return false;
}
public void release() throws Exception {
Integer count = lockCount.get();
if (count == null) {
return;
}
count--;
if (count > 0) {
lockCount.set(count);
} else {
lockCount.remove();
releaseLock("lock");
}
}
}
}
ZooKeeper 实现的优缺点:
- 优点:
可靠性高
自动释放锁
支持监听机制
- 缺点:
性能一般
实现复杂
需要维护 ZooKeeper 集群
业务场景分析
1. 秒杀场景
场景特点:
- 并发量极高
- 时间窗口集中
- 对性能要求极高
- 数据一致性要求高
推荐方案: Redis + Lua脚本
@Service
public class SeckillLockService {
@Autowired
private StringRedisTemplate redisTemplate;
public boolean trySecKill(String productId, String userId) {
// Lua脚本保证原子性
String script =
"if redis.call('exists', KEYS[1]) == 0 then " +
" redis.call('set', KEYS[1], ARGV[1]) " +
" redis.call('decrby', KEYS[2], 1) " +
" return 1 " +
"end " +
"return 0";
List<String> keys = Arrays.asList(
"seckill:lock:" + productId + ":" + userId,
"seckill:stock:" + productId
);
return redisTemplate.execute(
new DefaultRedisScript<>(script, Boolean.class),
keys,
"1"
);
}
}
原因分析:
- Redis 的高性能满足并发要求
- Lua 脚本保证原子性
- 内存操作速度快
- 集群方案保证可用性
2. 定时任务场景
场景特点:
- 多实例部署
- 任务不能重复执行
- 故障转移需求
- 实时性要求不高
推荐方案: ZooKeeper
public class ScheduledTaskLock {
private final CuratorFramework client;
public void executeTask() {
String taskPath = "/scheduled-tasks/daily-report";
try {
// 创建临时节点
client.create()
.creatingParentsIfNeeded()
.withMode(CreateMode.EPHEMERAL)
.forPath(taskPath);
try {
// 执行任务
generateDailyReport();
} finally {
// 删除节点
client.delete().forPath(taskPath);
}
} catch (NodeExistsException e) {
// 其他实例正在执行
log.info("Task is running on other instance");
}
}
}
原因分析:
- ZooKeeper 的临时节点特性保证故障时自动释放锁
- 强一致性保证任务不会重复执行
- Watch 机制便于监控任务执行状态
3. 订单支付场景
场景特点:
- 并发量适中
- 数据一致性要求高
- 需要事务支持
- 有业务回滚需求
推荐方案: 数据库行锁 + 事务
@Service
public class PaymentLockService {
@Autowired
private JdbcTemplate jdbcTemplate;
@Transactional
public boolean processPayment(String orderId, BigDecimal amount) {
// 使用 FOR UPDATE 锁定订单记录
String sql = "SELECT * FROM orders WHERE order_id = ? FOR UPDATE";
Map<String, Object> order = jdbcTemplate.queryForMap(sql, orderId);
// 检查订单状态
if (!"PENDING".equals(order.get("status"))) {
return false;
}
// 执行支付逻辑
jdbcTemplate.update(
"UPDATE orders SET status = 'PAID' WHERE order_id = ?",
orderId
);
// 记录支付流水
jdbcTemplate.update(
"INSERT INTO payment_log (order_id, amount) VALUES (?, ?)",
orderId, amount
);
return true;
}
}
原因分析:
- 数据库事务保证数据一致性
- 行锁防止并发支付
- 便于与其他业务集成
- 支持事务回滚
4. 库存扣减场景
场景特点:
- 并发量较高
- 需要预占库存
- 需要处理超时释放
- 对性能要求较高
推荐方案: Redis + 延时队列
@Service
public class InventoryLockService {
@Autowired
private StringRedisTemplate redisTemplate;
public boolean lockInventory(String productId, int quantity, String orderId) {
// 加锁并预占库存
String script =
"local stock = redis.call('get', KEYS[1]) " +
"if stock and tonumber(stock) >= tonumber(ARGV[1]) then " +
" redis.call('decrby', KEYS[1], ARGV[1]) " +
" redis.call('setex', KEYS[2], 1800, ARGV[1]) " +
" return 1 " +
"end " +
"return 0";
List<String> keys = Arrays.asList(
"inventory:" + productId,
"inventory:lock:" + orderId
);
boolean locked = redisTemplate.execute(
new DefaultRedisScript<>(script, Boolean.class),
keys,
String.valueOf(quantity)
);
if (locked) {
// 添加延时释放任务
redisTemplate.opsForZSet().add(
"inventory:timeout",
orderId,
System.currentTimeMillis() + 1800000
);
}
return locked;
}
}
原因分析:
- Redis 的高性能满足并发要求
- 延时队列处理超时释放
- 原子操作保证数据一致性
- 便于扩展和监控
实现方案对比
特性 | 数据库 | Redis | ZooKeeper |
性能 | 低 | 高 | 中 |
可靠性 | 高 | 中 | 高 |
实现复杂度 | 低 | 中 | 高 |
维护成本 | 低 | 中 | 高 |
自动释放 | 需要额外实现 | 支持 | 支持 |
可重入性 | 需要额外实现 | 需要额外实现 | 需要额外实现 |
最佳实践
- 选择建议
简单场景:使用数据库实现
高性能要求:使用 Redis 实现
高可靠要求:使用 ZooKeeper 实现
- 实现建议
设置合理的超时时间
实现可重入机制
添加监控和告警
做好日志记录
- 使用建议
缩小锁的粒度
减少锁的持有时间
避免死锁
做好异常处理
结论
分布式锁是分布式系统中的一个基础组件,选择合适的实现方案需要考虑:
- 性能要求
- 可靠性要求
- 开发维护成本
- 团队技术栈
没有最好的方案,只有最合适的方案。在实际应用中,要根据具体场景选择合适的实现方式。
正文内容从这里开始(可直接省略,亦可配图说明)。