当前位置: 首页 > news >正文

Java八股文——系统场景设计

如何设计一个秒杀场景?

面试官您好,设计一个秒杀系统,是对一个工程师综合技术能力的巨大考验。它的核心挑战在于,如何在极短的时间内,应对超高的并发请求,同时保证数据(尤其是库存)的强一致性,并尽可能地提升用户体验

我的设计思路会遵循一个核心原则:层层过滤,逐级削减流量,将压力尽可能地拦截在到达最终数据库之前。

我会从 前端优化、接入层防护、服务层核心处理、以及兜底与监控 这几个层面来构建整个系统。

1. 前端与接入层:第一道防线,过滤无效流量

这一层的目标是,将绝大多数的“无效”和“恶意”流量挡在门外。

  • 页面静态化与CDN加速

    • 将秒杀商品详情页、活动规则页等,尽可能做成静态HTML页面
    • 将这些静态资源(HTML, CSS, JS, 图片)全部部署到CDN上。这样,绝大多数用户的浏览请求,都会由离他们最近的CDN节点来响应,根本不会到达我们的源站服务器。
  • 前端交互优化与限流

    • 按钮定时点亮:秒杀开始前,抢购按钮是灰色不可点击的。前端通过定时器与服务端进行时间同步,在秒杀开始时才点亮按钮,避免用户提前无效点击。
    • 一次点击原则:用户点击抢购按钮后,立即将按钮再次置灰,并显示“处理中…”,防止用户因紧张而疯狂重复点击,产生大量无效请求。
    • 人机验证:加入图形验证码或滑块验证,有效拦截大部分脚本和机器人流量。
  • API网关层限流与熔断

    • 在网关层(如Nginx, Spring Cloud Gateway),配置基于IP、用户ID的限流规则,防止单一来源的攻击。
    • 配置熔断策略,如果后端服务出现异常,能快速熔断,避免雪崩。

2. 服务层核心处理:分而治之,异步削峰

这是秒杀系统的核心,我们需要在这里解决库存扣减高并发下单的问题。

a. 库存预热与读写分离
  • 数据预热:在秒杀开始前,通过一个后台任务,将秒杀商品的库存、价格等热点信息,从数据库中预加载Redis等分布式缓存中。所有的“读”操作,都直接访问Redis,完全不给数据库造成压力。
b. 核心业务逻辑:库存扣减 (Redis + Lua)

这是整个系统中最关键、并发冲突最激烈的一步。我绝对不会直接操作数据库。

  • 方案Redis原子操作 + 库存预扣减
  • 具体实现:我会使用一段 Lua脚本 来保证库存查询和扣减的原子性。这个脚本会做以下几件事:
    1. 获取指定商品ID的库存量。
    2. 判断库存是否大于0。
    3. 如果大于0,就将库存减一。
    4. 同时,可以将抢到资格的用户ID,存入一个Redis的Set集合中,用于后续快速判断用户是否已抢购。
    5. 如果库存不足,或用户已在Set中,则直接返回失败。
  • 优点:Redis的单线程模型和Lua脚本的原子性,确保了在高并发下库存扣减的绝对正确性。Redis的内存操作性能极高,可以轻松应对数十万QPS的请求。
c. 下单异步化:消息队列削峰填谷

库存扣减成功,只是代表用户获得了抢购资格,而不是订单创建成功。

  • 流程
    1. 当上述Redis Lua脚本执行成功后,服务层不会立即去数据库创建订单。
    2. 而是会生成一个包含用户ID、商品ID等信息的下单消息,并将其快速地发送到消息队列(如RocketMQ或Kafka) 中。
    3. 然后,立即向前端返回一个“抢购排队中,请稍后查看订单”的友好提示。
  • 优点
    • 极致削峰:MQ在这里扮演了一个巨大的“缓冲区”。无论前端的瞬时请求有多高,后端生成订单的速度,完全由我们自己的消费者集群的处理能力来决定。
    • 服务解耦:将“抢购资格校验”和“订单创建”这两个步骤解耦,提升了系统的整体可用性。

