1. 深入聊聊你的项目,从架构设计到技术选型的考虑 (30min)
项目背景和规模:
我负责的抽奖系统是为电商平台设计的营销工具,主要用于节日促销、新品推广等场景。系统上线后日均抽奖10万次,高峰期每秒1500次请求。注册用户50万,活跃用户5万左右。
架构演进过程:
最初是单体架构,所有功能在一个项目中。随着业务增长,出现了性能瓶颈和维护困难。我们进行了微服务改造,拆分为用户服务、活动服务、抽奖服务、奖品服务四个核心服务。
用户服务负责用户认证、权限管理、用户信息维护。活动服务负责活动的创建、配置、状态管理。抽奖服务是核心,负责抽奖逻辑、概率计算、库存扣减。奖品服务负责奖品管理、发放、物流对接。
技术选型考虑:
选择Spring Cloud作为微服务框架,因为它生态完善,社区活跃,团队熟悉。使用Nacos做注册中心和配置中心,相比Eureka更轻量,支持配置动态刷新。
数据库选择MySQL,因为抽奖数据需要强一致性,而且团队对MySQL运维经验丰富。使用主从复制实现读写分离,提高查询性能。
缓存选择Redis,用于存储活动配置、奖品库存、用户抽奖次数。Redis的高性能和丰富的数据结构非常适合这个场景。
消息队列选择RabbitMQ,用于异步处理中奖通知、奖品发放。选择RabbitMQ而不是Kafka,是因为我们的消息量不是特别大,更看重消息的可靠性和灵活的路由规则。
核心技术难点:
第一个难点是高并发下的库存扣减。我们使用Redis的Lua脚本保证原子性,避免超卖。使用分布式锁防止重复抽奖。使用限流算法控制请求速率,保护系统。
第二个难点是概率算法的设计。要支持灵活的概率配置,保证概率的准确性,还要考虑性能。我们使用区间映射法,预先计算好概率区间,抽奖时只需要生成随机数判断落在哪个区间。
第三个难点是数据一致性。Redis和数据库的数据要保持一致,异步任务失败要有补偿机制。我们使用最终一致性方案,配合定时对账和人工介入。
性能优化措施:
使用多级缓存,本地缓存加Redis缓存,减少网络IO。活动配置使用本地缓存,设置1分钟过期,大部分请求不需要访问Redis。
使用异步处理,抽奖成功后立即返回,中奖记录异步保存。使用消息队列削峰填谷,平滑处理高并发请求。
数据库优化,使用索引加速查询,使用分表减少单表数据量。中奖记录按月分表,历史数据归档到冷存储。
使用连接池复用连接,减少连接建立开销。使用线程池处理任务,避免频繁创建销毁线程。
监控和运维:
使用Prometheus采集指标,Grafana展示监控面板。监控QPS、响应时间、错误率、库存数量等关键指标。
使用Skywalking做链路追踪,快速定位性能瓶颈。使用ELK收集日志,分析用户行为和系统问题。
设置告警规则,异常时通过钉钉、短信通知。制定应急预案,系统故障时快速响应。
项目成果:
优化后系统可以稳定支持每秒1500次抽奖请求,响应时间在100毫秒以内。系统可用性达到99.9%以上,没有出现过超卖问题。用户反馈体验流畅,活动转化率提升了20%。
2. Redis的数据结构底层实现,跳表的原理
Redis数据结构:
Redis有五种基本数据结构,每种都有不同的底层实现。String底层是SDS简单动态字符串。List底层是quicklist,结合了ziplist和linkedlist。Hash底层是ziplist或hashtable。Set底层是intset或hashtable。Sorted Set底层是ziplist或skiplist加hashtable。
SDS简单动态字符串:
SDS不是C语言的字符串,而是自己实现的数据结构。它包含长度、剩余空间、字符数组三个字段。
SDS的优点是获取长度是O(1),不需要遍历。预分配空间,减少内存重分配次数。二进制安全,可以存储任意数据。
跳表原理:
跳表是Sorted Set的底层实现之一,用于有序集合。它是一种多层链表结构,通过索引层加速查找。
最底层是完整的有序链表,包含所有元素。上层是索引层,每层的元素数量是下层的一半左右。查找时从最高层开始,找到区间后下降到下一层,直到找到目标元素。
跳表的查找、插入、删除时间复杂度都是O(log n),和平衡树相当。但跳表实现更简单,而且支持范围查询。
跳表的插入:
插入元素时,先在最底层找到插入位置,插入元素。然后随机决定是否在上层也插入索引,概率是50%。这样保证了跳表的平衡性。
随机层数的期望是log n,所以跳表的高度是log n。空间复杂度是O(n),因为索引层的元素总数是n + n/2 + n/4 + ... ≈ 2n。
为什么用跳表而不是红黑树:
第一是实现简单,跳表的代码比红黑树简单很多,容易理解和维护。
第二是支持范围查询,跳表可以方便地遍历一个范围内的元素。红黑树需要中序遍历,相对复杂。
第三是内存局部性好,跳表的节点在内存中相对连续,缓存友好。红黑树的节点分散,缓存命中率低。
第四是并发性能好,跳表可以用CAS实现无锁并发,红黑树需要复杂的锁机制。
ziplist压缩列表:
ziplist是一种紧凑的数据结构,用于节省内存。它是一段连续的内存,每个元素包含前一个元素的长度、编码类型、数据。
ziplist适合元素数量少、元素大小小的场景。当元素数量或大小超过阈值,会转换为其他数据结构。
ziplist的缺点是插入删除需要移动元素,时间复杂度O(n)。而且可能触发连锁更新,影响性能。
3. JVM的垃圾回收器,G1和ZGC的区别
垃圾回收器演进:
Java的垃圾回收器经历了多代演进。Serial是单线程收集器,适合单核CPU。Parallel是多线程收集器,注重吞吐量。CMS是并发收集器,追求低停顿。G1是分区收集器,可预测停顿。ZGC是低延迟收集器,停顿时间在10毫秒以内。
G1收集器:
G1把堆划分为多个大小相等的Region,每个Region可以是Eden、Survivor、Old、Humongous区。大对象直接分配到Humongous区。
G1的回收过程分为四个阶段。初始标记标记GC Roots直接关联的对象,需要短暂停顿。并发标记从GC Roots遍历对象图,和应用程序并发执行。最终标记处理并发标记期间的变化,需要短暂停顿。筛选回收根据停顿时间目标,选择回收价值最大的Region进行回收。
G1的优点是可以设置停顿时间目标,比如200毫秒。G1会尽量在这个时间内完成回收。而且没有内存碎片问题,使用复制算法整理内存。
G1适合大堆内存场景,比如几十GB的堆。对于小堆内存,G1的开销反而更大。
ZGC收集器:
ZGC是JDK 11引入的低延迟收集器,目标是停顿时间不超过10毫秒,支持TB级别的堆。
ZGC使用染色指针技术,在指针中存储对象的状态信息。通过读屏障,在访问对象时检查状态,必要时进行处理。
ZGC的回收过程几乎全部并发执行,只有初始标记和最终标记需要短暂停顿。而且停顿时间不随堆大小增加,无论堆多大,停顿时间都在10毫秒以内。
ZGC使用内存多重映射技术,同一块物理内存映射到多个虚拟地址。通过切换映射,实现并发的内存整理。
G1 vs ZGC:
停顿时间方面,G1的停顿时间在几十到几百毫秒,ZGC在10毫秒以内。ZGC的停顿时间更短,更稳定。
吞吐量方面,G1的吞吐量更高,ZGC为了低延迟牺牲了一些吞吐量。
内存开销方面,ZGC的内存开销更大,需要额外的内存存储元数据。
适用场景方面,G1适合对停顿时间要求不是特别高的场景。ZGC适合对延迟敏感的场景,比如交易系统、实时系统。
选择建议:
JDK 8推荐使用G1,设置合理的停顿时间目标。JDK 11及以上,如果对延迟要求高,可以使用ZGC。
要根据实际情况测试,不同的应用特点不同,适合的收集器也不同。要监控GC日志,分析GC频率和停顿时间,调整参数。
4. 分布式事务的实现方案,Seata的AT模式原理
分布式事务问题:
在微服务架构中,一个业务操作可能涉及多个服务,每个服务有自己的数据库。如何保证这些操作要么全部成功,要么全部失败,这就是分布式事务问题。
常见解决方案:
两阶段提交2PC是传统方案,但性能差,而且存在单点故障。TCC方案性能好,但开发成本高,需要实现Try、Confirm、Cancel三个方
全部评论
(0) 回帖