← 返回题目列表

单例模式能保证分布式系统中只有一个实例吗?

高频 困难 第 15 / 26 题 更新于 2026/08/01
单例模式分布式系统分布式锁进程边界

简化版

不能。传统单例只能保证某个进程、某个类加载边界或某个容器作用域内的实例唯一,无法保证跨 JVM、跨机器、跨副本唯一。分布式唯一要依赖数据库唯一约束、分布式锁、选主、一致性协议或中心化服务,而不是靠静态字段。

详细版

单例的“唯一”一定有作用域。Java 静态单例通常是每个定义该类的 ClassLoader 一份;Spring singleton 是每个容器、每个 Bean 定义一份;当应用部署成 3 个副本时,每个副本都有自己的内存和静态字段,因此至少会有 3 份进程内单例。

如果业务要求全局只有一个执行者,例如定时任务只跑一次、订单号全局递增、主节点只有一个,就必须使用分布式协调机制。常见方案包括:

  1. 数据库唯一约束或乐观锁;
  2. Redis/ZooKeeper/etcd 分布式锁;
  3. 基于租约的 Leader 选举;
  4. 消息队列分区和消费者组;
  5. 把唯一资源收敛到一个外部服务。

面试回答要主动区分“进程内共享对象”和“分布式全局唯一资源”,这是单例模式最容易被夸大的边界。

完整版教学

一、单例的唯一性一定有边界

单例模式经常被描述为“保证一个类只有一个实例”,但严谨说法应该加上作用域。类、静态字段和对象都存在于具体运行时环境里,不会自动跨 JVM 同步。

常见边界如下:

场景单例实际边界
普通 Java 静态字段每个定义类加载器一份
单个 JVM 进程每个进程独立内存
Spring singleton每个容器、每个 Bean 定义一份
Kubernetes 3 副本每个 Pod 一份
分布式集群单例本身不提供全局唯一

所以面试中听到“系统只有一个实例”时,要追问是单进程、单 JVM、单容器,还是整个集群。边界不清,结论就不可靠。

二、为什么多副本部署会产生多个单例

假设一个服务部署 4 个副本,每个副本启动同一个 Java 应用。每个 JVM 都会加载自己的类,维护自己的静态字段:

Pod A: TaskRunner.INSTANCE
Pod B: TaskRunner.INSTANCE
Pod C: TaskRunner.INSTANCE
Pod D: TaskRunner.INSTANCE

从每个进程内部看,TaskRunner 都只有一个实例;从集群视角看,一共有 4 个。若每个实例都执行“每天 0 点生成报表”,报表就可能生成 4 次。

这不是单例实现写错,而是模式能力边界如此。静态字段没有跨进程通信能力,也没有故障检测和租约失效能力。

三、分布式唯一应该用什么机制

分布式唯一需要外部共享事实来源。不同业务对应不同方案:

需求常见方案关键点
防止重复插入数据库唯一约束以数据层约束兜底
只有一个任务执行者分布式锁或选主锁租约、续期、失败释放
全局递增 ID号段、雪花、数据库序列唯一性和趋势递增权衡
分区内有序消费MQ 分区 + 消费者组每个分区只给一个消费者
主节点协调ZooKeeper/etcd 选举临时节点、会话和 watch

如果只是节点内复用一个客户端,单例可以;如果要整个集群只有一个行为,就必须让所有节点竞争同一个外部协调资源。

四、分布式锁不是把单例放大那么简单

很多人会说“加 Redis 分布式锁就行”,但锁本身也有边界。正确使用至少要考虑唯一 token、过期时间、续期、释放时校验 owner,以及锁过期后业务是否还能继续执行。

一个简化时序:

T=0s   节点 A 获得锁,租约 30s
T=10s  A 开始执行任务
T=31s  A 卡顿未续期,锁过期
T=32s  节点 B 获得锁并开始执行
T=35s  A 恢复并继续写结果

如果任务没有幂等保护,A 和 B 仍可能同时写数据。分布式锁解决的是协调入口,不自动解决业务幂等和长事务一致性。

五、数据库唯一约束为什么常作为兜底

对于“同一订单只能创建一条支付记录”这类问题,最可靠的兜底通常是数据库唯一约束。即使应用层单例、分布式锁或消息去重出现问题,最终写入时仍会被数据层拦住。

例如:

CREATE UNIQUE INDEX uk_payment_order_id ON payment(order_id);

假设 3 个节点同时处理同一个 order_id=1001,只有一个事务能成功插入,另外两个会收到唯一键冲突。应用层要捕获冲突并转成幂等成功或明确失败,而不是依赖“理论上只有一个单例会处理”。

记忆钩子:进程内单例是内存事实,分布式唯一必须变成所有节点都承认的外部事实。

六、定时任务场景怎么回答

定时任务是这题的高频追问。单机时一个 Scheduler 单例就够;多实例部署后,每个实例都会触发同一任务。要做到集群只执行一次,可以选择:

  1. 用调度平台集中派发,例如 XXL-JOB、ElasticJob;
  2. 每次执行前竞争分布式锁;
  3. 用数据库任务表做状态抢占;
  4. 让消息队列只投递给消费者组中的一个消费者。

如果任务允许重复执行,最好设计幂等,例如按 job_date 建唯一记录。假设日报任务 2026-08-01 被触发 2 次,第二次插入同一个日期的结果时被唯一键拦住,就不会产生重复报表。

七、什么时候进程内单例仍然有价值

不能保证分布式唯一,不代表单例没有用。进程内单例仍适合复用本节点资源,例如 HTTP 客户端连接池、配置快照、指标注册表、线程池入口等。

关键是不要把两个问题混在一起:

节点内复用资源 -> 单例、容器 singleton、对象池
集群内只有一个行为 -> 锁、选主、唯一约束、队列分区

例如每个节点都有一个 HttpClient 单例是合理的,因为每个节点都需要发请求;但每个节点都有一个“全局订单号自增器”就危险,因为它们彼此不知道对方已经发了哪些号。

八、常见误区与追问

  • 误区:单例模式能保证整个系统只有一个对象。 它只在特定运行时边界内保证唯一,不能跨进程跨机器。
  • 误区:Kubernetes 只部署一个副本就等于分布式唯一。 扩容、重启、滚动发布和故障恢复都可能改变运行实例数量。
  • 误区:有分布式锁就不需要幂等。 锁可能过期、续期失败或业务超时,数据层幂等仍然重要。
  • 误区:Spring singleton 可以保证集群只有一个 Bean。 Spring 默认只保证每个容器、每个 Bean 定义一份。
  • 追问:全局 ID 生成器能用单例吗? 单机可以,分布式要用雪花、号段、数据库序列或中心化 ID 服务。
  • 追问:定时任务多副本重复执行怎么办? 用调度平台、分布式锁、任务表抢占或 MQ 消费者组,并设计幂等。
  • 追问:分布式锁释放时为什么要校验 token? 防止旧持有者误删新持有者的锁。

九、加强记忆

回答这题时先画边界:ClassLoader、JVM、容器、Pod、集群逐层扩大。单例最多解决进程内共享对象,分布式唯一要靠外部一致性机制和数据层约束。只要涉及多副本执行、全局 ID、主节点和定时任务,就不能把静态字段当答案。