3. 后端持久化与数据一致性

  • 订单服务消费消息
    • 订单服务作为消费者,平稳地从MQ中拉取下单消息。
    • 创建订单:为用户生成订单记录,写入数据库。
    • 真正扣减数据库库存:执行UPDATE product SET stock = stock - 1 WHERE product_id = ? AND stock > 0;。这里可以使用乐观锁(如带version字段)来保证并发更新的正确性。
  • 保证最终一致性
    • 消息幂等消费:消费者必须实现幂等,使用订单号或一个唯一的请求ID作为唯一键,防止重复创建订单。
    • 定期对账:通过定时任务,定期地比较Redis中的预扣减库存和数据库中的真实库存,如果发现不一致,及时发出告警并进行人工干预或自动校准。

4. 兜底与监控

  • 服务降级与限流:如果系统压力过大,可以随时开启降级开关,比如对于未抢到的用户,直接返回一个友好的“已售罄”静态页面,不再请求后端服务。
  • 全链路监控:对整个系统的各个环节(CDN命中率、网关QPS、Redis命中率、MQ堆积量、数据库慢查询等)进行实时监控,并设置告警,确保能第一时间发现并定位问题。

总结我的设计方案

  1. 前端:CDN + 静态化 + 交互限流,过滤90%以上的无效流量。
  2. 网关:IP/UID限流,拦截恶意攻击。
  3. 服务层
    • :通过Redis缓存预热,抗住海量查询。

    • 写(库存):通过Redis + Lua脚本进行原子性的库存预扣减,解决最核心的并发瓶颈。

    • 写(订单):通过消息队列进行异步化,实现削峰填谷,保护后端数据库。

  4. 数据层:通过乐观锁保证数据库库存一致性,通过幂等消费保证订单不重复创建。
  5. 保障:通过监控告警降级预案,保证系统的稳定运行。

通过这样一套层层递进的方案,我相信可以构建一个能够从容应对千万级流量的、健壮的秒杀系统。


设计题:订单到了半个小时,半个小时未支付就取消

面试官您好,这是一个非常经典的业务场景,核心是实现一个可靠的、高效的延迟任务调度系统。要解决这个问题,有多种技术方案可供选择,它们在实现复杂度、资源开销、可靠性和实时性上各有千秋。

我的选型思路是,从简单到复杂,从单体到分布式,逐一分析各种方案的优劣,并最终选择最适合现代微服务架构的方案。

方案一:定时任务轮询数据库 (最简单,但不推荐)

这是最容易想到的、最朴素的方案。

  • 实现:使用Spring的@Scheduled注解,或者Quartz、XXL-Job等定时任务框架,编写一个定时任务,比如每分钟执行一次。这个任务会去扫描订单表,找出所有创建时间超过30分钟且状态仍为“待支付” 的订单,然后执行取消操作。
  • 优点
    • 实现极其简单,开发成本低。
  • 缺点
    • 性能差,数据库压力大:频繁地全表或大范围扫描,会对数据库造成巨大的、不必要的压力,尤其是在订单量巨大时,这几乎是不可接受的。
    • 实时性差,精度低:任务的执行有延迟。比如,一个订单在10:00:01就超时了,但如果定时任务在10:01:00才执行,那么就存在近1分钟的延迟。
    • 并发问题:在集群环境下,需要处理任务的分布式锁问题,防止多个节点同时扫描和处理,导致重复取消。

结论:此方案只适用于数据量极小、并发度极低的早期系统,在生产环境中基本不予考虑。

方案二:基于内存的延迟队列 (JDK DelayQueue)

为了避免扫描数据库,我们可以把延迟任务放到内存中。

  • 实现:当一个订单创建后,将一个包含“订单ID”和“30分钟后过期时间”的任务对象,放入JDK的java.util.concurrent.DelayQueue中。这是一个无界的、线程安全的阻塞队列,只有当任务的延迟时间到了,才能从队列中被取出。后台会有一个专门的线程循环地从这个队列中take()任务,取出来就执行取消操作。
  • 优点
    • 性能高,实时性好:内存操作,效率极高。任务到期后能被立即感知和处理,非常精准。
    • 无数据库压力:完全避免了对数据库的轮询。
  • 缺点
    • 内存限制:所有未到期的订单任务都必须存在内存中,如果待支付订单量巨大,会消耗大量内存。
    • 数据丢失风险(致命缺陷):由于没有持久化,一旦服务宕机或重启,内存中所有的延迟任务都会全部丢失,导致这些订单永远无法被自动取消。

结论:此方案因其数据丢失的致命缺陷,在需要可靠性的生产环境中,也不适用

方案三:时间轮算法 (Time Wheel)

