【分布式锁】它是什么?怎么用?
为什么需要分布式锁?主要功能是什么?核心目的是什么?
为什么需要分布式锁?
在分布式系统中,多个服务实例需要访问共享资源(如数据库、缓存、文件等)。如果没有协调机制,并发操作会导致:
- 数据不一致:多个节点同时修改同一数据
- 重复处理:多个节点执行相同任务(如重复扣款)
- 资源竞争:如超卖问题(库存被多个请求同时扣减)
主要功能
- 互斥性(Mutual Exclusion):同一时刻只有一个客户端能持有锁
- 避免死锁(Deadlock Prevention):锁必须有超时机制
- 容错性(Fault Tolerance):即使持有锁的客户端崩溃,锁也能自动释放
- 高可用(High Availability):锁服务本身需保证高可用
核心目的
确保分布式环境下对共享资源的原子性访问,防止并发操作导致的数据不一致问题。
分布式锁实现方式及代码示例(含详细注释和使用案例)
方式1:基于Redis实现
原理:利用Redis的原子操作(SETNX + EXPIRE)
Maven依赖 (pom.xml
)
<!-- Redis客户端依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
配置文件 (application.yml
)
spring:
redis:
host: localhost # Redis服务器地址
port: 6379 # Redis端口
password: # 密码(没有则留空)
timeout: 5000ms # 连接超时时间
Java代码实现(含详细注释)
@Component
public class RedisDistributedLock {
@Autowired
private StringRedisTemplate redisTemplate; // Spring提供的Redis操作模板
/**
* 尝试获取分布式锁
* @param lockKey 锁的key
* @param clientId 客户端唯一标识(可使用UUID)
* @param expireSeconds 锁过期时间(秒)
* @return true-获取成功,false-获取失败
*/
public boolean tryLock(String lockKey, String clientId, long expireSeconds) {
// 使用SET key value NX EX命令保证原子性:
// NX: 仅当key不存在时设置
// EX: 设置过期时间(秒)
return redisTemplate.opsForValue().setIfAbsent(
lockKey,
clientId,
Duration.ofSeconds(expireSeconds) // 设置过期时间
);
}
/**
* 释放分布式锁
* @param lockKey 锁的key
* @param clientId 客户端唯一标识
* @return true-释放成功,false-释放失败
*/
public boolean unlock(String lockKey, String clientId) {
// 使用Lua脚本保证检查锁和删除锁的原子性
String script =
// 检查锁是否属于当前客户端
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " + // 属于则删除
"else " +
" return 0 " + // 不属于返回0
"end";
// 创建Redis脚本对象
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
// 执行脚本
Long result = redisTemplate.execute(
redisScript,
Collections.singletonList(lockKey), // KEYS[1]
clientId // ARGV[1]
);
// 结果1表示删除成功,0表示失败
return result != null && result == 1;
}
}
使用案例:商品库存扣减
@Service
public class ProductService {
@Autowired
private RedisDistributedLock redisLock;
@Autowired
private ProductRepository productRepo;
private static final String STOCK_LOCK_PREFIX = "lock:product:stock:";
/**
* 扣减商品库存(使用Redis分布式锁)
* @param productId 商品ID
* @param quantity 扣减数量
* @return 扣减结果
*/
public boolean reduceStock(Long productId, int quantity) {
String lockKey = STOCK_LOCK_PREFIX + productId;
String clientId = UUID.randomUUID().toString(); // 唯一客户端标识
boolean locked = false;
try {
// 尝试获取锁(等待1秒,锁有效期10秒)
locked = redisLock.tryLock(lockKey, clientId, 10);
if (!locked) {
throw new RuntimeException("获取分布式锁失败");
}
// 查询商品库存
Product product = productRepo.findById(productId)
.orElseThrow(() -> new RuntimeException("商品不存在"));
// 检查库存是否充足
if (product.getStock() < quantity) {
throw new RuntimeException("库存不足");
}
// 扣减库存
product.setStock(product.getStock() - quantity);
productRepo.save(product);
return true;
} finally {
// 释放锁
if (locked) {
redisLock.unlock(lockKey, clientId);
}
}
}
}
方式2:基于ZooKeeper实现
原理:利用临时顺序节点(Ephemeral Sequential Node)
Maven依赖 (pom.xml
)
<!-- ZooKeeper客户端Curator框架 -->
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>5.1.0</version>
</dependency>
Java代码实现(含详细注释)
public class ZookeeperDistributedLock {
private CuratorFramework client; // ZooKeeper客户端
private InterProcessMutex lock; // 分布式锁对象
/**
* 构造函数(创建ZooKeeper连接)
* @param connectString ZooKeeper连接地址
*/
public ZookeeperDistributedLock(String connectString) {
// 重试策略:初始间隔1秒,最多重试3次
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
// 创建客户端
client = CuratorFrameworkFactory.newClient(
connectString,
retryPolicy
);
client.start(); // 启动客户端
}
/**
* 尝试获取锁
* @param lockPath 锁路径(如:/locks/order)
* @param waitTime 等待时间
* @param unit 时间单位
* @return true-获取成功,false-获取失败
*/
public boolean tryLock(String lockPath, long waitTime, TimeUnit unit) throws Exception {
// 创建互斥锁(底层使用临时顺序节点)
lock = new InterProcessMutex(client, lockPath);
// 尝试获取锁,设置等待时间
return lock.acquire(waitTime, unit);
}
/**
* 释放锁
*/
public void unlock() throws Exception {
// 检查当前进程是否持有锁
if (lock != null && lock.isAcquiredInThisProcess()) {
lock.release(); // 释放锁(自动删除临时节点)
}
}
}
使用案例:配置中心配置更新
@Service
public class ConfigService {
private final ZookeeperDistributedLock zkLock;
private static final String CONFIG_LOCK_PATH = "/locks/config/update";
public ConfigService() {
// 初始化ZooKeeper连接(实际项目中应使用@Value注入地址)
this.zkLock = new ZookeeperDistributedLock("localhost:2181");
}
/**
* 更新全局配置(使用ZooKeeper分布式锁)
* @param config 新配置
*/
public void updateGlobalConfig(Config config) {
try {
// 尝试获取锁(最多等待2秒)
if (!zkLock.tryLock(CONFIG_LOCK_PATH, 2, TimeUnit.SECONDS)) {
throw new RuntimeException("配置更新正在被其他节点执行");
}
// 执行配置更新操作
System.out.println("开始更新全局配置...");
Thread.sleep(1000); // 模拟耗时操作
System.out.println("全局配置更新完成");
} catch (Exception e) {
throw new RuntimeException("配置更新失败", e);
} finally {
try {
// 释放锁
zkLock.unlock();
} catch (Exception e) {
// 记录日志
}
}
}
}
方式3:基于数据库实现
原理:利用数据库唯一索引或乐观锁
表结构(MySQL示例)
CREATE TABLE distributed_lock (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
lock_key VARCHAR(128) NOT NULL UNIQUE, -- 锁名称(唯一约束)
client_id VARCHAR(64) NOT NULL, -- 客户端ID
expire_time TIMESTAMP NOT NULL -- 过期时间
);
Java代码实现(含详细注释)
@Repository
public class DatabaseLockDao {
@Autowired
private JdbcTemplate jdbcTemplate; // Spring JDBC模板
/**
* 尝试获取锁
* @param lockKey 锁名称
* @param clientId 客户端ID
* @param expireSeconds 锁过期时间(秒)
* @return true-获取成功,false-获取失败
*/
public boolean tryLock(String lockKey, String clientId, int expireSeconds) {
try {
// 使用INSERT ON DUPLICATE KEY UPDATE实现锁竞争
int updated = jdbcTemplate.update(
"INSERT INTO distributed_lock (lock_key, client_id, expire_time) " +
"VALUES (?, ?, NOW() + INTERVAL ? SECOND) " + // 插入新锁
"ON DUPLICATE KEY UPDATE " + // 当锁已存在时更新
" client_id = IF(expire_time < NOW(), VALUES(client_id), client_id), " + // 如果过期则覆盖
" expire_time = IF(expire_time < NOW(), VALUES(expire_time), expire_time)", // 更新过期时间
lockKey, clientId, expireSeconds
);
// 返回是否成功获取锁
return updated > 0;
} catch (DuplicateKeyException e) {
// 锁已被其他客户端持有
return false;
}
}
/**
* 释放锁
* @param lockKey 锁名称
* @param clientId 客户端ID
*/
public void unlock(String lockKey, String clientId) {
// 删除当前客户端持有的锁
jdbcTemplate.update(
"DELETE FROM distributed_lock WHERE lock_key = ? AND client_id = ?",
lockKey, clientId
);
}
}
使用案例:定时任务调度
@Service
public class ScheduledTaskService {
@Autowired
private DatabaseLockDao dbLockDao;
private static final String TASK_LOCK_KEY = "daily_report_generation";
private static final String CLIENT_ID = "server-node-1"; // 通常使用主机名或IP
/**
* 生成每日报表(使用数据库分布式锁)
*/
@Scheduled(cron = "0 0 3 * * ?") // 每天凌晨3点执行
public void generateDailyReport() {
// 尝试获取锁(有效期30分钟)
boolean locked = dbLockDao.tryLock(TASK_LOCK_KEY, CLIENT_ID, 1800);
if (!locked) {
System.out.println("其他节点正在执行报表生成任务");
return;
}
try {
System.out.println("开始生成每日报表...");
// 模拟耗时操作
Thread.sleep(5000);
System.out.println("每日报表生成完成");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
// 释放锁
dbLockDao.unlock(TASK_LOCK_KEY, CLIENT_ID);
}
}
}
三种实现方式对比
特性 | Redis | ZooKeeper | 数据库 |
---|---|---|---|
性能 | ⭐⭐⭐⭐⭐ (10万+/秒) | ⭐⭐ (1万/秒) | ⭐ (500/秒) |
实现复杂度 | 简单 | 中等 | 简单 |
可靠性 | 依赖Redis持久化 | ⭐⭐⭐⭐⭐ (强一致性) | 依赖数据库可靠性 |
锁自动释放 | 通过EXPIRE实现 | Session断开自动删除临时节点 | 需额外实现超时清理 |
避免死锁 | 超时自动释放 | Session断开自动释放 | 需手动清理过期锁 |
可重入性 | 需自行实现 | 原生支持 | 需自行实现 |
适用场景 | 高并发、短任务 | 强一致性要求场景 | 低并发、简单系统 |
缺点 | 集群需RedLock方案 | 性能较低、依赖ZooKeeper可用性 | 性能差、数据库压力大 |
分布式锁 vs 分布式事务
关系说明
-
互补关系
- 分布式锁:解决短时资源互斥访问(如秒杀扣库存)
- 分布式事务:解决跨服务的多个操作原子性(如银行转账)
-
协同工作场景
graph LR A[订单服务] -->|锁定库存| B[分布式锁] A --> C[支付服务] C -->|扣款| D[分布式事务]- 步骤1:用分布式锁锁定商品库存(防止超卖)
- 步骤2:通过分布式事务协调订单+支付操作(保证两者同时成功/失败)
-
关键区别
维度 分布式锁 分布式事务 目标 资源互斥访问 跨服务操作原子性 持续时间 秒级(短时) 秒~分钟(长时) 典型方案 Redis/ZooKeeper Seata/TCC/Saga 数据一致性级别 最终一致性 强一致性
总结
- 分布式锁是分布式事务的前置保障(确保资源安全),但不是事务的替代品。
- 在复杂业务中,通常先加锁保护关键资源,再通过分布式事务完成跨服务操作。
典型应用场景:电商下单流程
- 使用Redis分布式锁锁定商品库存(防止超卖)
- 通过Seata分布式事务同时创建订单和扣减账户余额
- 若事务失败则释放锁并回滚所有操作
❤️ 如果你喜欢这篇文章,请点赞支持! 👍 同时欢迎关注我的博客,获取更多精彩内容!
本文来自博客园,作者:佛祖让我来巡山,转载请注明原文链接:https://www.cnblogs.com/sun-10387834/p/18925621