1. 分布式锁是什么
作为一名在互联网大厂摸爬滚打多年的架构师,见证了无数系统从单体应用演变为分布式架构的过程,也踩过不少分布式环境下的坑。
其中,分布式锁就是一个既简单又复杂的技术点,今天想和大家深入探讨一下这个话题。
分布式锁,顾名思义,就是在分布式环境下实现的锁机制。在单机系统中,我们可以使用语言提供的锁机制(如Java的synchronized或ReentrantLock)来控制共享资源的访问。
但在分布式系统中,这些本地锁就无能为力了,因为多个服务实例可能分布在不同的机器上,本地锁只能控制单机内的资源访问。
那么分布式锁到底有什么用途呢?简单来说,当多个分布式服务实例需要对共享资源进行互斥访问时,就需要用到分布式锁。典型的场景包括:
-
避免重复处理:如定时任务在多个服务实例上同时触发,但只需要一个实例处理就行。
-
数据一致性保护:多个服务实例可能同时修改同一份数据,需要加锁防止数据被污染。
-
秒杀场景:有限商品在短时间内面临大量用户抢购,需要确保不超卖。
-
业务流程控制:某些业务操作需要按顺序执行,分布式锁可以确保这种顺序性。
2. 分布式锁的核心特性
作为从业多年的架构师,我认为一个合格的分布式锁至少要满足以下几个特性:
2.1 互斥性
这是最基本的,在任何时候,只有一个客户端能持有锁。这就像一个会议室,当有人在使用时,需要挂上"使用中"的牌子,其他人看到后就会等待。
2.2 可重入性
同一个客户端可以多次获取同一把锁,这是避免死锁的必要特性。就像我进入自己的办公室后反锁了门,之后我可以随意在办公室和外面走动,而不需要每次都开新锁。
2.3 锁超时
即使持有锁的客户端崩溃,锁也会在一定时间后自动释放,防止死锁。这就好比图书馆的借阅系统,即使借书人忘记归还,系统也会在到期后将书籍标记为可借。
2.4 高可用
锁服务不应成为系统的单点故障。就像公司的门禁系统,如果只有一个门禁系统,它故障了会导致所有人都无法正常工作,所以应该有备用系统。
2.5 高性能
获取和释放锁的操作应该尽可能快,不应成为系统的性能瓶颈。就像高速公路的收费站,如果处理缓慢,会导致大量车辆排队等待。
3. 分布式锁的实现方式
经过多年的项目实践,我发现主流的分布式锁实现主要有以下几种:
3.1 基于数据库的实现
这是最容易理解的实现方式,利用数据库的唯一索引特性来实现互斥。
-- 创建锁表
CREATE TABLE distributed_lock (
lock_key VARCHAR(64) NOT NULL,
lock_value VARCHAR(128) NOT NULL,
expire_time TIMESTAMP NOT NULL,
PRIMARY KEY (lock_key)
);
-- 获取锁(使用INSERT语句)
INSERT INTO distributed_lock (lock_key, lock_value, expire_time)
VALUES ('order_process', 'client_123', NOW() + INTERVAL '30 SECOND');
-- 如果插入成功,则获取锁成功;如果因主键冲突插入失败,则获取锁失败
-- 获取锁的另一种方式(使用SELECT FOR UPDATE)
BEGIN TRANSACTION;
SELECT * FROM distributed_lock WHERE lock_key = 'order_process' FOR UPDATE;
-- 如果记录不存在,则插入一条记录
INSERT INTO distributed_lock (lock_key, lock_value, expire_time)
VALUES ('order_process', 'client_123', NOW() + INTERVAL '30 SECOND');
COMMIT;
-- 释放锁
DELETE FROM distributed_lock WHERE lock_key = 'order_process' AND lock_value = 'client_123';
-- 锁续期
UPDATE distributed_lock
SET expire_time = NOW() + INTERVAL '30 SECOND'
WHERE lock_key = 'order_process' AND lock_value = 'client_123';
优点:
- 实现简单,易于理解。
- 不需要引入额外的组件。
- 适合读写频率不高的场景。
缺点:
- 性能较差,特别是在高并发场景下。
- 数据库可能成为单点故障。
- 没有内置的自动过期机制,需要额外开发。
3.2 基于Redis的实现
Redis凭借其高性能和原子操作特性,成为实现分布式锁的热门选择。我们先来看一个简单的实现:
import redis.clients.jedis.Jedis;
import java.util.Collections;
public class RedisLock {
private static final String LOCK_SUCCESS = "OK";
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "PX";
private static final Long RELEASE_SUCCESS = 1L;
private Jedis jedis;
public RedisLock(Jedis jedis) {
this.jedis = jedis;
}
/**
* 获取分布式锁
* @param lockKey 锁的key
* @param requestId 请求标识(用于锁的释放验证)
* @param expireTime 锁的过期时间(毫秒)
* @return 是否成功获取锁
*/
public boolean tryLock(String lockKey, String requestId, int expireTime) {
String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST,
SET_WITH_EXPIRE_TIME, expireTime);
return LOCK_SUCCESS.equals(result);
}
/**
* 释放分布式锁
* @param lockKey 锁的key
* @param requestId 请求标识(确保释放的是自己的锁)
* @return 是否成功释放锁
*/
public boolean releaseLock(String lockKey, String requestId) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey),
Collections.singletonList(requestId));
return RELEASE_SUCCESS.equals(result);
}
}
这个简单的Redis锁实现利用了SET key value NX PX milliseconds
命令,该命令保证了操作的原子性:只有当key不存在时才会设置成功,并同时设置过期时间。释放锁时,使用Lua脚本保证了检查锁的拥有者和删除锁的原子性。
不过,这个简单实现存在一些问题:
- 单点故障:如果Redis宕机,所有锁操作都会失败。
- 锁的过期问题:如果持有锁的客户端在操作完成前崩溃,且操作耗时超过了锁的过期时间,就有可能导致其他客户端获取锁并开始操作,造成数据不一致。
为了解决这些问题,Redis官方推荐使用Redis Redlock算法,这是一种更可靠的分布式锁实现方式。
Redlock算法的核心思想是:
- 客户端依次向N个独立的Redis节点获取锁。
- 如果客户端能够在大多数节点(N/2+1)获取锁,且获取锁的总耗时小于锁的过期时间,则认为获取锁成功。
- 如果获取锁失败,客户端应该向所有节点发起释放锁的操作。
这种实现提高了系统的可用性,即使部分Redis节点出现故障,也能正常工作。在实际项目中,我一般推荐使用成熟的开源库,如Redisson,它已经实现了Redlock算法。
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import java.util.concurrent.TimeUnit;
public class RedissonLockExample {
public static void main(String[] args) {
// 配置Redisson
Config config = new Config();
config.useSingleServer()
.setAddress("redis://127.0.0.1:6379")
.setPassword("password");
// 创建Redisson客户端
RedissonClient redisson = Redisson.create(config);
// 获取锁对象
RLock lock = redisson.getLock("myLock");
try {
// 尝试获取锁,最多等待100秒,锁有效期为30秒
boolean isLocked = lock.tryLock(100, 30, TimeUnit.SECONDS);
if (isLocked) {
try {
// 执行业务逻辑
System.out.println("获取锁成功,执行业务逻辑");
Thread.sleep(15000); // 模拟业务操作耗时
} finally {
// 释放锁
lock.unlock();
System.out.println("业务执行完毕,释放锁");
}
} else {
System.out.println("获取锁失败");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 关闭Redisson客户端
redisson.shutdown();
}
}
}
Redisson不仅实现了基本的分布式锁功能,还提供了锁的自动续期、可重入锁、读写锁、公平锁等高级特性,让开发者可以更专注于业务逻辑。
3.3 基于ZooKeeper的实现
作为一个分布式协调服务,ZooKeeper的临时节点和有序节点特性使其非常适合实现分布式锁。
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.apache.curator.retry.ExponentialBackoffRetry;
import java.util.concurrent.TimeUnit;
public class ZookeeperLockExample {
public static void main(String[] args) {
// 创建CuratorFramework客户端
CuratorFramework client = CuratorFrameworkFactory.newClient(
"localhost:2181",
new ExponentialBackoffRetry(1000, 3)
);
client.start();
// 创建分布式锁
InterProcessMutex lock = new InterProcessMutex(client, "/locks/myLock");
try {
// 尝试获取锁,最多等待10秒
if (lock