这可以看作是方案二的一种高效实现,它解决了DelayQueue在任务量巨大时,因堆的调整可能带来的性能问题。

  • 实现:Netty的HashedWheelTimer是时间轮算法的经典实现。它将时间刻度(比如一年)划分成一个环形的“轮子”,每个“槽(slot)”代表一个时间单位(比如1秒)。延迟任务根据其到期时间,被放入到对应槽位的链表中。一个指针会随着时间一格一格地转动,当指针扫过一个槽位时,就执行该槽位链表中的所有任务。
  • 优缺点:与DelayQueue类似,性能极高,但同样存在内存限制数据丢失的问题。

结论:时间轮算法是一种非常高效的单机延迟任务调度模型,但在分布式和高可用的场景下,同样受限于其非持久化的特性。

方案四:基于Redis的延迟方案

为了解决数据丢失问题,我们需要一个持久化的、分布式的存储。Redis提供了几种可行的方案。

  • a. ZSet (有序集合)

    • 实现:将订单ID作为member,将订单的过期时间戳作为score,存入一个ZSet中。然后,用一个定时任务去轮询这个ZSet,通过zrangebyscore命令,查询出所有score小于当前时间戳的订单,然后执行取消。
    • 优点:利用了Redis的持久化,解决了数据丢失问题。性能远高于直接扫数据库。
    • 缺点:本质上还是轮询,存在延迟和精度问题。
  • b. Keyspace Notifications (键空间通知 / 过期回调)

    • 实现:当一个订单创建时,向Redis中SET一个key(如 order:expire:orderId),并为其设置30分钟的过期时间(TTL)。然后,利用Redis的发布订阅功能,订阅__keyevent@0__:expired 这个特殊的频道。当这个key因过期被Redis删除时,我们的服务会收到一个通知,然后执行取消订单的逻辑。
    • 优点实时性非常好,由Redis主动通知,避免了轮询。
    • 缺点
      • 可靠性不高:Redis官方文档明确指出,这种过期事件的通知是不保证100%送达的。如果通知在网络中丢失,订单就漏掉了。
      • 中心化瓶颈:所有过期事件都由一个订阅客户端处理,可能成为瓶颈。

结论:Redis方案解决了持久化问题,但要么存在轮询的延迟,要么存在通知不可靠的问题,都不是最完美的方案。

方案五:基于消息队列 (MQ) 的延迟消息 (最终推荐方案)

这是目前业界最成熟、最可靠的分布式延迟任务解决方案。

  • 实现:利用MQ自带的延迟消息功能。
    • RocketMQ:原生支持延迟消息,可以指定延迟级别。
    • RabbitMQ:可以通过“消息TTL + 死信队列(DLX)”的组合来巧妙地实现。即,将订单消息发送到一个设置了30分钟TTL的队列A,不让任何消费者消费它。30分钟后,这条消息会自动过期,并成为“死信”,被路由到与之绑定的死信队列B。我们只需要让一个消费者来正常地消费死信队列B,就能实现延迟30分钟处理订单的效果。
  • 流程
    1. 用户下单后,生产者向MQ发送一条延迟30分钟的消息,消息内容为订单ID。
    2. 30分钟后,这条消息被MQ投递给消费者。
    3. 消费者收到消息后,先去数据库查询该订单的状态。
    4. 如果订单状态仍然是“待支付”,则执行取消订单操作。
    5. 如果订单状态已经是“已支付”或“已取消”,则直接忽略该消息(实现幂等)。
  • 优点
    • 高可用、高可靠:MQ集群保证了消息的持久化和高可用,不会因单点故障丢失任务。
    • 性能好,可扩展:消费端可以水平扩展,轻松应对海量订单。
    • 与业务解耦:将延迟调度逻辑从业务系统中剥离出来,由专业的MQ负责,架构清晰。

最终选型

综合来看,基于消息队列的延迟消息/死信队列方案,是实现此类需求的最佳实践。它在可靠性、可扩展性、实时性和系统解耦方面,都远优于其他方案,是构建现代化、高可用分布式系统的首选。


如果做一个大流量的网站,单Redis无法承压了如何解决?

面试官您好,当一个网站的流量大到单机Redis无法承受时,这通常意味着我们遇到了两种类型的瓶颈之一,或者是两者的叠加:

  1. 读压力过大:大量的读请求(GET, HGET等)耗尽了单机Redis的CPU和网络资源。
  2. 写压力或数据量过大:大量的写请求,或者需要缓存的数据量超过了单台服务器的内存上限。

