一、项目问题
1、如何对所有分布式节点的应用,活动信息本地内存更新?
通常我们会有诉求在不重启系统的时候,就要动态变更所有分布式应用节点中某个属性的值,如开关、缓存、调试日志开启/关闭、熔断、限流、或者抽奖黑名单以及概率等。这些东西通常不是 Redis 存储,而是应用中具体字段的属性值,这样效率更高。
而这个操作需要使用到类似于 Zookeeper 组件的临时节点监听,动态变更字段值。
2、应用刚启动完成,外部调用过程中发现操作数据库连接池不足,超时断开,过一会又好了?是什么问题?
因为它是刚开始有问题,过一会又好了,所以很有可能是池化的连接数配置的最小值与最大值不是一个,这样应用就会先初始一个最小范围的连接数,随着调用没了在初始化到最大连接数。所以一般我们会把最小连接数和最大连接数配置为一个,避免使用的时候还需要初始化。因为初始化连接也是需要花费时间的。【再有注意配置链接的超时时间,不要太小,也不要太大】
3、消费MQ的过程中,如果使用多线程会遇到什么问题?
这是一个非常容易产生事故的问题,本身 MQ 消费就是多个应用分别消费,如果有消费失败的,可以抛异常重试。但如果是一个消费 MQ 的应用,里面写了多线程,就可能会出现大量的 MQ 挤压,消费不过来,导致系统瘫痪。而如果你重启,那么这些拉下来的 MQ 消息也就随时丢失了。
4、分库分表怎么让任务扫描到指定的库表?
分库分表以后,需要扫描每个库表中的任务表,则需要手动设定具体要扫描的库和表。如果分库分表的数量比较多,可以用不同的任务配置扫描不同的库表方式来部署,这样可以提高扫描效率。
如果说扫描出来的数据需要更低的延迟性,可以考虑做低延迟任务调度设计
5、如果在多机部署的情况下,是不是每台机器都会有这个定时任务,如果它们都捞到同一条发送失败的消息,会不会导致消息的重复发送?怎么避免?
-
幂等的这个是ok的,没问题的。
-
一个任务就是要有多机备份,避免一个挂了,就没有人执行了。之后这里的方案是加锁;
- 2.1 设计一个抢占锁,多个任务抢占同一个锁,谁抢占到了,谁可以执行。
- 2.2 如果抢占的执行失败了,删掉锁,重新执行。
- 2.3 如果删锁失败,对于是谁抢占的,谁可以做重入锁,继续执行。
- 2.4 锁有失效时间,如果抢占到的自己挂了,等待锁失效后,重新轮候抢占。
6、关于幂等和流程解耦
1. rabbitMQ判断重复消费的逻辑是什么,是直接在数据库中查询返利记录表是否有相同的订单ID(这个订单ID对应着当天日期?)的记录,如果发现重复就不消费?
每一条mq,都有业务对应的唯一字段。可以查询、缓存,来做一层优化,或者数据库还会有这个字段的唯一索引拦截。
2. 感觉“签到返利”这一块也有点像幂等性的实现,即可能会执行多次签到动作,但每天只会执行一次返利操作?
对的,每天一条签到返利,有一个 outBusinessId 透传业务唯一索引。签到是每日的,那么每天就是一套记录对应的所以你记录。
7、策略领域和奖品领域为什么要划分开
- 这部分要从建模讲,最开似的系统建模是整理出用户用例图,根据用例图梳理四色建模的领域事件。
- 在领域事件脑暴完成,之后就是识别领域角色和对象,这个过程会显而易见的发现有抽奖领域、发奖领域。他们可以作为解耦设计,独立使用。因为抽奖不一定发奖,抽奖可以独立提供算法结果,由外部其他系统使用。发奖也可以除了抽奖的发奖,还有积分兑换的发奖等。如果抽奖和发奖合并,那么外部调用就会不那么清晰。
8、如果把redis中 滑块锁过期时间设置为活动过期时间的时候,如果活动时间很长导致滑块锁过多怎么解决?
可以考虑给活动库存的锁的key上年月日,每个日的key,明天就重新从新的key开始了。之后这样就比较好较短时间存储了。
9、让你设计一个发红包点红包场景,该怎么设计接口和表
在Java中设计高并发、高可用的发红包/抢红包场景时,核心架构需围绕用户权限校验、金额精准计算、库存并发控制和数据一致性展开。表结构上,通过user表管理用户余额与状态,red_packet表记录红包总额、剩余数量及生命周期,win_record表存储中奖明细并利用联合唯一索引防重抢;接口设计中,发红包接口采用事务确保扣款与红包创建的原子性,抢红包接口通过分布式锁(如Redis的SETNX)或数据库乐观锁(版本号+剩余量联合更新)解决超卖问题,并结合Redis预扣库存提升性能;高并发优化方面,以Redis原子操作(DECRBY)快速扣减红包库存,异步消息队列(如Kafka)解耦余额更新与业务逻辑,定时任务清理过期红包;安全设计上,使用BigDecimal规避浮点误差,通过唯一约束和状态校验(如红包过期、库存耗尽)防止非法操作,最终结合分库分表(按用户ID分片红包表、按时间分片中奖表)和配置中心实现灵活扩展,满足亿级流量下的稳定性与效率需求。
10、分布式锁使用场景
项目中有两个地方使用了分布式锁,分别来处理库存的抢占竞争和分布式任务调度的抢占。
- 库存的抢占设计的是接近于无锁化的库存编号自增后加锁,做兜底设计,这样的用户的抢占就是 incr 后的结果加锁,降低竞争。
- 另外一个是项目是分布式架构,有多个任务执行(补偿mq、流转订单状态等),之后如2个任务,一起补偿发mq,避免发送多了,就会做一个抢占设计。谁先拿到可执行key,那么这个任务就执行。这样确保了,一个任务挂了,也可以有另外任务做处理。
11、【美团】领域模型是怎么设计的,抽奖过程是什么样,DDD四层架构和职责,以及为什么要这么设计?少卖和超卖。
1. 领域模型是怎么设计的
领域模型官方话术是头脑风暴,罗列事件和行为,在根据实体来划分领域的。简单说也就是根据业务流程划分的。这个过程包括;活动域、抽奖域、积分域、兑换域。也就是用户通过某种行为记录,发放计算,兑换活动参与资格,完成抽奖获得奖品。
2. 关于 DDD 的分层结构介绍,和每层的关系
1. 架构分层(DDD)
在 DDD 架构分层中,domain 模块最重要的,也是最大的那个。所有的其他模块都要围着它转。所有 domain 下的各个领域模块,都包含着一组完整的:model - 模型对象、service - 服务处理,以及在有需要操作数据库时,再引入对应的 IRepository - 仓储服务。这个 domain 的实现,就像是实现了一个炸药包,炸药包的火药、引线、包布等都是一个个物料被封装到一起使用。
如下是 DDD 架构所呈现出的一种四层架构分层,可能和一些其他的 DDD 分层略有差异,但核心的重点结构是不变的。尤其是 domain 领域、infrastructure 基础,是任何一个 DDD 架构分层都需要有的分层模块。
- 应用封装 - app:这是应用启动和配置的一层,如一些 aop 切面或者 config 配置,以及打包镜像都是在这一层处理。你可以把它理解为专门为了启动服务而存在的。
- 接口定义 - api:因为微服务中引用的 RPC 需要对外提供接口的描述信息,也就是调用方在使用的时候,需要引入 Jar 包,让调用方好能依赖接口的定义做代理。
- 领域封装 - trigger:触发器层,一般也被叫做 adapter 适配器层。用于提供接口实现、消息接收、任务执行等。所以对于这样的操作,这里把它叫做触发器层。
- 领域编排【可选】 - case:领域编排层,一般对于较大且复杂的的项目,为了更好的防腐和提供通用的服务,一般会添加 case/application 层,用于对 domain 领域的逻辑进行封装组合处理。但对于一些小项目来说,完全可以去掉这一层。少量一层对象转换,代码的维护成本会降低很多。
- 领域封装 - domain:领域模型服务,是一个非常重要的模块。无论怎么做DDD的分层架构,domain 都是肯定存在的。在一层中会有一个个细分的领域服务,在每个服务包中会有【模型、仓库、服务】这样3部分。
- 仓储服务 - infrastructure:基础层依赖于 domain 领域层,因为在 domain 层定义了仓储接口需要在基础层实现。这是依赖倒置的一种设计方式。所有的仓储、接口、事件消息,都可以通过依赖倒置的方式进行调用。
- 外部接口 - gateway:对于外部接口的调用,也可以从基础设施层分离一个专门的 gateway 网关层,来封装外部 RPC/HTTP 等类型接口的调用。
- 类型定义 - types:通用类型定义层,在我们的系统开发中,会有很多类型的定义,包括:基本的 Response、Constants 和枚举。它会被其他的层进行引用使用。(这一层没有画到图中)
3. 少卖和超卖
超卖不会出现,有个保证的点,一个是 decr 值的限制,另外一个是对每个key加锁的兜底设计。确保了不会超卖。少卖是有可能的,核心原因是因为 decr 操作和数据操作不是是一个事务,有可能库存扣减完了,但最终操作库失败了。那么这个库存就丢失了,可能会少卖。但一般并不会对少卖做过多的流程,如果想管理,也可以把少卖的库存异常,加入单独的 redis 队列来重新消费就可以了。
12、【淘天】对于数据库和redis的一致性怎么解决
- 第1个手段;每次消费 decr 值,写入延迟队列,趋势更新数据库数据。最终一致。
- 第2个手段;decr 库存值消耗为0时,发送mq消息,更新最终库存量。(可能不准,比如 decr 值消耗中,少卖的情况)
- 第3个手段;活动到期后,任务扫描活动产生订单量,校准库存。
13、【其他】什么情况下使用DDD架构,什么情况下使用mvc架构?
DDD 是软件设计方法,对复杂的项目更为合适。但这里要清楚,DDD 如果只的是设计方法中的建模工程结构,和 MVC 对比的话。DDD 的结构更为先进,即使不使用 DDD 的软件设计方法,只遵循这套结构,都是可以编写出非常好的代码的。MVC 的约束相对较低,个人开发还好,如果多人协作,会出现腐化严重的问题。
14、【其他】设计模式带来了什么好处?举个例子
设计模式可以让工程设计的迭代性、扩展性、维护性,都更强,更好。如,抽奖策略计算中,用到了责任链、组合模式的规则树。规则树可以动态的调整配置的节点,来满足各种业务诉求。还可以结合产品需求,迭代的时候添加对应的节点开发就可以。避免了大量if...else的使用,让变动范围缩小到指定的类中。研发成本更低,提测质量更高(测试更快),交付效率更强。这些都是使用设计模式的优势。
15、【其他】 抽奖项目按道理应该不是一个高并发的过程,那为啥还要把库存缓存到redis去抗并发呢?
要好好留心日常的场景,都是面试的话术;
- 春晚红包是不是抽奖,拼多多一进页面就有各种【转转转】来获得一个券,支付完成又一个转转转。直接领券远没有抽奖来的刺激,即使是发券,也是用抽奖方式更多。
- 每秒的请求量如果超过1000tps,打到库上资源竞争,都会出现大量的数据库连接等待。一般一个应用分配的数据库连接池也就那么20来个。如果都打到库上,都能把库打挂。
- 面试这么问,大部分是为了通过一个质疑的场景,来看是否有思考过。而不是面试问什么就是什么,反而是问什么就不是什么,但不是什么,要拿出举证理由。
16、【其他】抽奖算法提问
1. 问:数据库路由算法
在 Java 中对数据的散列算法:HashMap 用到的是一次扰动函数下的哈希散列、ThreadLocal 用到的斐波那契散列。而通常数据库路由组件用到的是整数模除法散列,这也是实践中最简单和最常用的方法之一。
答:hashcode保证两次散列结果一致,扰动函数保证散列均匀。
问:除了hashcode数据库路由算法还有其他的吗?
2. 问:抽奖除了加分布式锁还有没有别的思路?
答:这里本质上还是保证库存不超卖,说了低并发下的数据库行锁,redis队列
问:还有其他思路吗?
答:抽奖系统在高并发场景下,除分布式锁外,可通过 “缓存原子操作(如 Redis 扣减库存)结合异步队列削峰” 实现高效控制,例如用 Lua 脚本封装库存校验与发放逻辑,辅以 分桶抽奖(用户哈希分流) 降低单点压力,并通过 限流(令牌桶/漏桶) 和 空转校验(快速失败无效请求) 保障系统稳定性,最终结合 消息队列异步处理 与 数据补偿机制 确保最终一致性,兼顾性能与可靠性。
3. 计次模型,设计一天一次的参与规则,怎么实现?
使用用户日抽奖表限制
4. 抽奖算法的实现?这个随机怎么可控,怎么避免高价的奖品一开始就被抽掉了,类似于活动中间阶段才能中一次一等奖
有次数锁、有权重,可以避免一开始就中一等奖。
5. 了解过其他抽奖算法吗?
算法如;线性同余生成器、梅森旋转算法、洗牌算法、加权随机。
6. 黑名单如果上到一定规模,比如百万级别,有其他的设计思路吗?
对于百万级规模的黑名单,可采用 “分片存储(如 Redis Cluster/分库分表)+ 布隆过滤器快速拦截” 组合设计,通过 冷热分离(近期高频黑名单存缓存,历史数据存数据库) 降低存储压力,结合 布隆过滤器前置过滤误判请求,并利用 消息队列异步同步更新状态 确保最终一致性,同时通过 分层存储(如 Elasticsearch 按时间/规则分类检索) 提升查询效率,避免单点瓶颈。
7. 扣减库存的分段竞态锁用 incr 还是 decr,为什么是 incr/decr?
incr 和总量比,decr 和 0 比。decr 适合过程中不允许补充库存的。incr 可以在过程中添加库存,因为总量可以增加对比。
8. 如果活动开始了,要加奖品库存怎么办?
17、我跟他说用户在抽奖系统用积分兑换抽奖机会,我方会向mq发消息,积分微服务那边拿到消息之后扣减会员的积分,他问我:
1. 你为什么在这种积分扣减用mq?
核心原因:业务解耦与最终一致性
积分扣减作为抽奖流程的关键依赖,若采用同步调用会导致抽奖服务雪崩影响用户体验。通过MQ异步解耦后,抽奖服务可立即响应用户(发放虚拟奖品),积分扣减在独立服务中异步执行,结合Saga事务模式保证最终一致性(30分钟内完成扣分或触发人工补偿)。此外,MQ的集群化部署(如Kafka多分区+ISR机制)支持高吞吐(日均千万级消息)和跨机房容灾,避免单点故障,同时消息持久化保障了操作可靠性。
️2. 如果用户用脚本频繁兑换抽奖机会,你们怎么应对的。
通过积分支付接口返回的信息开始变更订单记录,发放抽奖次数。
- 网关:基于用户等级的动态限流(VIP每小时100次)+ 请求签名防篡改
- 服务层:Redisson分布式锁(毫秒级互斥)+ 实时规则引擎(基础规则+VIP特权)
- 数据层:唯一性约束(用户-兑换类型-时间)+ 异常流水ES存储
3. 如果下游服务迟迟没有对mq进行消息消费,你们怎么处理的?你假设作为系统的架构者,是怎么监控这种状况的。
系统是有边界管控的,在实际工作中,当前的系统要保证发送mq,下游的系统要保证消费mq。mq的消费会有监控配置,比如日常每分钟100次,如果连续n次在n分钟内,低于80次或者高于140次,则进行报警。这样就可以监控到了。
18、【网易】在抽奖过程时,需要预热处理,可是不可能让用户点击活动装配呀,这个问题你怎么解决?
在活动运营配置后,可以审核通过就预热了。等待活动到了有效期就可以使用了。
19、【网易】在抽奖的结果中,如果一个用户在前端抽到了一个奖品,对用户是可见的。可是在扣减库存时,却发现没有了对应的库存,此时应该怎么办?
库表设计中,抽奖的每条策略是有库存限制的,抽到后才会展示给用户。如果库存不足直接就是兜底积分了。这个也有人遇到过反过来问的,为什么要给策略上也加库存,直接奖品加库存不就可以。其实是不可以的。
20、【其他】既然为了想承受更高并发使用redis做库存扣减,那生成奖品id后续要等中奖订单入库才能给用户展示结果吗?那用redis做库存扣减不就没意义了
最核心的其实是,如果没有redis做库存扣减,就要数据库里的一个库存记录做扣减。那么就会有成千上万条的请求在同一个表行记录开始独占竞争加锁,其余的请求进入等待,直至耗尽所有数据库连接。那么整个系统服务也就会被拖垮,一个普通的查询也会从原来的几十毫秒变成到一分钟也拿不到结果了。
使用redis,就是为了解决这个事情。之后在写入库里的记录,都是无竞争的。那么就不会让数据库被夯住。可以快速被处理。在大厂的数据库配置,基本这类操作不会被 redis 慢多少。
在抽奖场景中,我们通过Redis预分配奖品池+异步事件驱动架构实现高性能与实时性的平衡:
- 前端实时响应:用户中奖时直接从Redis预生成的奖品池中原子性获取未使用奖品ID(Lua脚本保证操作的原子性),通过双写Redis Hash表(用户-奖品ID映射+奖品池状态)实现秒级响应,避免数据库瓶颈;
- 异步保障最终一致:用户返回奖品ID后,通过Kafka解耦订单入库、短信通知等下游操作,利用消费者组并行处理(订单入库采用批量JDBC批处理提升吞吐,短信通知通过第三方API异步调用);
- 一致性兜底:通过监控MySQL入库状态,结合Debeziumbinlog监听实现异常数据自动补偿(如5分钟内未入库则触发库存回滚+奖品ID重新分配),最终保证Redis库存与MySQL订单数据一致性;
- 高可用设计:Redis集群多副本+Kafka多分区部署,配合Prometheus监控消费者Lag(>5分钟触发扩容)和重试率(>3次进入死信队列人工处理),实现单活动百万级QPS(P99<200ms)与99.99%成功率。
该设计既发挥了Redis的瞬时高性能,又通过事件驱动和补偿机制确保了核心业务数据的最终一致性,解决了实时响应与异步处理的矛盾。
21、【其他】Redis问题
1. 加分段锁的目的是为了什么?
答:核心目标是为了防止低概率下集群、主从故障导致的超卖,做一个兜底逻辑。
2. 如果不需要补库存,分段锁是不是可以不用加?
答:即使不考虑手动补库存的情况,如果集群、主从故障,不加分段锁还是会可能超卖的,所以这里的分段锁不单单是为了补库存的场景而设计。
3. 那直接 incr 不可以吗?
答:在 redis 集群模式下,incr 请求操作可能发生网络抖动超时返回。这个时候 incr 有可能成功,也有可能失败。可能是请求超时,也可能是请求完的应答超时。那么 incr 的值可能就不准。【实际使用中10万次,可能会有10万零1和不足10万】,那么为了这样一个临界状态的可靠性,所以添加 setNx 加锁只有成功和失败。
还有一种情况是主从切换的时候,如果主节点的 incr 还没同步到从节点,主节点挂了,丢失了部分未同步的数据,incr 的值从 8 变成 6,如果没有加锁就可能超卖,属于极端情况下的一种兜底策略,有 setNX 锁拦截后,会更加可靠。
4. 那如果考虑集群故障,机器挂掉的情况,setNX 不也会报错吗?
答:setNX 如果失败了,就直接报错返回 "活动库存不足" 即可,也就是可能会导致少卖,但是不会导致超卖。并且 incr 和 setNX 的 key 不同,incr 的 key 和滑块锁的 key 大概率不在同一节点上,从而双重保证,如果 senNx 的 key 和库存的 key 节点都 down 机了,那这里确实有超卖的可能,不过这个概率可以低到忽略不计。
5. decr 和 incr 两种扣减方式有什么不同?
答:二种方式都可以,decr 适合固定库存场景,和 0 对比,incr 适合可以补库存的场景,和库存总量对比。
6. 那为什么要分段,直接对一个 key senNX 不可以吗?
答:分段锁的话,setNX 因为是非独占锁,所以 key 不存在释放。setNX 的 key 的过期时间可以优化为活动的有效期时间为结束。而独占锁,其实你永远也不好把握释放时间,因为秒杀都是瞬态的,释放的晚了活动用户都走了,释放的早了,流程可能还没处理完。
7. incr 扣减模式下,如果同一个用户并发进来,那么缓存中的库存就会+并发数,但实际这个用户只会领取到一条数据,所以就要恢复并发数-1的库存数量。这样种情况并不是 redis 不稳定导致的,而是同一用户并发导致的,应该及时去恢复数据啊,不然的话缓存中的库存直接一下就给一个用户并发干没了,然后再去恢复,效率太低了吧?
针对同一用户并发导致Redis库存被超额扣减的问题,核心在于原子性校验用户资格与库存扣减,而非事后恢复。可通过 Lua脚本封装“检查用户是否已领奖+扣减库存”的原子操作,确保同一用户的并发请求中仅首次扣减生效,其余直接返回失败,避免无效扣减与恢复开销。
答:不需要恢复,还是回到上面,核心是保证不超卖,关于库存恢复,一般这类抽奖都是瞬态的,且 redis 集群非常稳定。所以很少有需要恢复库存,如果需要恢复库存,那么是把失败的秒杀 incr 对应的值的 key,加入到待消费队列中。等整体库存消耗后,开始消耗队列库存,等补偿恢复,活动已经基本过去了。所以超卖,快速结束是最好的。这个一般是基于运营策略配置何种方式恢复库存,可以失败的专门扫描到恢复库存列表用于消耗,也可以不恢复(因为失败概率很低,也允许不超买即可)。
二、路由组件项目问题
0、为什么要自研,市面不是有吗,怎么回答?
- 维护性;市面的路由组件比如 shardingsphere 但过于庞大,还需要随着版本做一些升级。而我们需要更少的维护成本。
- 扩展性;结合自身的业务需求,我们的路由组件可以分库分表、自定义路由协议,扫描指定库表数据等各类方式。研发扩展性好,简单易用。
- 安全性;自研的组件更好的控制了安全问题,不会因为一些额外引入的jar包,造成安全风险。 当然,我们的组件主要是为了更好的适应目前系统的诉求,所以使用自研的方式处理。就像shardingsphere 的市场占有率也不是 100% 那么肯定还有很多公司在自研,甚至各个大厂也都自研一整套分布式服务,来让自己的系统更稳定的运行。分库分表基本是单表200万,才分。
0、那么面试怎么说; 你们为什么分库分表?
- 我们分库分表用的非常熟。但不能为了等到系统到了200万数据,才拆。那么工作量会非常大
- 我们的做法是,因为有成熟方案,所以前期就分库分表了。但,为了解释服务器空间。所以把分库分表的库,用服务器虚拟出来机器安装。这样即不过多的占用服务器资源,也方便后续数据量真的上来了,好拆分。
- 同时,抽奖系统,是瞬时峰值较高的系统,历史数据不一定多。所以我们希望,用户可以快速的检索到个人数据,做最优响应。因为大家都知道,抽奖这东西,push发完,基本就1~3分钟结束,10分钟人都没了。所以我们这也是做了分库分表的理由。
0、流程
基于AOP实现分库分表路由流程:通过拦截数据库操作,对用户ID进行Hash路由计算,将路由结果存储在ThreadLocal实现线程内共享,并在SQL执行前动态切换目标库表,实现了对业务代码零侵入的数据分片机制
该数据流图展示了基于动态数据源与分库分表策略的系统架构设计,核心组件包括:
- 动态数据源管理(DynamicDataSource/DynamicMybatis)通过ThreadLocal保存上下文(如routerKey),结合AOP拦截(@Around/@OBRod)实现请求路由;
- 路由策略层(DBRouterStrategy)基于参数(如hashCode、业务键)或配置规则动态选择目标数据源或分表逻辑,支持事务控制(TransactionTemplate);
- 分表实现通过MyBatis拦截器动态改写SQL,结合配置中心(DBRouterConfiguration)实现灵活扩展;
- 线程安全与上下文传递依赖DBContextHandler维护数据源状态,确保多层级调用的一致性。
该流程图描述了基于动态数据源与分库分表策略的系统架构,核心流程如下:
- 初始化配置:通过
DBRouterConfig加载环境配置(如数据源、分表规则),初始化AbstractRoutingDataSource作为动态数据源基类; - 路由决策:在业务方法执行前,通过
@DBRouterPoint(AOP切面)拦截请求,基于DBRouterStrategy(如哈希、业务键)计算路由键(routerKey),动态选择目标数据源或分表逻辑; - 数据源切换:
DynamicDataSource结合ThreadLocal(DBContextHandler)保存上下文状态,确保线程安全,并通过DynamicMybatisPlugin拦截MyBatis SQL,改写分表语句; - 事务与执行:利用
TransactionTemplate管理事务一致性,最终完成数据库操作并返回结果。
整体通过接口(如IDServer)、策略模式和AOP技术实现模块解耦,支持灵活扩展与动态配置。
0、算法选择与雪崩测试
路由算法 三种散列算法对比:整数取模/乘法/斐波那契 在算法选项过程中我们对比了三种主流散列算法进行了雪崩测试:除法散列满足50%数据变化的表是斐波那契散列的3倍;乘法散列通过乘法和位移代替除法,扩容下数据迁移量不稳定。
雪崩测试:
- 准备10万个单词用作样本数据。
- 对比测试除法散列、乘法散列、斐波那契散列。
- 基于条件1、2,对数据通过不同的散列算法分两次路由到8库32表和16库32表中,验证每个区间内数据的变化数量,是否在50%左右。
- 准备一个 excel 表,来做数据的统计计算。
1、 如何解决路由数据分布不均?
考察:分布式哈希设计能力。
答:采用MurmurHash扰动算法打散键值分布,结合一致性哈希环和虚拟节点机制平衡负载,通过压力测试验证分布均匀性(标准差<5%),热点数据通过本地缓存(Caffeine)预加载降低计算频率。
2、 为何用AOP实现路由而非业务代码侵入?
考察:设计模式与解耦思维。
答:AOP通过动态代理将路由逻辑与业务代码解耦,支持声明式注解(如@DBRoute)控制粒度,避免代码污染;通过切面性能监控(如SkyWalking)验证代理开销可控(<1ms)。
3、 动态数据源切换实现原理?
考察:Spring动态数据源机制。
答:继承AbstractRoutingDataSource重写determineCurrentLookupKey(),结合AOP切面将路由键存入ThreadLocal,通过Nacos监听配置变更动态刷新数据源列表,并验证双写期间路由一致性(如单元测试覆盖@RefreshScope场景)。
4、 什么是 AOP?在 DB-Router 中如何应用 AOP?
答:AOP(面向切面编程)通过动态代理将横切关注点(如日志、事务、路由)与业务逻辑解耦。在DB-Router中,通过自定义注解(如@DBRoute)标记路由方法,利用Spring AOP创建切面,在方法执行前动态注入路由上下文(如路由键),并切换至对应数据源,实现非侵入式路由控制。
5、 AbstractRoutingDataSource 是什么?它的作用是什么?
答:AbstractRoutingDataSource是Spring AOP的核心类,用于动态切换数据源。其作用是通过重写determineCurrentLookupKey()方法获取路由键(如用户ID),结合线程绑定的ThreadLocal路由上下文,决定当前请求应访问哪个数据源,实现读写分离或分库分表功能。
6、 ThreadLocal 是什么?在 DB-Router 中是如何使用的?
答:ThreadLocal是线程局部存储工具,确保数据在单线程内隔离。在DB-Router中,用于存储路由键(如分片标识),避免多线程间污染。通过AOP切面在方法入口设置ThreadLocal值,并在请求结束时(如通过@Transactional回调)清理,防止内存泄漏,结合弱引用(WeakReference)进一步降低风险。
7、 什么是哈希散列?在 DB-Router 中为什么选择了哈希散列算法?
答:哈希散列是将数据映射到固定范围的算法(如MurmurHash、CRC32)。在DB-Router中,选择MurmurHash因其非线性、低碰撞概率和高性能特性,结合一致性哈希环实现均匀分片,避免热点数据集中,通过扰动函数打乱原始键分布,提升负载均衡能力。
8、 SAC 测试是什么?在 DB-Router 中如何应用 SAC 测试?
答:SAC(Service Availability Check)是服务可用性测试,通过模拟高并发、故障场景验证系统稳定性。在DB-Router中,通过JMeter模拟分片节点宕机、网络延迟等异常,测试路由容灾机制(如自动切换备库、熔断降级),并验证全链路监控(如Prometheus指标)是否能及时报警,确保服务可用性。
9、 什么是 MyBatis Plugin?在 DB-Router 中如何应用 MyBatis Plugin 实现动态变更表信息?
答:MyBatis Plugin是基于动态代理的插件机制,可拦截SQL执行流程。在DB-Router中,开发插件拦截Executor的update方法,动态修改SQL中的表名(如user_0→user_1),通过解析路由注解(如@TableShard("user", "user_id"))获取分片信息,并替换为对应分片表名,实现透明化分表操作。
10、 分库分表的散列算法有哪些,各自的优缺点是什么?
答:
• 一致性哈希:优点是增删节点时数据迁移量小,负载均衡好;缺点是复杂度高,需维护虚拟节点。
• 取模运算:优点是简单高效,但节点增减需重新哈希,导致数据大规模迁移。
• 范围分片:优点是支持顺序查询,但热点数据分布不均。
在DB-Router中采用一致性哈希+扰动函数,平衡扩展性与性能。
11、 在 DB-Router 中如何支持个性化的分库分表控制?请结合具体实例说明。
答:通过自定义注解(如@ShardBy(field="user_id", strategy="mod", count=10))允许业务方指定分片字段、算法(如取模、哈希)和分片数。在AOP切面解析注解参数,动态生成路由键(如user_id % 10),并通过配置中心(如Nacos)管理个性化策略,支持不同业务表独立配置分片规则。
12、 在 DB-Router 中如何实现扩展监控、扫描、策略等规则?
答:设计可插拔的规则引擎(如基于Aviator的表达式解析),将监控指标(如QPS、延迟)、扫描任务(如分片健康检查)、策略配置(如路由算法)抽象为统一接口。通过SPI机制动态加载实现类,并集成配置中心实现热更新。例如,新增一个PrometheusMetricsRule插件,自动暴露路由命中率指标至Prometheus。
13、DB-Router执行流程总结,以及与MyBatis对比
1、MyBatis执行流程
2、应用运用DB-Router执行流程
3、DB-Router类图
14、其他问题
1)在 DB-Router 中如何支持个性化的分库分表控制?请结合具体实例说明。
DB-Router 用户可以自己实现散列算法,比如使用一个字段分库、一个字段分表。实现后替换现有散列算法配置进去就可以。
2)DB-Router 中如何实现扩展监控、扫描、策略等规则?
DB-Router 可以与监控服务配合,在路由执行时,在方法上添加监控配置信息。一般是一个key。这里通过key与对应的方法名称组合出唯一值,来监控路由方法的执行。包括耗时、异常等信息。
3)在 DB-Router 的架构模型中,如何实现扩展性和灵活性的平衡?
DB-Router 可以支持数据源的设计、多种散列算法的自定义实现。
4)在 DB-Router 中如何保证数据路由的高效性和准确性?
DB-Router 这个内容会涉及到路由字段的设计。可以基于场景回答。如;早期对用户的id设计长短、字符类型、特殊字符、纯数字等,都无限制,因此路由的时可能会有偏差。为此通过将路由字段统一截取到固定长度,在进行路由会更加准确。同时也保证计算的高效性。
5)在 DB-Router 中,如何避免分库分表后产生的性能问题?
DB-Router 的性能主要体现在路由计算、切换数据源、更改SQL,此逻辑操作在进行压测下,平均只需要2~4毫秒左右,因此不会对性能造成影响。
6)在 DB-Router 中如何应对高并发的场景?请结合具体实例说明。
DB-Router 高并发场景需要注意在应用分布式部署后,数据库的连接数也应随之配置,同时注意配置路由的连接池需要根据业务场景进行验证压测。另外尽量必要做过大的事务,这样会长时间占用数据库资源。
7)在 DB-Router 中如何应用MyBatis Plugin 实现动态变更表信息?
DB-Router 基于 MyBatis 插件拦截机制,获取 SQL 信息,并进行修改操作。主要涉及MyBatis源码类;StatementHandler - 语句处理器、MetaObject - 元对象、MappedStatement 映射语句对象。
MyBatis Plugin 是 MyBatis 框架提供的拦截器机制,允许开发者通过插件的方式拦截 SQL 执行过程中的关键节点(如执行前、结果返回前),从而动态修改 SQL 内容或添加额外逻辑。在 DB-Router 中,MyBatis Plugin 被用于实现动态表信息变更,例如分库分表场景下的表名路由。具体来说,插件会拦截业务 SQL,解析其中的分片键(如用户ID),结合分片规则计算目标分片位置,然后动态替换原始 SQL 中的表名为实际对应的子表名(如 order_01)。这一过程对业务代码完全透明,开发者无需手动拼接表名,DB-Router 通过插件机制自动完成 SQL 改写、路由选择及多数据源切换,实现动态表信息的无缝处理。
8)什么是哈希散列?在 DB-Router 中为什么选择了哈希散列算法?
哈希散列是一种通过哈希函数将任意长度的数据映射为固定长度唯一值(哈希值)的技术,其核心在于快速定位数据位置并实现均匀分布。在数据库领域,哈希散列常用于分库分表场景,通过将数据键(如用户ID)计算哈希值后分配到特定节点,确保数据分散存储以提高并发性能和存储效率。DB-Router 选择哈希散列算法主要因其高效性和数据分布的均匀性:哈希计算速度快,能快速路由到目标分片;同时,均匀的分布特性可避免数据倾斜,减少热点问题;此外,结合一致性哈希等优化策略,哈希散列还能在节点动态扩缩容时最小化数据迁移量,提升系统的扩展性和容错能力。
9)分库分表的散列算法有哪些,各自的优缺点是什么?
常见的分库分表散列算法包括:
• 范围分片:按数据键的范围划分分片(如按时间或ID区间),优点是查询简单且范围查询高效,缺点是数据分布易不均匀,可能产生热点,扩容时需重新分配大量数据。
• 哈希分片:通过哈希函数(如MD5、CRC32)将键映射到固定分片,优点是数据分布均匀、无热点,缺点是扩容时需重新哈希导致数据迁移量大,且非范围查询效率低。
• 一致性哈希:将分片和数据映射到一个环形哈希空间,数据顺时针匹配最近节点,优点是节点增减时仅少量数据迁移,适合动态扩缩容,缺点是实现复杂,且节点过少时负载可能不均。
• 取模分片:对数据键取模决定分片(如 id % N),优点是简单易实现,缺点是分片数固定,扩容需重新设计模数,且数据分布受模数影响可能不均。
各算法需根据业务场景权衡:范围分片适合时间序列查询,但需解决热点问题;哈希分片适合写密集型场景,但扩容成本高;一致性哈希适合动态扩展,但实现复杂度高;取模分片简单却牺牲了灵活性。