为了避免丢消息问题需要落表,如何设计这张消息表?

典型回答 这个其实是典型的本地消息表的方案: ✅如何基于本地消息表实现分布式事务? 那么,我们就需要自己定义一张表,用来存储本地消息,这张表不仅用于存储,还会用来做扫表重试,那么这张表该如何设计呢? 以下是这张表需要包含的一些字段 字段名 类型 描述 id BIGINT (PK) 主键 gmt_create DATETIME 创建时间 gmt_modified DATETIME 更新时间 message_key VARCHAR 消息的业务唯一标识(用于幂等处理) message_id VARCHAR 消息的唯一ID,发送成功后才有,如果没成功,则该字段为null message_type VARCHAR 消息的类型,根据这个消息类型执行不同的消息处理逻辑 topic VARCHAR 消息所属主题或分类 message_body TEXT / JSON 消息体,内容序列化存储 state VARCHAR 消息状态(如:待发送,已发送,失败,已消费等) retry_count INT 重试次数 next_retry_at DATETIME 下一次重试时间(用于任务调度) last_retry_time DATETIME 上一次的重试时间(用于任务调度) fail_reason TEXT 失败原因,可以记录具体的错误码,或者异常的堆栈信息 lock_version BIGINT 乐观锁版本号 索引情况: state 作为普通索引,用于高效扫表。这个也可以和retry_count、next_retry_at等字段一起建一个联合索引。 message_key +message_type 作为唯一索引,防止重复插入 关于state设置索引是否有用,可以看下面这篇: ✅区分度不高的字段建索引一定没用吗? 基本信息 id BIGINT (PK) 主键 gmt_create DATETIME 创建时间 gmt_modified DATETIME 更新时间 lock_version BIGINT 乐观锁版本号 这些都是一些基本字段了, ...

March 22, 2026 · 1 min · santu

有一个银行系统,对实时性要求比较高,你会怎么选择垃圾回收器?