针对不同的瓶颈,我会采用不同的、层层递进的架构演进方案。

方案一:主从复制 + 读写分离 (解决高并发“读”的瓶颈)

如果系统的瓶颈主要是读请求非常多,而写请求相对较少,那么主从复制读写分离是首选的、最简单的扩展方案。

  • 架构设计

    1. 部署一个主节点(Master)一个或多个从节点(Slave)
    2. 所有的写操作SET, HSET等)都必须在Master节点上进行。
    3. Master节点会自动地、异步地将所有数据变更复制(Replicate) 给所有的Slave节点。
    4. 所有的读操作,则可以分摊到所有的Slave节点上去执行。
  • 优点

    • 架构简单:实现和运维相对简单。
    • 读能力水平扩展:通过增加Slave节点的数量,可以线性地扩展系统的读并发能力。
    • 高可用基础:主从架构也是实现高可用的基础。如果配合哨兵(Sentinel) 机制,当Master宕机时,Sentinel可以自动地从Slave中选举出一个新的Master,实现故障转移。
  • 缺点

    • 写能力无扩展:所有的写操作压力,依然全部集中在唯一的Master节点上。
    • 数据量受限:所有节点(主和从)都拥有全量的数据副本,所以整个集群的数据存储能力,受限于单台服务器的内存大小
    • 数据一致性问题:主从复制是异步的,存在一定的延迟。在极端情况下,可能会从Slave上读到旧的数据。

结论:当瓶颈是读密集型,且数据总量不大时,主从读写分离是一个性价比非常高的解决方案。

2. Redis Cluster 集群 (解决高并发“写”和“海量数据存储”的瓶颈)

写请求的压力变得巨大,或者数据量大到单机内存无法容纳时,主从复制就无能为力了。这时,我们就必须采用Redis Cluster方案。

  • 核心思想分片(Sharding)。Redis Cluster不再让每个节点都存储全量数据,而是将整个数据集分割成多个部分,分散地存储在不同的节点上。

  • 架构设计

    1. 哈希槽(Hash Slot):Redis Cluster预先将整个键空间划分成了16384个哈希槽
    2. 数据分片:当一个key需要被存入时,集群会使用 CRC16(key) % 16384 这个公式,计算出这个key应该属于哪个哈希槽。
    3. 槽位分配:在集群搭建时,这16384个槽会被均匀地分配给集群中所有的Master节点。比如,有3个Master,那么M1可能负责0-5460槽,M2负责5461-10922槽,M3负责10923-16383槽。
    4. 请求路由:客户端连接到集群中的任意一个节点,当它要操作某个key时,会先计算出该key所属的槽。如果这个槽正好由当前节点负责,就直接执行。如果不由它负责,节点会返回一个 MOVED重定向指令,告诉客户端应该去哪个正确的节点执行操作。智能的客户端(如Jedis Cluster)会自动处理这个重定向。
  • 优点

    • 读写能力双重扩展:通过增加Master节点的数量,可以将数据和请求压力分散出去,从而同时实现了读能力和写能力的水平扩展
    • 海量数据存储:集群的总数据存储能力,是所有Master节点内存容量的总和,可以轻松支持TB级别的数据量。
    • 内置高可用:Redis Cluster的每个Master节点,都可以配置一个或多个Slave节点。当Master宕机时,集群会自动将其中的一个Slave提升为新的Master,实现了内置的、去中心化的故障转移,无需Sentinel。
  • 缺点

    • 实现复杂:相比主从模式,集群的运维和管理更复杂。
    • 部分功能受限:一些涉及多个key的命令(如MSET, MGET),如果这些key不属于同一个哈希槽,就无法执行。事务和Lua脚本也只能在单个节点上原子执行。

总结与选型

方案解决的核心问题优点缺点
主从复制+读写分离高并发读架构简单,读能力线性扩展写能力和数据量受限于单机
Redis Cluster高并发写 + 海量数据存储读写能力和数据量均可水平扩展,内置高可用架构复杂,部分多Key操作受限

在实践中,我们的架构演进路径通常是:

  1. 初期单机Redis
  2. 流量增长,读压力变大:演进为主从复制 + 读写分离 + Sentinel高可用
  3. 业务继续增长,写压力或数据量成为瓶颈:最终演进为Redis Cluster集群

如何设计一个可重入的分布式锁,用什么结构设计?

