目录
1. UUID (Universally Unique Identifier)
6. ULID (Universally Unique Lexicographically Sortable Identifier)
分布式系统介绍
一、定义与概念
分布式系统是由多个通过网络连接的独立计算机节点组成的系统,这些节点相互协作,共同完成一个或多个任务,对外表现得像一个单一的系统。每个节点都有自己的处理器、内存和存储,它们通过消息传递进行通信和协调。例如,大型电商平台如淘宝、京东,背后支撑其运行的就是复杂的分布式系统,众多服务器节点分别负责用户管理、商品展示、订单处理等不同功能,协同为用户提供服务。
二、分布式系统的特点
- 分布性:组件分布在不同的物理或虚拟节点上,这些节点可能位于不同地理位置的数据中心。例如,一家跨国公司的业务系统,其用户数据可能存储在位于不同国家的数据中心节点上。
- 并发性:多个节点可能同时处理不同的任务,节点之间需要协调和同步操作,以避免数据冲突等问题。比如在分布式数据库中,多个节点可能同时对相同的数据进行读写操作。
- 故障独立性:某个节点出现故障不应导致整个系统崩溃,其他节点应能继续提供部分或全部功能。像云存储系统,即使个别存储节点发生故障,用户仍然可以访问和存储数据。
- 透明性:对于用户和应用程序而言,分布式系统的复杂性被隐藏,它们看到的就像一个单一的系统。例如,用户在使用在线支付功能时,无需关心背后是由多少个分布式节点协同完成支付处理的。
三、分布式系统面临的挑战
- 网络问题
- 网络延迟:不同节点间的数据传输需要时间,可能导致操作响应缓慢。比如,从欧洲的数据中心向亚洲的数据中心请求数据,由于物理距离远,网络延迟较高。
- 网络分区:网络故障可能导致部分节点间通信中断,形成网络分区,使系统出现数据不一致等问题。
- 数据一致性
在分布式环境下,确保多个节点上的数据一致性是一大挑战。例如,在分布式数据库的读写操作中,不同节点可能由于网络延迟等原因,在同一时刻的数据状态不一致。 - 故障处理
由于节点众多,某个节点发生故障的可能性增加。需要设计有效的故障检测、恢复和容错机制,以保证系统的可用性。例如,服务器硬件故障、软件崩溃等情况都需要系统能够快速应对。
四、分布式系统的常见应用场景
- 大型网站和互联网应用:如社交媒体平台、在线游戏平台等,需要处理海量用户的并发请求,通过分布式系统可以实现水平扩展,提高系统的处理能力和可用性。
- 大数据处理:分布式计算框架如 Hadoop、Spark 用于处理大规模数据集,将数据和计算任务分布到多个节点上并行处理,加快数据处理速度。
- 云计算平台:提供弹性计算、存储和网络等服务,背后依靠分布式系统实现资源的动态分配和管理,满足不同用户的需求。
分布式系统一定是由多个节点组成的系统。 其中节点指的是计算机服务器,而且这些节点一般不是孤立的,而是互通的。
微服务架构是一种架构风格,它提倡将单一应用程序划分成一组小的服务,每个服务运行在自己进程中,服务间通信机制采用轻量级通讯机制(通常是基于 HTTP 的 RESTful API ) 。
CAP 定理
- 定义:CAP 定理指出,在一个分布式系统中,一致性(Consistency)、可用性(Availability)和分区容错性(Partition Tolerance)这三个特性不能同时满足,最多只能同时满足其中两个。
- 一致性:所有节点在同一时间具有相同的数据,即更新操作成功并返回客户端后,所有节点在同一时间的数据完全一致。
- 可用性:系统在任何时候都能响应客户端的请求,即每个请求都能在有限时间内得到响应,不会出现无限期的等待。
- 分区容错性:系统能够在网络分区的情况下继续正常运行,即当网络出现分区(部分节点之间无法通信)时,系统仍然能够提供服务。
BASE 理论
- 定义:BASE 理论是对 CAP 定理中一致性和可用性权衡的结果,它的核心思想是通过牺牲强一致性来获得高可用性。BASE 是基本可用(Basically Available)、软状态(Soft State)和最终一致性(Eventually Consistent)三个短语的缩写。
- 基本可用:系统在出现故障时,允许损失部分可用性,但仍然能够提供基本的服务。例如,在某些情况下,系统可能会降低响应速度或返回部分数据,但不会完全不可用。
- 软状态:系统中的数据可以存在中间状态,并且允许这种中间状态在一段时间内存在。也就是说,数据不需要在所有节点上立即保持一致,而是可以在一段时间内逐渐达到一致。
- 最终一致性:尽管系统中的数据在一段时间内可能不一致,但最终会达到一致状态。在最终一致性的系统中,只要客户端在一段时间后再次请求数据,就能够得到最新的、一致的数据。
CAP 定理强调了分布式系统中三个关键特性之间的权衡关系,而 BASE 理论则提供了一种在实际应用中实现分布式系统的思路,即在满足分区容错性的前提下,通过牺牲强一致性来换取高可用性和可扩展性。
BASE理论是如何保证最终一致性的
BASE 理论通过以下几种方式来保证最终一致性:
- 异步处理与补偿机制
- 操作异步执行:在分布式系统中,当一个写操作发生时,系统不会立即要求所有相关节点都完成数据更新,而是先将操作记录下来,并异步地将更新请求发送到其他节点。这样可以避免因等待所有节点同步完成而导致的性能下降和可用性降低。
- 补偿操作:如果在异步更新过程中出现错误或失败,系统会采用补偿机制来确保数据最终达到一致。例如,通过重试机制重新发送更新请求,或者执行一些补偿性的操作来修正数据。
- 数据复制与传播
- 多副本数据存储:在分布式系统中,数据通常会在多个节点上进行复制,以提高系统的可用性和容错能力。当一个节点的数据发生更新时,更新会逐渐传播到其他副本节点。
- 传播策略:通过采用合适的传播策略,如基于时间戳或版本号的更新传播方式,确保数据在不同节点之间能够正确地同步。例如,当一个节点接收到一个更新请求时,它会比较请求中的数据版本号与本地数据的版本号,如果请求的版本号更高,则更新本地数据。
- 一致性检查与修复
- 定期检查:系统会定期对数据进行一致性检查,通过比较不同节点上的数据版本、校验和或其他一致性指标,来发现数据不一致的情况。
- 主动修复:一旦发现数据不一致,系统会采取主动修复措施,例如从其他正确的节点获取最新的数据来更新不一致的节点,或者通过一些特定的算法来计算出正确的数据状态并进行修复。
- 事务处理与协调
- 柔性事务:在 BASE 理论中,通常采用柔性事务来处理分布式数据的更新。柔性事务允许在一定程度上放松对事务的严格一致性要求,通过使用一些补偿性的操作来保证最终一致性。例如,使用 TCC(Try - Confirm - Cancel)模式,先尝试执行操作,然后根据情况确认或取消操作,如果出现问题则通过补偿操作来恢复数据的一致性。
- 分布式事务协调:通过分布式事务协调器来管理和协调不同节点之间的事务处理。协调器会跟踪事务的执行状态,并在必要时采取措施来保证事务的最终一致性,如在部分节点出现故障时,协调其他节点进行相应的处理,以确保整个事务能够正确完成。
分布式锁的常见使用场景有哪些?
分布式锁在分布式系统中用于控制多个节点对共享资源的并发访问,常见使用场景包括:
1. 防止多节点重复操作
- 定时任务幂等性:在分布式系统中,多个节点可能同时触发同一个定时任务(如数据同步、报表生成),使用分布式锁可以确保只有一个节点执行任务,避免重复计算或数据冲突。
- 防止重复提交:在高并发场景下,用户可能多次提交同一请求(如支付、订单创建),通过分布式锁可以确保同一操作只被执行一次。
2. 资源互斥访问
- 分布式缓存更新:当多个节点同时更新同一个缓存项时,使用分布式锁可以保证只有一个节点进行更新操作,避免缓存击穿或数据不一致。
- 数据库写冲突:多个节点同时修改同一数据库记录时,通过分布式锁可以防止脏写或数据冲突。例如,库存扣减、账户余额更新等操作。
3. 分布式事务
- 跨服务资源锁定:在分布式事务中,涉及多个服务对不同资源的操作,使用分布式锁可以确保这些操作的原子性。例如,在订单支付过程中,锁定库存和账户余额,防止超卖或余额不足。
4. 全局 ID 生成器
- 唯一 ID 生成:多个节点需要生成全局唯一 ID 时,通过分布式锁可以保证生成的 ID 不重复。例如,使用数据库自增 ID 或 UUID 时,可能存在并发冲突,分布式锁可以确保 ID 生成的唯一性。
5. 分布式限流
- 全局流量控制:在分布式系统中,为防止某个服务被过多请求压垮,可以使用分布式锁实现全局限流。例如,限制同一时间内最多有 N 个请求访问某个资源。
6. 任务调度与协调
- Leader 选举:在分布式集群中,多个节点需要选举一个 Leader 来协调任务分配,使用分布式锁可以确保只有一个节点被选为 Leader。
- 资源抢占:多个节点竞争同一资源(如分布式文件系统中的文件锁、数据库连接池中的连接)时,分布式锁可以实现公平或非公平的资源分配。
7. 防止缓存雪崩
- 热点缓存重建:当缓存失效时,多个节点可能同时尝试重建缓存,导致数据库压力骤增。使用分布式锁可以确保只有一个节点负责重建缓存,其他节点等待缓存更新完成后直接使用新缓存。
8. 分布式系统初始化
- 系统启动互斥:多个节点同时启动时,可能需要对共享资源进行初始化(如配置加载、数据预热),使用分布式锁可以确保初始化操作只被执行一次。
常见实现方式
- 基于数据库:通过数据库的唯一索引或行锁实现。
- 基于 Redis:使用
SETNX
命令或 RedLock 算法。 - 基于 ZooKeeper:通过创建临时有序节点实现公平锁。
- 基于 Etcd:利用其 CAS(Compare - And - Swap)机制实现分布式锁。
选择合适的分布式锁实现需要根据具体场景权衡性能、可靠性和可用性。
避免分布式锁带来的死锁问题?
在分布式系统中,死锁是使用分布式锁时需要特别注意的问题,以下是避免死锁的几种常见方法:
1. 设置锁超时时间
- 自动释放机制:为每个锁设置合理的超时时间(TTL),即使持有锁的节点崩溃或处理超时,锁也会在超时后自动释放,避免其他节点无限等待。
- 示例:在 Redis 中使用
SET key value NX PX timeout
命令,或在 ZooKeeper 中创建临时节点。
2. 按顺序获取锁
- 固定加锁顺序:当需要获取多个锁时,所有节点必须按照相同的顺序获取锁,避免循环等待。例如,若业务需要同时锁定资源 A 和 B,所有节点必须先锁 A 再锁 B。
3. 使用可重入锁
- 避免自我死锁:允许同一节点在持有锁的情况下再次获取锁(需记录重入次数),防止自身因重复请求锁而导致死锁。
4. 实现锁的公平性
- 公平锁机制:使用公平锁(如 ZooKeeper 的临时有序节点)确保锁的获取按照请求顺序进行,避免饥饿和死锁。
5. 检测与恢复机制
- 死锁检测算法:实现分布式死锁检测,定期检查是否存在循环等待的锁依赖关系,一旦发现则强制释放某些锁。
- 超时重试策略:当获取锁失败时,设置合理的重试次数和退避策略(如指数退避),避免频繁重试加剧死锁风险。
6. 原子化操作
- 使用原子指令:利用 Redis 的
SETNX
、RedLock
算法或 ZooKeeper 的 CAS(Compare - And - Swap)操作确保锁的获取和释放是原子性的,减少中间状态导致的死锁。
7. 锁粒度控制
- 细化锁粒度:尽量缩小锁的范围,只锁定关键资源,减少锁的持有时间,从而降低死锁概率。例如,使用分段锁替代全局锁。
8. 心跳机制与健康检查
- 节点状态监控:通过心跳机制检测持有锁的节点是否存活,若节点失效则自动释放锁。例如,在 Redis 中使用 Lua 脚本原子化检查锁和节点状态。
9. 事务与补偿机制
- 柔性事务:对于复杂操作,使用 TCC(Try - Confirm - Cancel)或 Saga 模式替代传统的刚性事务,通过补偿操作释放已获取的锁。
10. 降级与熔断策略
- 服务保护:当系统负载过高或锁竞争过于激烈时,自动降级某些非关键业务,避免因锁等待导致的级联故障。
分布式环境下高可用解决方案
在分布式环境下,实现高可用的解决方案通常涉及到多个方面,包括冗余设计、故障检测与恢复、负载均衡、数据一致性等。以下是一些常见的分布式环境下高可用解决方案:
- 冗余与集群技术
- 服务器冗余:部署多个服务器节点,通过集群技术将它们组成一个逻辑整体。当某个节点出现故障时,其他节点可以接管其工作,实现自动故障转移,如通过 Keepalived 实现服务器的主备切换。
- 数据冗余:采用数据复制技术,将数据同步到多个节点或不同的数据中心,确保数据的安全性和可访问性。如 MySQL 的主从复制,以及分布式文件系统 Ceph 通过多副本机制实现数据冗余。
- 故障检测与恢复
- 健康检查机制:通过心跳检测、监控系统等手段,定期检查节点和服务的状态。如 ZooKeeper 通过心跳机制检测节点的存活状态,发现故障节点后通知其他节点进行相应处理。
- 自动恢复策略:一旦检测到故障,系统自动触发恢复流程,如自动重启故障服务、切换到备用节点或进行数据恢复操作。像 Kubernetes 可以自动重启故障的容器,并根据预设的策略重新调度到其他可用节点上。
- 负载均衡
- 请求分发:使用负载均衡器将客户端请求均匀分配到多个服务器节点上,避免单点负载过高。常见的负载均衡器有 Nginx、F5 等,可以根据不同的算法(如轮询、加权轮询、最少连接数等)进行请求分发。
- 动态负载调整:根据服务器的负载情况实时调整负载均衡策略,将请求分配到负载较轻的节点上。一些云平台提供的负载均衡服务可以自动根据服务器的 CPU、内存等资源使用情况进行动态调整。
- 数据一致性保证
- 分布式一致性算法:采用如 Paxos、Raft 等一致性算法,确保在分布式环境中数据的一致性。这些算法通过选举领导者、日志复制等方式,保证数据在多个节点之间的一致状态。
- 数据同步机制:对于分布式数据库或存储系统,需要建立可靠的数据同步机制,确保数据在不同节点之间的及时更新和一致。如 Cassandra 等分布式数据库通过 gossip 协议进行数据同步和状态传播。
- 分布式配置管理
- 集中式配置管理:使用集中式的配置管理工具(如 Apollo、Nacos),将系统的配置信息统一管理和分发。这样可以方便在分布式环境中对各个节点的配置进行修改和更新,确保配置的一致性和准确性。
- 配置版本控制:对配置信息进行版本管理,记录配置的变更历史,方便回滚和跟踪。同时,支持配置的动态更新,使系统能够在不重启的情况下应用新的配置。
- 监控与告警系统
- 全面的监控体系:建立覆盖整个分布式系统的监控系统,包括服务器性能、网络状况、应用程序指标等方面的监控。通过收集和分析这些监控数据,及时发现潜在的问题和异常。如 Prometheus 可以收集各种指标数据,并通过 Grafana 进行可视化展示。
- 实时告警机制:当监控到异常情况时,能够及时发送告警信息给相关人员。告警方式可以包括邮件、短信、即时通讯等多种方式,以便快速响应和处理问题。
- 服务治理
- 服务注册与发现:使用服务注册中心(如 Eureka、Consul),让服务提供者将自己的服务信息注册到中心,服务消费者可以从中心获取服务的地址和相关信息,实现服务的动态发现和调用。
- 熔断与降级:当某个服务出现故障或负载过高时,通过熔断机制快速切断对该服务的调用,避免故障扩散。同时,根据业务情况进行服务降级,如返回缓存数据或简化的响应结果,保证系统的核心功能仍然可用。
这些解决方案通常需要结合具体的业务场景和技术架构进行综合应用,以构建一个高可用的分布式系统。
分布式事务
分布式事务是指操作跨越多个资源(如数据库、服务)的事务,需要保证这些操作的原子性、一致性、隔离性和持久性(ACID)。在分布式系统中,由于网络延迟、节点故障等因素,实现分布式事务具有挑战性。以下是常见的解决方案和关键概念:
一、分布式事务的挑战
- CAP 定理:在分布式系统中,一致性(Consistency)、可用性(Availability)和分区容错性(Partition Tolerance)只能同时满足两个。
- BASE 理论:通过牺牲强一致性换取高可用性,采用最终一致性模型。
- 网络问题:跨节点通信可能失败或延迟,导致数据不一致。
二、常见解决方案
1. 两阶段提交(2PC)
- 流程:
- 准备阶段:协调者向所有参与者发送事务请求,参与者执行操作并反馈结果。
- 提交阶段:若所有参与者成功,则协调者发送提交指令;否则回滚。
- 优点:实现强一致性。
- 缺点:同步阻塞,单点故障(协调者),性能差。
- 适用场景:对一致性要求极高且节点数少的场景。
2. 三阶段提交(3PC)
- 改进:在 2PC 基础上增加CanCommit阶段,减少参与者阻塞时间,并引入超时机制。
- 流程:CanCommit → PreCommit → DoCommit。
- 优点:减少阻塞,提高可用性。
- 缺点:仍存在数据不一致风险(如网络分区)。
3. TCC(Try-Confirm-Cancel)
- 柔性事务:将事务分为三个阶段:
- Try:预留资源(如冻结账户余额)。
- Confirm:确认执行,提交资源。
- Cancel:回滚,释放预留资源。
- 优点:无长时间锁,性能高。
- 缺点:开发成本高,需要编写补偿逻辑。
- 示例:支付系统中,先冻结用户余额(Try),订单确认后扣款(Confirm),失败则解冻(Cancel)。
4. 消息队列(最终一致性)
- 流程:
- 业务操作完成后发送消息到队列。
- 消费者异步处理消息,更新相关资源。
- 通过重试或补偿机制确保最终一致性。
- 优点:高吞吐量,解耦服务。
- 缺点:不保证实时一致性,需幂等设计。
- 工具:RocketMQ、Kafka 等支持事务消息的队列。
5. Saga 模式
- 长事务分解:将大事务拆分为多个本地事务,每个子事务有对应的补偿操作。
- 两种模式:
- 向前恢复:失败时重试子事务。
- 向后恢复:失败时执行补偿操作回滚。
- 优点:无单点问题,适合长流程事务。
- 缺点:补偿逻辑复杂,不保证强一致性。
6. 最大努力通知
- 流程:
- 业务操作完成后发送通知(如短信、邮件)。
- 接收方定期查询状态,确认最终结果。
- 适用场景:对实时性要求不高的场景(如支付结果通知)。
三、分布式事务工具
- Seata:阿里巴巴开源框架,支持 AT(自动补偿)、TCC、Saga 等模式。
- ByteTCC:支持 TCC 和消息事务。
- OpenTransaction:轻量级分布式事务框架。
- 数据库原生支持:如 MySQL XA 协议、PostgreSQL 的 dblink。
四、最佳实践
- 优先避免分布式事务:通过业务设计减少跨服务事务(如缓存、最终一致性)。
- 选择合适的一致性级别:根据业务场景权衡强一致性和可用性(如支付用 2PC,订单用最终一致性)。
- 幂等设计:所有事务操作必须支持重试(如唯一 ID 防重复提交)。
- 监控与补偿:建立事务监控系统,对失败事务自动重试或人工干预。
- 熔断与降级:对关键服务设置熔断机制,防止级联故障。
五、对比总结
方案 | 一致性级别 | 性能 | 实现复杂度 | 适用场景 |
---|---|---|---|---|
2PC | 强一致 | 低 | 低 | 金融转账等严格场景 |
TCC | 最终一致 | 中 | 高 | 业务流程长且需回滚 |
消息队列 | 最终一致 | 高 | 中 | 异步通知、高并发场景 |
Saga | 最终一致 | 高 | 高 | 微服务长事务 |
最大努力通知 | 最终一致 | 极高 | 低 | 实时性要求不高的通知 |
根据业务需求选择合适的方案,通常结合多种模式以达到最佳效果。
接口的幂等性/解决重复消费
在分布式系统中,接口的幂等性和解决重复消费是非常重要的概念,它们有助于确保系统的稳定性和数据的一致性。以下是关于它们的详细介绍:
接口幂等性
- 定义:幂等性是指一个接口在多次调用时,产生的结果与一次调用相同,不会对系统造成额外的副作用。例如,对于查询接口,无论调用多少次,只要查询条件不变,返回的结果应该是一致的;对于删除接口,多次删除同一个资源,结果与一次删除相同,即资源被删除,不会出现多次删除导致错误的情况。
- 实现方式
- 使用唯一 ID:在请求中携带一个唯一的标识,如订单号、交易 ID 等。服务端在处理请求时,根据这个唯一 ID 判断请求是否已经被处理过。如果已经处理过,则直接返回之前的结果,不再执行实际的业务逻辑。
- 数据库约束:利用数据库的唯一约束来保证幂等性。例如,在插入数据时,通过设置唯一索引,如果重复插入相同的数据,数据库会抛出异常,从而避免数据重复插入。
- 状态机控制:对于一些有明确状态流转的业务,如订单的创建、支付、发货等,可以通过状态机来控制接口的幂等性。只有当订单处于特定状态时,才能执行相应的操作,避免重复操作导致状态混乱。
解决重复消费
- 问题场景:在消息队列等异步处理场景中,可能会出现消息被重复消费的情况。例如,消费者在处理消息时,由于网络故障、进程崩溃等原因,导致消息处理未完成,但消息已经被标记为已消费,当消费者重新启动后,可能会再次消费该消息,从而造成数据不一致或业务逻辑错误。
- 解决方法
- 消息幂等性处理:与接口幂等性类似,可以在消息处理逻辑中加入幂等性判断。例如,给每条消息分配一个唯一的 ID,消费者在处理消息时,先根据消息 ID 检查是否已经处理过该消息,如果是,则直接跳过处理。
- 使用消息队列的特性:一些消息队列提供了消息去重的功能,如 RocketMQ 的事务消息和 Kafka 的幂等生产者。可以利用这些特性来确保消息在生产和消费过程中的唯一性。
- 消费状态记录:在数据库或缓存中记录消息的消费状态,消费者在处理消息前先查询状态,判断是否已经消费过。如果已经消费过,则不再处理。同时,要确保消费状态的记录和消息处理的操作在同一个事务中,以保证数据的一致性。
- 分布式锁:在处理消息时,先获取分布式锁,只有获取到锁的消费者才能处理消息。这样可以避免多个消费者同时处理同一条消息,从而防止重复消费。但使用分布式锁要注意避免死锁问题。
无论是实现接口幂等性还是解决重复消费问题,都需要根据具体的业务场景和技术架构选择合适的方法,以确保系统的稳定性和数据的准确性。
新增、更新会出现。
场景:1、用户重复点击多次 2、接口超时/重试 3、消息重复消费
解决方案:
- 数据库唯一索引 -- 防止新增脏数据
- Token+Redis机制防重 -- 防止页面重复提交( 第一次请求生成token,第二次发起请求后携带token,执行完成后删除。)
- 乐观锁 -- 基于版本号version实现, 在更新数据那一刻校验数据
- 分布式锁 -- redis(redisson、zookeeper) 性能低。
- 状态机 -- 状态变更, 更新数据时判断状态
- 异步请求携带唯一标识
分布式唯一ID
概述
在分布式系统中,唯一 ID 是用于标识不同节点上的资源、事务或消息的标识符。设计分布式唯一 ID 需要考虑以下核心要素:
- 全局唯一性:确保所有节点生成的 ID 不重复
- 趋势递增 / 有序性:部分场景需要 ID 按时间有序生成
- 高性能:生成过程高效,避免成为系统瓶颈
- 高可用:确保分布式环境下持续生成 ID
- 安全性:ID 不可预测,避免被猜测
常见实现方案
1. UUID (Universally Unique Identifier)
原理:基于时间戳、MAC 地址或随机数生成 128 位标识符
优点:本地生成,无需依赖外部服务,性能极高
缺点:无序字符串,索引效率低;占用空间大 (16 字节)
Java 实现:
import java.util.UUID;
public class UUIDGenerator {
public static String generate() {
return UUID.randomUUID().toString().replace("-", "");
}
}
2. 数据库自增 ID
原理:利用数据库的 AUTO_INCREMENT 特性生成唯一 ID
优点:实现简单,有序性好
缺点:单点故障风险;性能瓶颈 (依赖数据库事务)
优化方案:
- 号段模式:数据库预分配多个 ID 段,应用本地缓存使用
- 双 Buffer 优化:当前号段使用到一定比例时,异步预取下一号段
3. Snowflake 算法(雪花算法)
原理:Twitter 开源的 64 位长整型 ID 生成算法,结构如下:
- 1 位符号位(始终为 0)
- 41 位时间戳(毫秒级,支持约 69 年)
- 5 位数据中心 ID(最多 32 个)
- 5 位机器 ID(每个数据中心最多 32 台机器)
- 12 位序列号(同一毫秒内生成的 ID,最多 4096 个)
雪花算法是 64 位 的二进制,一共包含了四部分:
- 1位是符号位,也就是最高位,始终是0,没有任何意义,因为要是唯一计算机二进制补码中就是负数,0才是正数。
- 41位是时间戳,具体到毫秒,41位的二进制可以使用69年,因为时间理论上永恒递增,所以根据这个排序是可以的。
- 10位是机器标识,可以全部用作机器ID,也可以用来标识机房ID + 机器ID,10位最多可以表示1024台机器。
- 12位是计数序列号,也就是同一台机器上同一时间,理论上还可以同时生成不同的ID,12位的序列号能够区分出4096个ID。
优点:高性能、趋势递增、可定制化
缺点:依赖系统时钟(需处理时钟回拨问题)
Java 实现:
public class SnowflakeIdGenerator {
private final long startTimeStamp = 1609459200000L; // 2021-01-01 00:00:00 UTC
private final long dataCenterIdBits = 5L;
private final long machineIdBits = 5L;
private final long sequenceBits = 12L;
private final long maxDataCenterId = -1L ^ (-1L << dataCenterIdBits);
private final long maxMachineId = -1L ^ (-1L << machineIdBits);
private final long machineIdShift = sequenceBits;
private final long dataCenterIdShift = sequenceBits + machineIdBits;
private final long timestampLeftShift = sequenceBits + machineIdBits + dataCenterIdBits;
private final long sequenceMask = -1L ^ (-1L << sequenceBits);
private final long dataCenterId;
private final long machineId;
private long sequence = 0L;
private long lastTimestamp = -1L;
public SnowflakeIdGenerator(long dataCenterId, long machineId) {
if (dataCenterId > maxDataCenterId || dataCenterId < 0) {
throw new IllegalArgumentException("DataCenter ID must be between 0 and " + maxDataCenterId);
}
if (machineId > maxMachineId || machineId < 0) {
throw new IllegalArgumentException("Machine ID must be between 0 and " + maxMachineId);
}
this.dataCenterId = dataCenterId;
this.machineId = machineId;
}
public synchronized long nextId() {
long currentTimestamp = System.currentTimeMillis();
// 处理时钟回拨
if (currentTimestamp < lastTimestamp) {
throw new RuntimeException("Clock moved backwards. Refusing to generate id for " +
(lastTimestamp - currentTimestamp) + " milliseconds");
}
if (currentTimestamp == lastTimestamp) {
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
// 当前毫秒内序列号用完,等待下一毫秒
currentTimestamp = waitNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = currentTimestamp;
return ((currentTimestamp - startTimeStamp) << timestampLeftShift) |
(dataCenterId << dataCenterIdShift) |
(machineId << machineIdShift) |
sequence;
}
private long waitNextMillis(long lastTimestamp) {
long timestamp = System.currentTimeMillis();
while (timestamp <= lastTimestamp) {
timestamp = System.currentTimeMillis();
}
return timestamp;
}
}
4. Redis 原子操作
原理:利用 Redis 的原子自增命令 (INCR) 生成唯一 ID
优点:高性能、支持分布式
缺点:依赖 Redis 可用性;需处理持久化问题
Java 实现:
import redis.clients.jedis.Jedis;
public class RedisIdGenerator {
private final Jedis jedis;
private final String key;
public RedisIdGenerator(String host, int port, String key) {
this.jedis = new Jedis(host, port);
this.key = key;
}
public long nextId() {
return jedis.incr(key);
}
public void close() {
if (jedis != null) {
jedis.close();
}
}
}
Incr 命令将 key 中储存的数字值增一 , Increment 英 /ˈɪŋkrəmənt/
decr key 递减
5. 数据库号段模式
原理:数据库存储当前号段的最大值和步长,应用批量获取号段本地使用
优点:高性能、减少数据库访问
缺点:号段预分配可能导致 ID 浪费
Java 实现:
import java.sql.*;
import java.util.concurrent.atomic.AtomicLong;
public class SegmentIdGenerator {
private final String jdbcUrl;
private final String username;
private final String password;
private final String bizType;
private final int step;
private AtomicLong currentId;
private AtomicLong maxId;
public SegmentIdGenerator(String jdbcUrl, String username, String password, String bizType, int step) {
this.jdbcUrl = jdbcUrl;
this.username = username;
this.password = password;
this.bizType = bizType;
this.step = step;
this.currentId = new AtomicLong(0);
this.maxId = new AtomicLong(0);
}
public synchronized long nextId() {
if (currentId.get() >= maxId.get()) {
// 号段用完,获取下一个号段
refreshSegment();
}
return currentId.incrementAndGet();
}
private void refreshSegment() {
try (Connection conn = DriverManager.getConnection(jdbcUrl, username, password)) {
// 开启事务
conn.setAutoCommit(false);
// 查询当前号段
String selectSql = "SELECT max_id FROM id_generator WHERE biz_type = ? FOR UPDATE";
try (PreparedStatement pstmt = conn.prepareStatement(selectSql)) {
pstmt.setString(1, bizType);
try (ResultSet rs = pstmt.executeQuery()) {
if (rs.next()) {
long oldMaxId = rs.getLong("max_id");
long newMaxId = oldMaxId + step;
// 更新号段
String updateSql = "UPDATE id_generator SET max_id = ? WHERE biz_type = ? AND max_id = ?";
try (PreparedStatement updateStmt = conn.prepareStatement(updateSql)) {
updateStmt.setLong(1, newMaxId);
updateStmt.setString(2, bizType);
updateStmt.setLong(3, oldMaxId);
int rows = updateStmt.executeUpdate();
if (rows == 0) {
throw new RuntimeException("Failed to update segment for bizType: " + bizType);
}
// 更新本地号段
currentId.set(oldMaxId);
maxId.set(newMaxId);
}
} else {
// 初始化号段
String insertSql = "INSERT INTO id_generator (biz_type, max_id, step) VALUES (?, ?, ?)";
try (PreparedStatement insertStmt = conn.prepareStatement(insertSql)) {
insertStmt.setString(1, bizType);
insertStmt.setLong(2, step);
insertStmt.setInt(3, step);
insertStmt.executeUpdate();
// 更新本地号段
currentId.set(0);
maxId.set(step);
}
}
}
}
conn.commit();
} catch (SQLException e) {
throw new RuntimeException("Failed to get next segment", e);
}
}
}
6. ULID (Universally Unique Lexicographically Sortable Identifier)
原理:结合 UUID 和 Snowflake 优点,生成 128 位 ID:
- 48 位时间戳(毫秒级,有序)
- 80 位随机数(保证唯一性)
优点:字符串形式、有序性、高性能
Java 实现(需引入依赖):
import com.github.f4b6a3.ulid.UlidCreator;
public class ULIDGenerator {
public static String generate() {
return UlidCreator.getUlid().toString();
}
}
Maven 依赖:
<dependency>
<groupId>com.github.f4b6a3</groupId>
<artifactId>ulid-creator</artifactId>
<version>5.0.0</version>
</dependency>
选择建议
方案 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
UUID | 本地生成、简单 | 无序、占用空间大 | 缓存键、日志追踪 |
数据库自增 ID | 有序、简单 | 单点故障、性能低 | 小规模系统、数据库主键 |
Snowflake | 高性能、有序 | 时钟回拨问题 | 高并发场景(如订单 ID) |
Redis INCR | 高性能、分布式 | 依赖 Redis | 分布式计数器、全局 ID |
号段模式 | 高性能、数据库压力小 | 号段浪费 | 中大规模系统 |
ULID | 有序字符串、高性能 | 需引入依赖 | 分布式系统唯一标识 |
根据业务需求选择合适的方案,通常需要权衡性能、有序性、唯一性和系统复杂度。
RPC
RPC(Remote Procedure Call,远程过程调用)是一种允许程序调用远程计算机上的服务或方法的技术,使得远程调用看起来就像本地调用一样简单。以下是关于 RPC 的详细介绍:
一、核心概念
基本原理:
- 本地调用:程序直接调用本地函数 / 方法。
- 远程调用:通过网络将请求发送到远程服务器,服务器执行相应方法后返回结果。
核心组件:
- 客户端(Client):发起调用的程序。
- 服务端(Server):提供服务的程序。
- 客户端存根(Client Stub):负责将调用参数打包(序列化)并发送请求。
- 服务端存根(Server Stub):接收请求、解包参数并调用实际服务。
- 网络传输层:负责数据传输(如 TCP/IP、HTTP)。
关键特性:
- 透明性:调用远程方法与本地方法语法一致。
- 可靠性:处理网络异常、超时等问题。
- 高性能:减少网络开销,优化序列化 / 反序列化。
二、常见 RPC 框架
gRPC:
- 语言:支持多语言(Java、Python、Go 等)。
- 协议:基于 HTTP/2,使用 Protocol Buffers 序列化。
- 特点:高性能、强类型、支持流式通信。
Apache Dubbo:
- 语言:主要支持 Java。
- 协议:支持多种协议(Dubbo、REST、gRPC 等)。
- 特点:服务治理能力强(负载均衡、服务发现、熔断等)。
Thrift:
- 语言:多语言支持。
- 协议:二进制协议,支持多种传输层。
- 特点:Facebook 开源,适合跨语言场景。
JSON-RPC/XML-RPC:
- 协议:基于 JSON/XML 的简单 RPC 协议。
- 特点:轻量、易实现,适合简单场景。
Spring Cloud:
- 语言:主要支持 Java。
- 协议:基于 HTTP(RESTful)。
- 特点:微服务生态完善(服务发现、配置中心等)。
三、工作流程
客户端:
- 调用本地代理方法(Client Stub)。
- 参数序列化(如转为 JSON、Protobuf)。
- 通过网络发送请求到服务端。
服务端:
- 接收请求并反序列化参数。
- 调用实际服务方法。
- 将结果序列化并返回给客户端。
四、核心技术
序列化与反序列化:
- 将对象转换为字节流以便传输,常见方式:
- 文本格式:JSON、XML。
- 二进制格式:Protocol Buffers、Thrift、Kryo。
- 语言特定:Java 序列化、Python Pickle。
- 将对象转换为字节流以便传输,常见方式:
服务发现:
- 客户端如何找到服务端?
- 静态配置:硬编码服务地址。
- 注册中心:服务启动时注册到中心(如 ZooKeeper、Nacos、Consul)。
- 客户端如何找到服务端?
负载均衡:
- 多个服务实例时,如何选择?
- 轮询:按顺序依次选择。
- 随机:随机选择一个实例。
- 加权:根据实例性能分配权重。
- 多个服务实例时,如何选择?
熔断与限流:
- 熔断:当服务不可用时,快速失败避免级联故障。
- 限流:限制请求速率,保护服务不被压垮。
五、优缺点
- 优点:
- 简化分布式系统开发,降低复杂度。
- 提高开发效率,屏蔽网络细节。
- 支持跨语言、跨平台调用。
- 缺点:
- 增加系统复杂度(网络延迟、故障处理)。
- 调试和排查问题难度大。
- 过度依赖 RPC 可能导致服务间耦合度高。
六、Java 实现示例(gRPC)
- 定义服务接口(.proto 文件):
syntax = "proto3";
package example;
// 定义请求和响应消息
message HelloRequest {
string name = 1;
}
message HelloResponse {
string message = 1;
}
// 定义服务
service Greeter {
rpc SayHello (HelloRequest) returns (HelloResponse);
}
生成代码:
- 使用
protoc
编译.proto 文件,生成客户端和服务端代码。
- 使用
服务端实现:
import io.grpc.Server;
import io.grpc.ServerBuilder;
import io.grpc.stub.StreamObserver;
public class HelloServer {
private Server server;
private void start() throws Exception {
server = ServerBuilder.forPort(50051)
.addService(new GreeterImpl())
.build()
.start();
System.out.println("Server started, listening on " + 50051);
}
private static class GreeterImpl extends GreeterGrpc.GreeterImplBase {
@Override
public void sayHello(HelloRequest req, StreamObserver<HelloResponse> responseObserver) {
HelloResponse reply = HelloResponse.newBuilder()
.setMessage("Hello " + req.getName())
.build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
}
}
public static void main(String[] args) throws Exception {
final HelloServer server = new HelloServer();
server.start();
server.blockUntilShutdown();
}
private void blockUntilShutdown() throws InterruptedException {
if (server != null) {
server.awaitTermination();
}
}
}
- 客户端实现:
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
public class HelloClient {
private final ManagedChannel channel;
private final GreeterGrpc.GreeterBlockingStub blockingStub;
public HelloClient(String host, int port) {
channel = ManagedChannelBuilder.forAddress(host, port)
.usePlaintext()
.build();
blockingStub = GreeterGrpc.newBlockingStub(channel);
}
public void shutdown() throws InterruptedException {
channel.shutdown().awaitTermination(5, TimeUnit.SECONDS);
}
public void greet(String name) {
HelloRequest request = HelloRequest.newBuilder().setName(name).build();
HelloResponse response;
try {
response = blockingStub.sayHello(request);
} catch (StatusRuntimeException e) {
System.err.println("RPC failed: " + e.getStatus());
return;
}
System.out.println("Greeting: " + response.getMessage());
}
public static void main(String[] args) throws Exception {
HelloClient client = new HelloClient("localhost", 50051);
try {
String user = "World";
if (args.length > 0) {
user = args[0];
}
client.greet(user);
} finally {
client.shutdown();
}
}
}
七、适用场景
- 微服务架构中不同服务间的通信。
- 分布式系统中跨节点的方法调用。
- 异构系统间的集成(如 Java 服务调用 Python 服务)。
八、对比 REST API
特性 | RPC | REST API |
---|---|---|
调用方式 | 像本地方法一样调用 | 通过 HTTP 请求调用 |
协议 | 自定义协议(如 HTTP/2) | 基于 HTTP 协议 |
数据格式 | 二进制(如 Protobuf) | 文本(如 JSON/XML) |
性能 | 通常更高(二进制) | 较低(文本格式) |
灵活性 | 较低(强类型) | 较高(URL 资源导向) |
跨语言 | 需生成代码 | 天然支持 |
适用场景 | 内部服务调用 | 对外 API 暴露 |
根据场景选择合适的通信方式,两者并非互斥,可结合使用。
Protocol Buffers vs Thrift
Protocol Buffers(简称 Protobuf)和 Apache Thrift 都是用于序列化结构化数据的高性能框架,广泛应用于分布式系统和 RPC 通信中。以下是它们的详细对比:
一、核心特性对比
特性 | Protocol Buffers | Thrift |
---|---|---|
开发公司 | Google(2001 年) | Facebook(2007 年)→ Apache |
语言支持 | 多语言(Java、Python、Go、C++ 等) | 多语言(Java、Python、Go、C++、PHP 等) |
协议类型 | 仅序列化协议 | 序列化协议 + RPC 框架 |
IDL 语法 | .proto 文件,简洁 |
.thrift 文件,更复杂(支持服务定义) |
序列化速度 | 极快(二进制编码优化) | 快(略慢于 Protobuf) |
压缩率 | 高(二进制格式紧凑) | 高(二进制格式) |
版本兼容性 | 良好(支持字段增删改,需遵循规则) | 良好(支持字段增删改,需遵循规则) |
元数据 | 需生成代码(.proto → 语言特定类) |
需生成代码(.thrift → 语言特定类) |
扩展能力 | 通过oneof 、map 等灵活扩展 |
通过struct 、union 、service 扩展 |
二、序列化性能
- Protobuf:
- 二进制格式更紧凑,序列化速度更快。
- 生成的代码更优化,内存占用更小。
- Thrift:
- 性能略低于 Protobuf,但支持多种传输协议(如 Binary、Compact、JSON)。
三、IDL(接口定义语言)
Protobuf 示例
syntax = "proto3";
message Person {
string name = 1;
int32 id = 2;
string email = 3;
}
Thrift 示例
struct Person {
1: required string name,
2: required i32 id,
3: optional string email
}
service PersonService {
Person getPerson(1: i32 id),
void savePerson(1: Person person)
}
四、RPC 集成
- Protobuf:
- 需结合 gRPC 等 RPC 框架使用。
- gRPC 基于 HTTP/2,提供高性能、流式通信能力。
- Thrift:
- 内置 RPC 支持,提供多种传输协议和服务模型。
- 支持同步、异步、单线程、多线程服务模型。
五、适用场景
场景 | Protobuf + gRPC | Thrift |
---|---|---|
内部微服务通信 | 推荐(高性能、强类型) | 推荐(内置 RPC,多语言支持) |
异构系统集成 | 推荐(跨语言支持) | 推荐(跨语言支持更丰富) |
需要多种协议 | 需额外开发(仅支持 HTTP/2) | 内置多种协议(Binary、Compact、JSON) |
快速开发 | 需分别实现序列化和 RPC | 一站式解决方案(IDL 定义服务) |
大数据量传输 | 推荐(压缩率高、性能优) | 推荐(性能接近) |
六、优缺点总结
Protocol Buffers
- 优点:
- 性能卓越,序列化速度快。
- 社区活跃,支持语言广泛。
- 与 gRPC 深度集成,生态完善。
- 缺点:
- 仅专注于序列化,需额外集成 RPC 框架。
- 二进制格式可读性差,调试复杂。
Thrift
- 优点:
- 一站式解决方案(序列化 + RPC)。
- 支持更多传输协议和服务模型。
- 内置服务发现和负载均衡能力。
- 缺点:
- 性能略低于 Protobuf。
- 社区活跃度低于 Protobuf。
七、选择建议
优先选择 Protobuf + gRPC:
- 若项目已采用 gRPC 或需要极致性能。
- 仅需序列化功能,计划自行集成 RPC 框架。
- 主要使用 Java、Go、Python 等主流语言。
优先选择 Thrift:
- 需要一站式 RPC 解决方案。
- 需支持 PHP、Ruby 等小众语言。
- 需要灵活选择传输协议(如 JSON、Binary)。
混合使用:
- 不同服务间根据需求选择(如内部服务用 Protobuf,对外服务用 Thrift)。
八、性能对比测试
测试场景 | Protobuf (ms) | Thrift (ms) | JSON (ms) |
---|---|---|---|
序列化 1000 对象 | 12 | 15 | 35 |
反序列化 1000 对象 | 8 | 10 | 40 |
数据大小(KB) | 28 | 32 | 45 |
(数据来源:第三方性能测试,仅供参考)
九、总结
两者都是优秀的序列化框架,选择取决于具体需求:
- Protobuf:性能之王,适合追求极致性能的场景,需与 RPC 框架结合。
- Thrift:功能全面,适合需要一站式解决方案的场景,支持更多协议和语言。
客户端在不知道调用细节的情况下,调用存在于远程计算机上的某个对象,就像调用本地应用程序中的对象一样。
比较正式的描述是:一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。
RPC: (Remote Procedure Call) 采用客户端/服务器方式(请求/响应),发送请求到服务器端,
服务端执行方法后返回结果。优点是跨语言跨平台,缺点是编译期无法排错,只能在运行时检查。
常见的 RPC 框架:阿里的 Dubbo,Google 开源的 gRpc,百度开源的 brpc,Dubbo等。
RPC 组成架构
一个完整的 RPC 架构里包含了四个核心的组件,分别是 Client、Client Stub、Server 以及 Server Stub,
这个 Stub 可以理解为存根。
- Client(客户端):服务的调用者
- Client Stub(客户端存根):存放服务端的地址消息,
再将客户端的请求参数打包成网络消息,然后通过网络远程发送给服务方
- Server(服务端):服务的提供者
- Server Stub(服务端存根):接收客户端发送过来的消息,将消息解包,并调用本地的方法
链接:https://juejin.cn/post/7029481269415116813
总结:
分布式系统是由多个通过网络连接的独立计算机节点组成的系统,这些节点相互协作,共同完成任务,对外表现为一个单一系统。分布式系统的特点包括分布性、并发性、故障独立性和透明性。然而,分布式系统也面临网络延迟、数据一致性、故障处理等挑战。常见的应用场景包括大型网站、大数据处理和云计算平台。分布式事务的实现方案包括两阶段提交(2PC)、三阶段提交(3PC)、TCC、消息队列和Saga模式等。接口的幂等性和解决重复消费问题在分布式系统中尤为重要,常见的解决方案包括数据库唯一索引、Token+Redis机制、乐观锁、分布式锁和状态机等。分布式唯一ID的生成方案有UUID、数据库自增ID、Snowflake算法、Redis原子操作、数据库号段模式和ULID等。RPC(远程过程调用)技术允许程序调用远程计算机上的服务,常见的RPC框架包括gRPC、Apache Dubbo、Thrift和Spring Cloud等。RPC的核心组件包括客户端、服务端、客户端存根和服务端存根,其工作流程涉及参数序列化、网络传输和结果反序列化。RPC的优缺点包括简化分布式系统开发、提高开发效率,但也增加了系统复杂度和调试难度。