本文目录导读:

在分布式系统中,传统的 Java 单例模式(如双重检查锁、静态内部类)无法保证全局唯一性,因为它们仅能在单个 JVM 进程内生效。
要在分布式环境下实现“单例”(即全局唯一实例),核心思路是引入第三方协调组件,将实例的创建权交给分布式锁或注册中心。
以下是分布式环境下实现“分布式单例”的 4 种主流方案:
基于分布式锁(推荐)
这是最直接的方式:在创建单例对象时,先获取一个全局锁,获取成功才创建,创建后释放锁。
实现思路(以 Redis 为例):
public class DistributedSingleton {
private static volatile DistributedSingleton instance;
private static final String LOCK_KEY = "singleton:lock";
private DistributedSingleton() {}
public static DistributedSingleton getInstance() {
if (instance == null) { // 本地快速检查
String token = UUID.randomUUID().toString();
try {
// 尝试获取分布式锁(setnx + expire原子操作)
boolean locked = RedisUtil.setIfAbsent(LOCK_KEY, token, 10, TimeUnit.SECONDS);
if (locked) {
if (instance == null) { // 双重检查
instance = new DistributedSingleton();
}
} else {
// 获取不到锁,可以选择等待、重试或抛出异常
Thread.sleep(100);
return getInstance(); // 递归重试(注意防死循环)
}
} finally {
// 释放锁:必须验证是自己的锁(通过token)
if (token.equals(RedisUtil.get(LOCK_KEY))) {
RedisUtil.delete(LOCK_KEY);
}
}
}
return instance;
}
}
💡 注意:实际生产中用 Redisson 等成熟库,避免自己实现锁导致的死锁、锁过期等问题。
基于数据库唯一约束
利用数据库的唯一索引或主键约束,确保全局只有一个记录。
实现方式:
- 在数据库中建一张表
global_singleton,字段id、instance_data。 - 将
id设为唯一主键(如固定值1)。 - 所有节点尝试
INSERT该记录,只有第一个插入成功的节点才能创建实例。 - 后续节点
INSERT会失败(违反唯一约束),直接返回已有实例。
核心代码片段:
public class DistributedSingleton {
private static DistributedSingleton instance;
public static DistributedSingleton getInstance() {
if (instance == null) {
try {
// 尝试插入唯一记录
jdbcTemplate.update("INSERT INTO global_singleton (id, data) VALUES (1, ?)", createData());
instance = new DistributedSingleton();
} catch (DuplicateKeyException e) {
// 已有节点创建,读取实例数据
String data = jdbcTemplate.queryForObject("SELECT data FROM global_singleton WHERE id = 1", String.class);
// 反序列化或直接复用
}
}
return instance;
}
}
基于注册中心(ZooKeeper / etcd)
利用选举机制:只有 Leader 节点才负责创建单例实例。
ZooKeeper 实现步骤:
- 所有节点在 ZK 上创建临时顺序节点
/singleton/instance-0001。 - 编号最小的节点(
/singleton/instance-0001)成为 Leader。 - Leader 节点创建实例,其他节点监听该节点,实例销毁时重新选举。
优点:天然具备高可用和自动切换能力。
基于全局序列化 ID(特殊场景)
如果单例不是“对象”,而是一个“服务实例”本身(如某个定时任务只在一个节点执行),可以用幂等 ID:
- 每个请求携带全局唯一 ID
- 第一个处理该 ID 的节点执行,后续节点检测到已执行则跳过
核心冲突:单例模式定义 vs 分布式环境
传统的单例模式要求:
只创建一次,全局只有一个实例
在分布式系统中:
- Java 对象的单例:只在单进程内唯一,分布式要求多进程共享同一个对象,但不同 JVM 进程中的对象地址不同,无法通过语言特性做到。
- 实际业务含义:分布式单例的核心是“保证对某资源的独占访问”或“只初始化一次”,分布式任务调度中只允许一个节点执行任务、全局唯一的配置加载器等。
选型建议
| 场景 | 推荐方案 |
|---|---|
| 高并发、低延迟 | Redis 分布式锁(Redisson) |
| 已有 ZK 集群 | ZooKeeper 选举模式 |
| 简单且无额外中间件 | 数据库唯一约束 |
| 定时任务、离线处理 | 无状态加锁 |
常见陷阱
- 锁过期导致重复创建:确保锁持有时间 >> 实例创建时间,或使用 Redisson 看门狗自动续期。
- 死锁:务必加锁释放逻辑(try/finally)。
- 性能问题:所有节点抢锁,单点瓶颈,考虑本地缓存 + 定期刷新。