面试官您好,设计一个可重入的分布式锁,是在普通分布式锁的基础上,增加了一个核心特性:允许同一个线程(或进程)在已经持有锁的情况下,可以重复地、安全地获取这把锁,而不会造成自己死锁自己

要实现这一点,我们不仅需要标记“锁被占用了”,还需要记录 “锁是被谁占用的” 以及 “被占用了多少次”

1. 核心数据结构设计 (基于Redis)

使用Redis的哈希表(Hash) 是实现可重入锁最理想的数据结构。

我会定义一个key来代表这把锁(比如 lock:my_resource),这个key对应一个Hash结构,里面包含两个核心字段:

  • owner_id: 存储持有锁的客户端的唯一标识(比如 UUID 或者 IP:ThreadId)。
  • reentrant_count: 一个计数器,记录当前持有者重入的次数。
// Redis中的数据结构示意
"lock:my_resource": {"owner_id": "client-uuid-12345","reentrant_count": 2  // 表示这个客户端重入了2次
}

2. 核心操作流程与原子性保证 (使用Lua脚本)

所有对锁的操作,都必须是原子的,以防止并发下的竞态条件。因此,我会将加锁和解锁的逻辑,都封装在Lua脚本中,通过Redis的EVAL命令来执行。

a. 加锁 (Lock) 的Lua脚本逻辑

当一个客户端(clientId)尝试加锁时,Lua脚本会执行以下逻辑:

  1. 检查锁是否存在 (EXISTS lock_key):

    • 如果锁不存在:说明没有人和它竞争。直接创建一个新的Hash,设置 owner_id 为当前 clientIdreentrant_count 设为 1。同时,为这个key设置一个过期时间(TTL),以防止死锁。加锁成功,返回1
    • 如果锁已存在:进入下一步。
  2. 检查锁的持有者 (HGET lock_key owner_id):

    • 获取当前锁的owner_id,判断它是否等于当前请求的 clientId
    • 如果是同一个客户端(重入):将 reentrant_count 的值加一HINCRBY lock_key reentrant_count 1)。同时,刷新锁的过期时间。加锁成功,返回1
    • 如果不是同一个客户端:说明锁被别人持有,加锁失败。直接返回0

Lua伪代码:

if (redis.call('exists', KEYS[1]) == 0) thenredis.call('hset', KEYS[1], 'owner_id', ARGV[1]);redis.call('hset', KEYS[1], 'reentrant_count', '1');redis.call('expire', KEYS[1], ARGV[2]);return 1;
end;if (redis.call('hget', KEYS[1], 'owner_id') == ARGV[1]) thenredis.call('hincrby', KEYS[1], 'reentrant_count', 1);redis.call('expire', KEYS[1], ARGV[2]);return 1;
end;return 0;
  • KEYS[1] = lock_key
  • ARGV[1] = clientId
  • ARGV[2] = expire_time
b. 解锁 (Unlock) 的Lua脚本逻辑

当一个客户端(clientId)尝试解锁时:

  1. 检查锁是否存在且持有者是自己 (HGET lock_key owner_id):

    • 如果锁不存在,或者锁的owner_id不是当前clientId,说明发生了错误(比如尝试释放别人的锁),直接返回失败(或抛出异常)。不允许释放不属于自己的锁。
    • 如果持有者是自己,进入下一步。
  2. 递减重入次数 (HINCRBY lock_key reentrant_count -1):

    • reentrant_count的值减一
    • 获取递减后的新值。
  3. 判断是否需要真正释放

    • 如果递减后的reentrant_count大于0,说明这只是一次重入的退出,锁还没有完全释放。什么都不做,解锁操作结束。
    • 如果递减后的reentrant_count等于0,说明这是最后一层锁了,需要真正地释放锁。此时,执行DEL lock_key,将整个Hash删除。

Lua伪代码:

if (redis.call('hget', KEYS[1], 'owner_id') == ARGV[1]) thenlocal count = redis.call('hincrby', KEYS[1], 'reentrant_count', -1);if (count > 0) thenreturn count; -- 仍然持有锁elseredis.call('del', KEYS[1]);return 0; -- 锁被释放end;
end;
return -1; -- 尝试释放别人的锁,失败