典型回答 这个题目中比价关键的一个词是“实时性要求比较高”,**实时性意味着延迟低,延迟低意味着停顿时间短。意味着STW的时间短。**那么就需要选择STW耗时短的垃圾回收器。 ✅什么是STW?有什么影响? 但是,这个问题很难回答,因为最主要的就是根据不同的JDK版本,可能选择的结果是不一样的。 我们从高往低了开始说。 JDK版本 垃圾收集器选择 >= JDK 12 Shenandoah GC 或者 ZGC >= JDK 11 & < JDK 12 ZGC >=JDK 7 G1 < JDK 7 Parallel Scavenge + CMS 垃圾收集器有几种,首先我们肯定要排除Serial GC和Serial Old这两个串行的垃圾回收器了。然后就在区分为并发垃圾回收器和并行垃圾回收器。 ✅说一说JVM的并发回收和并行回收 并行回收其实就是Parallel Scavenge,Parallel Old,ParNew等收集器。这种垃圾回收器更加关注的是吞吐量(吞吐量=代码运行时间/(代码运行时间+垃圾收集时间),在这个公式里,垃圾收集时间是分母的一部分,但是他并不是全部,他只能一定程度上影响吞吐量,但是这几种垃圾回收器,会综合看吞吐量的指标,并不会极端的优化垃圾收集时间。 而我们说的并发回收比较典型的就是 **CMS、G1等,他们更加关注的垃圾回收的停顿时间。其实主要就是引入了三色标记法。**引入三色标记法之后,就是把STW的时间缩短了,具体原因参考: ✅介绍下CMS的垃圾回收过程 所以,如果能选择CMS和G1肯定要比Parallel Scavenge,Parallel Old,ParNew这几个更适合我们的这个场景。而且G1是整堆回收器,而CMS是老年代回收器,所以如果用G1的话,一个就够了,如果用CMS的话,还需要搭配一个年轻代的回收器,比如Parallel Scavenge。 所以,如果是JDK 1.7之后的版本的话(小于JDK 11),建议直接上G1了,当然,G1堆内存大小有一定要求,一般要求4G起步,8G以上最好。G1可以把STW时长控制在10ms-100ms左右。 如果是JDK版本更新的话,比如JDK 11以后,就可以考虑Shenandoah GC 和 ZGC这两个超低延迟垃圾收集器了,他们适用于超大堆内存。暂停时间通常在10ms以下,适合对响应时间有极高要求的应用。

March 22, 2026 · 1 min · santu

百万级会员的用户平台,如何实现快到期的会员的消息提醒?

典型回答 这是一个典型的方案设计类的题目,题目的难点和隐藏的考点有这么几个: 1、百万级数据量 如何快速查询出要到期的数据 如何高效的针对大批量用户做推送 2、快到期的会员 如何知道哪些用户快到期了 3、消息提醒 用具体什么样的提醒方式 如何避免一个用户被频繁提醒 逐一拆解一下这几个问题吧。 首先可以明确的是,**百万级会员,这个量级不算大!!!**才百万级,根本不需要上分库分表、读写分离,直接单表就能抗,稍微有点索引就能扛得住了。 如果你查GPT或者DeepSeek,他会告诉你百万级太大了,需要做分库分表了,甚至有的还告诉你用到期时间做个分区。。。这根本就不靠谱,这个数据量根本不需要,而且到期时间是可能会变的,用一个可变字段做分区,这不是坑人么。 还有的是说针对这些数据做冷热分离,将历史过期会员归档到独立表中,减少主表数据量。。。这也完全是过度设计,百万级的数据量,根本不需要做归档。 需不需要一张消息推送表 针对消息推送的这个场景,我们其实是需要一张表记录下所有的推送的(有的时候可以不建表,可以通过固定的格式打印日志,然后拉取日志之后解析日志。)。 有这样表的好处是可以知道具体的推送的情况,什么时间、用什么渠道、给谁做了推送,推送了什么内容,结果是什么,都比较清晰。 有这样表之后,还可以有一个好处就是可以做幂等控制、疲劳度控制、以及失败的重试、还有数据分析。可以参考以下设计: 字段名 类型 必填 默认值 说明 id BIGINT UNSIGNED 是 自增 主键,唯一标识 user_id BIGINT UNSIGNED 是 - 接收用户ID(与用户表关联) message_type VARCHAR(20) 是 - 消息类型(如:**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">到期提醒</font>**) channel VARCHAR(20) 是 - 推送渠道(如:**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">sms</font>**/**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">email</font>**/**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">app_push</font>**/**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">站内信</font>**) title VARCHAR(200) 否 NULL 消息标题(邮件主题、推送标题) content TEXT 是 - 消息内容(支持模板变量,如**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">{username}</font>**) status TINYINT 是 0 状态(**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">0=待发送</font>**/**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">1=已发送</font>**/**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">2=发送失败</font>**/**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">3=已重试</font>**) send_time DATETIME 否 NULL 实际发送时间 create_time DATETIME 是 NOW() 创建时间 update_time DATETIME 是 NOW() 更新时间 retry_count TINYINT UNSIGNED 是 0 重试次数(超过阈值后标记为失败) third_msg_id VARCHAR(100) 否 NULL 第三方平台消息ID(如短信服务商返回的ID,用于对账) error_info TEXT 否 NULL 错误详情 is_read TINYINT(1) 是 0 是否已读(仅对站内信有效,**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">0=未读</font>**/**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">1=已读</font>**) click_time DATETIME 否 NULL 用户点击时间(用于统计转化率) extra_info JSON 否 NULL 扩展字段(如:模板参数、业务上 如何识别即将到期的用户 首先,我们可以在会员表中增加一个到期时间的字段,如expire_time,然后针对这个字段建立一个索引。这样我们在查询的时候,根据 expire_time < now() 就能一次性的捞出所有已经过期的用户了,如果想要查询还有三天过期的用户,那么可以用以下SQL: ...

March 22, 2026 · 2 min · santu

如果设计一个缓存,需要考虑哪些方面?

典型回答 缓存分为本地缓存和分布式缓存,如果是本地缓存,需要考虑的问题有: 数据结构、线程安全、对象上限、清除策略、过期时间等。 数据结构 一般来讲,为了提升缓存的效率,通常采用Key-Value结构进行数据存储,也就是说,缓存中的数据保存和读取都需要有一个Key,通过Key来读取固定的缓存的Value。 线程安全 本地缓存一定要考虑线程安全的问题,因为大多数情况下本地缓存都是一个全局可访问的变量,那么就会有多个线程同时访问,所以线程安全问题不容忽视。 对象上限 因为是本地缓存,而本地内存中的数据是要占用JVM的堆内存的,所以内存是有上限要求的,如果无限存储,最终一定会导致OOM的问题。 清除策略 为了避免OOM的问题,一般会考虑在缓存中增加清除策略,通过一定的手段定期的清理掉一些数据,来保证内存占用不会过大,常见清除策略主要有有LRU(最近最少使用)、FIFO(先进先出)、LFU(最近最不常用)、SOFT(软引用)、WEAK(弱引用)等; 过期时间 有了清除策略并不能保证百分百的可以删除数据,极端情况会会使得某些数据一直无法删除。这时候就需要有一种机制,能够保证某些K-V一定可以删除。通常采用的方案是给每一个缓存的key设置过期时间,当达到过期时间之后直接删除,采用清除策略+过期时间双重保证; 以下是Caffeine这个优秀的缓存框架在这几个方面的做法: 数据结构:Caffeine 的核心数据结构基于 **<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">ConcurrentHashMap</font>** 和 时间轮(Timer Wheel) 实现高效缓存管理。通过锁分段技术减少锁竞争,每个 Segment 独立管理一部分哈希桶,提升并发读写性能。 线程安全:Caffeine在关键路径(如缓存命中计数、频率统计)中使用 CAS(Compare-and-Swap)操作,减少锁开销。并且实用**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">ConcurrentHashMap</font>** 这种线程安全的数据结构。 对象上限:通过 maximumSize(long size) 设置缓存最大条目数。通过 weigher((k, v) -> weight) 自定义权重计算,配合 maximumWeight(long weight) 限制总权重。 清除策略:Caffeine 的淘汰策略是其性能优势的核心,采用 Window TinyLFU 算法,结合 LRU 和 LFU 的优点。 过期时间:Caffeine 支持多种过期策略,并通过时间轮高效管理。 如果是分布式缓存,在本地缓存的基础上,还需要考虑持久化机制、集群模式等。以下是Redis这个优秀的缓存框架在这几个方面的做法: 数据结构:https://www.yuque.com/hollis666/ec96i7/hlg4u2 线程安全:https://www.yuque.com/hollis666/ec96i7/og6nf4 ...

March 22, 2026 · 1 min · santu

扫表任务,如何写SQL可以避免出现跳页的情况?

典型回答 这其实是一个非常常见的问题,只不过面试中直接这么问他的不多,但是工作中遇到的很多,有的面试官也会问一个扫表任务具体如何写SQL等问题。 一般来说,我们的扫表都是定时任务执行的,比如说我们要扫描一张表中所有INIT状态的数据,因为内存有限,所以不太可能一次性都扫描出来,只能分页,所以,最简单的方式就是: select from table where … state ='init' order by limit 0,100 通过以上SQL实现分页查询,然后上面是查询第一页,如果接着要查第二页,则用以下SQL: select from table where … state ='init' order by limit 100,200 看上去好像没啥问题是吧?但是实际上这个查询,会导致跳页的问题。 因为以上SQL的页面的跳转,有一个前提,那就是数据是不变的,即状态是不会发生改变的,可是,如果在查询完第一个之后,我们是需要处理这些数据的,处理完之后,如果成功了,是要把状态从INIT改成其他状态比如SUCCESS的,这时候,第一页(0,100)的数据已经变成SUCCESS,那么你再根据INIT查询(100,200)的数据的时候,虽然查询的是INIT状态的第二页,但是实际上查询的已经是第三页了,因为第一页被你改成SUCCESS了。 那么也就意味着,第二个的数据被你跳过了。并没有实际执行。如果想要让这个查询生效,那么应该第二次查询的时候也查第一页,即还是select from table where … state ='init' order by limit 0,100,这样只要能保证前面100个数据状态变成SUCCESS之后,我永远查询第一个的INIT就行了。 但是这个假设太理想了,实际执行的时候,有些任务可能就是无法执行的,那么就会导致第一页的无法成功的数据不断堆积,堆积的数据不断被执行,但是他会一直占用这个分页的窗口,导致后面的数据无法被执行。甚至严重的会导致任务陷入**死循环,**即第一个数据永远无法成功,但是又无限的循环执行。 那么到底如何解决呢?有一个好的方案,那就是选择一个游标,也就是我们标记上第一个处理过的最大的id,下一页查询的时候,用select from table where … state ='init' and id> ${last_max_id} order by id limit 0,100 这样的查询。 只要每次我们都把上一页的最大id获取到,就能保证下一次的查询数据一定是不重复且不丢的。

March 22, 2026 · 1 min · santu

购物车中如何解决重复下单的问题?

典型回答 ✅如何解决消息重复消费、重复下单等问题? 我们前面的题目中介绍过基于token机制来解决重复下单问题的方案。但是这个方案在商品详情页下单是可以用的。,但是如果是购物车下单,就不ok了。 因为前面的方案我们说为了解决用户提前刷一批token的情况,基于"品+用户"的维度生成token,但是在购物车中是有很多品的,用户刚进购物车的时候,你也不知道用户这次要购买的是哪个品。 那怎么办呢?可以参考淘x的方案。那就是每一个商品在加入购物车的时候,都生成一个cart_item_id。 这个cart_item_id的特点是: 1、全局唯一,所有用户的cart_item_id都不一样 2、只有这个sku在首次进入购物车时才生成,如果是已有sku的加购,无需生成,以为这种情况是直接修改数量。 3、如果一个商品在购物车中被购买了之后,再次被加入购物车,则生成新的cart_item_id 整个cart_item_id的生成和校验逻辑如下: 通过setnx,借助redis来保证只有一个线程可以成功,因为cart_item_id是全局唯一的,所以能保证在某个购物车的某个商品,只能被下单成功一次。 后续这个cart_item_id可以做个定期清理,或者设置一个失效时间也行(如:SET cart_item_id cart_item_id NX EX 300 ),这个失效时间其实不用太长,3-5分钟完全够了。 扩展知识 恶意攻击 上面的方案,能够防止用户的误操作,或者是秒杀时候的重复点击下单按钮的这种重复下单。但是如果是恶意攻击怎么办?会不会有人伪造请求,直接向后端发送下单请求,然后自己伪造一些cart_item_id呢,每次都传不一样的,不就绕开了setnx了么 并不会,因为cart_item_id是我们生成的,购物车在加购的时候是有后端交互的,这时候是后端生成的这个cart_item_id。 这时候下单带过来cart_item_id的时候,我们可以先做个校验,确保这个cart_item_id是有的,并且是在这个用户的购物车中有的才让他下单就行了。

March 22, 2026 · 1 min · santu

阿里出的Java开发手册看过吗,对哪条规约印象深刻?

典型回答 这个问题一般在阿里系的面试中出现的比较多,其他公司可能会这么问:你公司内部有哪些开发规约吗?你自己平时写代码有哪些好的开发习惯吗?你有看过一些开发规约吗,哪条你觉得挺好的? 这个问题如果看过相关的规约,其实挺好回答的,如果没看过,就不太好回答。给大家列几个规约供参考: ✅SimpleDateFormat是线程安全的吗?使用时应该注意什么? ✅RPC接口返回中,使用基本类型还是包装类? ✅HashMap的容量设置多少合适? ✅Spring中如何开启事务? ✅为什么不建议通过Executors构建线程池 ✅为什么不推荐使用外键? ✅String、StringBuilder和StringBuffer的区别? ✅为什么不能直接使用Log4j、Logback中的 API? ✅BigDecimal(double)和BigDecimal(String)有什么区别? ✅为什么不能用BigDecimal的equals方法做等值比较? 为啥我不建议使用@Transactional事务

March 22, 2026 · 1 min · santu

MySQL千万级大表中如何增加字段?

典型回答 这个问题其实考察的是数据库在增加字段过程中可能会存在的锁表导致影响到正常的业务的增删改查操作如何解决,因为千万级大表的话,增加字段的过程会耗时比较长,所以如果处理不好,影响可能就格外的大。 OnlineDDL 首先,如果你用的是MySQL,并且版本是大于等于5.6的,并且你用的是InnoDB,那么这个事儿就很简单到,简单到你直接找一个非业务高峰期执行ALTER TABLE就行了。因为这个版本开始有了一个新特性,叫做**OnlineDDL** ✅什么是OnlineDDL Online DDL(在线DDL)简单点说就是允许在表上执行DDL的操作的同时不阻塞并发的DML操作和查询操作。 有了OnlineDDL之后,在表上增加列就可以用到其中的inplace的方式(原理上文有介绍),这个过程中是允许并发的增删改查的。 使用OnlineDDL进行增加字段的SQL如下: 1 2 3 ALTER TABLE order_table ADD COLUMN hollis_test INT DEFAULT 0, ALGORITHM=INPLACE, LOCK=NONE; 但是也需要注意,即使有了OnlineDDL,也需要注意不要在业务高峰期执行增加字段的操作。因为Online DDL 在执行过程中还是会占用系统资源,如 CPU、内存和 I/O的,尤其是在某些复杂操作(如涉及索引重建、大量数据的表结构更改)中,可能仍会有短暂的锁表情况。 ✅MySQL做索引更新的时候,会锁表吗? pt-online-schema-change 如果不能用OnlineDDL,比如不是MySQL,或者不是InnoDB,或者是版本低于5.6那么,可以使用Percona的 **<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">pt-online-schema-change</font>****工具来操作。**这里不展开讲,只说下简单原理和用法,详见:https://docs.percona.com/percona-toolkit/pt-online-schema-change.html 这是 Percona 提供的工具,用于 在线无锁修改大表结构。它的原理是: 创建一个带新字段的临时表。 增量复制原表数据到新表(通过触发器)。 切换表名。 1 2 3 4 5 6 pt-online-schema-change \ --alter="ADD COLUMN new_column INT DEFAULT 0 COMMENT '新字段'" \ D=database_name,t=order_table \ --execute \ --no-drop-old-table \ --max-load="Threads_running=50" **<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">--alter</font>**:新增字段的SQL语句。 **<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">--no-drop-old-table</font>**:保留旧表(便于回滚)。 **<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">--max-load</font>**:当负载超过阈值时暂停操作。 ...

March 22, 2026 · 1 min · santu

如果一个接口响应时间不符合预期,怎么排查跟解决?

典型回答 如果一个接口的接口响应时间(RT)不符合预期,就是慢了呗,那想要解决这个问题,首先应该知道哪里慢了。 性能优化,就是哪里亮了点哪里,首先需要知道哪里亮了。 如果是前端耗时长,那么可以考虑是网络请求发出到返回的耗时,可能包含 DNS、TCP 建连、TLS 握手、数据传输等。 如果是后端耗时长,那么可能是后端应用处理时间,比如业务逻辑、数据库、外部接口调用等。 如果A调用B,B的耗时并不长,但是A却提示调用B超时,可能考虑的网络耗时,比如TCP重传的情况。 ✅一次RPC请求,客户端显示超时,但是服务端不超时,可能是什么原因? 所以,我们需要有一套行之有效的做线上接口慢的定位的方案。 问题定位 首先,最简单的方案就是,如果你是分布式系统,接入了分布式链路追踪的工具,比如skywalking,可以通过他的trace来查看具体哪里慢了,是自己慢了,数据库慢了,Redis慢了,还是下游接口满了。 ✅如何实现应用中的链路追踪? 如果你不知道啥叫分布式链路追踪,或者没用过Prometheus,那么就可以用另外一个方案,那就是Arthas来查看trace的情况。 PS:很多人说线上不能使用arthas,我只能说,这玩意阿里开源的,我们线上定位慢接口都也在用,所以没啥不能用的。当然,做一些特殊操作的时候要注意,可能会导致一些STW,比如拉堆dump,线程dump等,但是这个和arthas没关系,是这些操作本身就这样的,你自己用其他命令执行也一样。 下面这个文档中,介绍了如何通过arthas查看接口耗时。 ✅RT飙高问题排查过程 下载arthas后,运行: 1 sh as.sh 使用 trace 命令查看接口耗时: 1 2 3 4 5 6 7 8 9 10 11 12 13 [arthas@1658]$ trace com.alibaba.fin.pricing.**.PriceCalculateService trial '#cost > 50' -n 3 Press Q or Ctrl+C to abort. Affect(class count: 1 , method count: 1) cost in 427 ms, listenerId: 6 `---ts=2021-11-08 15:10:24;thread_name=HSFBizProcessor-DEFAULT-8-thread-224;id=2d7c1;is_daemon=true;priority=10;TCCL=com.taobao.pandora.boot.loader.LaunchedURLClassLoader@783e6358;trace_id=2132e43116363554229592404e58b8;rpc_id=9.40.6 `---[264.85838ms] com.alibaba.fin.pricing.**.service.PriceCalculateService:trial() +---[0.012009ms] com.alibaba.fin.pricing.**.request.PriceCalculateRequest:getTenant() #95 +---[0.001564ms] com.alibaba.fin.pricing.**.request.PriceCalculateRequest:getProduct() #96 ... ... ... +---[221.884809ms] com.alibaba.fin.pricing.*.ExercisePriceDomainService:queryMatchedEffectiveExercisePrice() #167 +---[0.002242ms] com.alibaba.fin.pricing.**.service.PriceQueryRequest:<init>() #170 `---[0.012586ms] com.alibaba.fin.pricing.**.service.PriceCalculateService:getTieredPrice() #170 ...

March 22, 2026 · 1 min · santu

5000w数据查询电话号码后4位,如何优化?

典型回答 这个问题,其实是一个典型的模糊查询的优化问题。首先看难点在哪? 5000万数据,纯全表扫,不用索引那肯定不现实,查询起来怎么也得10来秒了,太慢了,所以就一定要通过索引的方式来查询。 但是,要查询电话号码的后四位,SQL大概就是phone like “%1234”,那因为这个查询是没有遵循最左前缀匹配的,所以就算phone字段有索引,也是无法命中索引的。 那么如果还要优化的话,有一个比较常见的方案,也是很多大厂都在用的方案,那就是把手机号分段存储。比如原来表中有一个phone字段,保存了完整的手机号,那么可以再多冗余3个字段,分别是phone_part1、phone_part2、phone_part3,分别存储手机号的前3位,中间4位和最后4位。然后再给这几个字段加一下索引。 之所以这么分,是因为大多数的手机号的查询都是通过前三位+后四位,或者直接通过后四位来做查询的。 有了这几个手机号的分段存储和索引之后,就可以完全按照phone_part3 = “1234"这样的方式去做查询了,这就可以完全命中索引了。 这是一种非常典型的用空间换时间的方法,这个方法也不是完美的(不是银弹),也有缺点,但是我觉得都不算啥大问题,比如: 1、浪费存储 2、手机号修改的话,需要一起改。 扩展知识 模糊查询优化方案 like还有其他的优化,比如字段逆序,但是不适合我们的场景,不过也可以结合着看。 ✅MySQL中like的模糊查询如何优化 加密后如何做模糊查询 本文的方案和这个方案比较像,可以结合着看。 ✅数据库加密后怎么做模糊查询?

March 22, 2026 · 1 min · santu

留言给博主