3. 锁续期与高可用

  • 锁续期(看门狗机制):为了防止业务执行时间超过锁的固定TTL,我会引入一个后台守护线程。当加锁成功后,这个线程会定期地(比如每隔TTL/3的时间)去刷新锁的过期时间,直到锁被主动释放。
  • 高可用:在Redis集群环境下,为了防止单点故障和主从切换导致的锁失效问题,可以考虑使用Redlock算法,但这会增加系统的复杂性。在大多数场景下,一个高可用的Redis主从或集群部署已经足够。

总结我的设计方案

  1. 数据结构:使用Redis的哈希表(Hash)key为锁名,field包含owner_idreentrant_count
  2. 原子性:所有加锁和解锁的核心逻辑,都必须封装在Lua脚本中执行,保证原子性。
  3. 可重入性:通过在Lua脚本中判断owner_id并增减reentrant_count来实现。
  4. 防死锁:在加锁和续期时,都设置合理的过期时间(TTL)
  5. 防误删:解锁时必须校验owner_id
  6. 防超时:通过后台线程实现锁的自动续期

在实际开发中,我通常会直接使用像 Redisson 这样的成熟开源库,因为它已经为我们优雅地实现了以上所有复杂的逻辑,包括可重入、公平/非公平、读写锁以及自动续期等功能,可以让我们更专注于业务开发。


你有看过一些负载均衡的一些方案吗

面试官您好,是的,负载均衡是构建高可用、高可扩展性系统的基石,我了解并研究过多种负载均衡的方案。这些方案通常可以根据其实现层面(硬件 vs. 软件)和工作的网络层次(二层、三层、四层、七层)来进行分类。

1. 基于硬件的负载均衡 (Hardware Load Balancer)

这是最高性能、也是最昂贵的解决方案。

  • 实现:通过专门的硬件设备来实现,比如 F5 的 BIG-IPA10 等。这些设备本质上是经过高度优化的、专用的计算机,集成了强大的网络处理芯片(ASIC)。
  • 工作层次:通常工作在网络层(三层)和传输层(四层),可以进行IP地址和端口号的转发。高端设备也支持应用层(七层) 的内容分发。
  • 优点
    • 性能极强:能够轻松应对千万级甚至更高的并发连接,吞吐量巨大。
    • 功能全面:通常集成了防火墙、SSL卸载、流量整形、防DDoS攻击等多种高级功能。
    • 稳定性高:硬件经过严格测试,非常稳定可靠。
  • 缺点
    • 成本极其昂贵:设备采购和维护成本都非常高,通常只有大型企业或金融机构才会采用。
    • 扩展性差:扩展能力受限于硬件规格,升级或替换成本高。
    • 灵活性低:配置和二次开发不如软件灵活。

2. 基于软件的负载均衡 (Software Load Balancer)

这是目前互联网应用中最主流、应用最广泛的解决方案。它通过在普通服务器上运行软件来实现负载均衡。

a. 工作在四层(传输层)的负载均衡
  • 代表技术LVS (Linux Virtual Server)
  • 核心原理:工作在Linux内核层面,通过修改IP数据包的目标地址(NAT模式)或MAC地址(DR模式),来实现请求的转发。它不关心数据包的具体内容,只做“包的转发”。
  • 优点
    • 性能极高:由于在内核态工作,没有用户态和内核态的切换开销,性能非常接近硬件负载均衡。
    • 稳定性好
  • 缺点
    • 不支持七层的高级功能,无法根据URL、HTTP头等内容进行路由。
    • 配置和运维相对复杂。
b. 工作在七层(应用层)的负载均衡
  • 代表技术Nginx、HAProxy、Apache 等。
  • 核心原理:作为反向代理工作。它会完整地解析应用层协议(如HTTP),然后根据请求的具体内容(如URL路径、Host、HTTP头、Cookie等),做出更智能的路由决策。
  • 优点
    • 功能强大,灵活性极高:可以实现动静分离、URL重写、基于内容的路由、HTTPS证书管理、灰度发布、A/B测试等各种高级功能。
    • 配置简单:相比LVS,配置更简单直观。
    • 成本低廉
  • 缺点
    • 性能相对LVS较低:因为需要解析完整的应用层协议,有更多的CPU消耗和内存开销。但对于绝大多数Web应用来说,其性能已经绰绰有余。

3. 基于DNS的负载均衡

这是最简单、最基础的一种负载均衡方式,工作在网络的入口处。

  • 实现:在DNS服务器上,为同一个域名配置多个不同的IP地址。当用户通过域名访问时,DNS服务器会根据一定的策略(如轮询),返回其中一个IP地址给客户端。
  • 优点
    • 实现极其简单,是实现地理级别(跨数据中心) 负载均衡的基础。
    • 可以将流量引导到离用户最近的服务器,提升访问速度。
  • 缺点
    • 可用性差:DNS服务器无法感知后端服务器的真实健康状况。如果某个IP对应的服务器宕机了,DNS依然可能会将用户导向这个无效的地址。
    • 更新不及时:由于DNS缓存的存在,当后端服务器IP变更时,需要很长时间才能在全网生效。
    • 分配策略简单:通常只能做到简单的轮询,无法根据服务器的真实负载进行动态调整。

4. CDN (内容分发网络)

CDN虽然主要目的是内容加速,但它本质上也包含了一套非常复杂的、全球性的负载均衡系统。

  • 实现:将网站的静态内容(图片、CSS、JS、视频)缓存到全球各地离用户最近的边缘节点上。当用户请求这些资源时,DNS会将其导向最近的CDN节点,由该节点直接响应。
  • 负载均衡的体现
    • 地理级别的负载均衡:将流量分散到全球的边缘节点。
    • 后端回源的负载均衡:当CDN节点需要回源站拉取数据时,其内部也有负载均衡机制来选择最优的回源链路。
  • 优点:极大提升了静态内容的加载速度,并大大减轻了源站服务器的压力。

总结与选型

在典型的互联网架构中,这些方案通常是组合使用的:

  1. 入口层:使用 DNS负载均衡CDN,实现地理级别的流量分发和静态内容加速。
  2. 接入层:在数据中心入口,可能会使用 LVS硬件负载均衡器 作为主要的四层负载均衡,负责承载海量的入口流量。
  3. 应用层:在LVS后面,部署 Nginx集群 作为七层负载均衡和反向代理,负责处理HTTP协议,并根据业务逻辑将请求分发给后端的微服务。
  4. 服务间:在微服务内部,还会使用客户端负载均衡(如Ribbon, Spring Cloud LoadBalancer),由服务消费者自己来决定调用哪个服务提供者实例。

总的来说,没有一种方案能包打天下,我们需要根据业务的流量规模、性能要求、成本预算以及对功能灵活性的需求,来组合使用这些负载均衡技术,构建一个稳定、高效的系统架构。

参考小林 coding

http://www.xdnf.cn/news/1065025.html

相关文章:

  • 多设备Obsidian笔记同步:WebDAV与内网穿透技术高效实现教程
  • 从【人工智能】到【计算机视觉】,【深度学习】引领的未来科技创新与变革
  • Linux->进程概念(精讲)
  • GPU机器安装docker
  • Python下构建毫秒级低延迟RTSP/RTMP播放器并实现AI视觉处理
  • 数据库(1)-SQL
  • EXPLAIN优化 SQL示例
  • Oracle 数据库查询:单表查询
  • 统计用户本月的连续登录天数
  • 62-Oracle ADR(Automatic Diagnostic Repository)
  • 量化-因子处理
  • 【递归,搜索与回溯算法】记忆化搜索(二)
  • Vue.js数据代理与事件处理全解析:从原理到实践
  • 【DDD】——带你领略领域驱动设计的独特魅力
  • React基础
  • MakeItTalk: Speaker-Aware Talking-Head Animation——说话者感知的说话头动画
  • 【笔记】Windows 系统迁移 Ubuntu(Preview)应用到其他磁盘
  • Element表格表头合并技巧
  • 第八章 目录一致性协议 A Primer on Memory Consistency and Cache Coherence - 2nd Edition
  • Bytemd@Bytemd/react详解(编辑器实现基础AST、插件、跨框架)
  • 分库分表下的 ID 冲突问题与雪花算法讲解
  • JVM(10)——详解Parallel垃圾回收器
  • python高校教务管理系统
  • 超详细YOLOv8/11图像菜品分类全程概述:环境、数据准备、训练、验证/预测、onnx部署(c++/python)详解
  • TypeScript类型定义:Interface与Type的全面对比与使用场景
  • 【HarmonyOS Next之旅】DevEco Studio使用指南(三十六) -> 配置构建(三)
  • 算法导论第二十五章 深度学习的伦理与社会影响
  • C4.5算法深度解析:决策树进化的里程碑
  • 怎么让二级域名绑定到wordpesss指定的页面
  • 0-机器学习简介