外卖系统,一天一千万条数据,用户需要查到近30天的数据,商家也要查询到30天的数据,怎么设计表?

典型回答 分析一下题干的几个关键信息: 1、一天一千万数据。 2、只查最近30天的数据 3、买家和商家都需要查询 通过以上三个信息,我们可以得出以下结论: 1、单表扛不住: 1天就1000万数据,一年就30多亿数据,那么这个量如果用一张单表存放的话,数据库肯定扛不住。 2、可以做数据归档 因为用户只查询最近30天的数据,所以,可以简单的按照时间来划分冷热数据,30天内的数据为热数据,30天以外的数据为冷数据。冷数据和热数据可以做分离,即针对30天以上的数据做归档。 3、归档后单表还是扛不住 但是,归档之后,热数据的表还要保留30的数据,也就是3个亿,那还是很多的,单靠数据库也扛不住。需要考虑做分库分表 4、分表键不能直接选择买家或者卖家 因为要同时满足买家和卖家的查询,不可能只基于买家或者卖家去做分表。如果按照卖家 id 分表,那么买家的查询就会跨表,查询很慢。按照买家 id 分表也是同理。 5、基本不太可能使用缓存提效 这个很好理解,一方面是数据量太大了,另外一方面订单信息可能会频繁修改,数据一致性不太好保障,这里用缓存不合适。 那么,这个问题最终就变成了一个,如何基于海量数据(3亿)数据走高效查询,并且要支持多个分表键。 分布式数据库 首先,我们可以用支持海量数据存储和查询的各种分布式数据库,如TiDB(pingcap)、OceanBase(蚂蚁)、Spanner(google)等。 ✅什么是分布式数据库,有什么优势? 用了分布式数据库之后,为了提升买家和卖家的查询效率,需要分别创建索引: 1 2 3 4 5 6 7 8 9 10 CREATE TABLE Orders ( order_id BIGINT AUTO_INCREMENT PRIMARY KEY, buyer_id BIGINT NOT NULL, seller_id BIGINT NOT NULL, order_date DATETIME NOT NULL, amout DECIMAL(10, 2), status ENUM('Pending', 'Completed', 'Cancelled', 'Refunded'), INDEX idx_buyer_date (buyer_id, order_date), INDEX idx_seller_date (seller_id, order_date) ); 创建买家ID+时间、卖家 ID+时间等多个联合索引,来提升按照时间查询的效率。 ...

March 22, 2026 · 1 min · santu

大型电商的订单系统,如何设计分库分表方案?

典型回答 这是一个关于分库分表的场景题,其实涉及到的知识点在我们的分库分表的模块中都能知道,按照这个问题的解题思路,分析下该如何回答。 订单系统的分库分表,主要考虑的就是分表数量、分表字段、分表算法这几个方面,至于其他的什么分布式 ID、跨表查询这些可以先不用提,面试官问到了再进一步回答就好了。 题外话,最近不是也有很多其他在卖八股文么,然后好几个人跟我反馈过说在别人那里看到的就像是 GPT 生成的,于是我再写这个问题之前,我好奇的去问一下 GPT,看看他给出的答案是啥样的: 嗯,讲的有道理,但是都是一堆没用的废话。如果你这么给面试官说,那只能让你回去等通知了。 言归正传。我们接下来按照以下流程介绍这个问题的关键点。 分库还是分表 在让你设计分库分表的时候,一定要知道,分库、分表、分库分表他是三件事儿,不是一件事儿。所以首先你要说的就是我们需要考虑是否需要分库、是否需要分表。 如果说做这个方案的目的是为了解决单表数据量太大,查询效率慢的问题,那分表就够了,把这些数据分散到不同的表中,让每一张单表的数据量变小,那就可以了。 如果说,做这个方案的目的,是为了解决并发量太高,数据库连接数不够、数据库的资源性能(内存、CPU、磁盘)不够的问题,那就需要做分库,用更多的数据库实例来抗更高的并发。 所以说,分表解决的是大数据量的问题,分库解决的是高并发的问题。但一般来说,高并发往往伴随着大数据量,所以很多时候分库分表是一起做的。但是也不绝对。有些系统就是并发不高,但是年头很多了,数据量很大。 分表方式 确定了分库还是分表、还是直接做分库分表之后,就需要考虑分库、分表的方式、一般来说就两种,垂直分和水平分。 ✅什么是分库?分表?分库分表? 所谓垂直分库,就是把不同的业务的库拆分开,比如一个电商库,拆分成订单库、库存库、商品库。但是因为我们这个问题的前提已经限定了订单系统,所以不太涉及到垂直分库。 垂直分表,就是把一张大表,比如有100个字段,拆分成多张表,比如2张表,每张表各50个字段。这种在订单中是可行的,比如拆分成订单的主表以及扩展表,把一些订单的扩展信息单独拆分出去也是可以的。减少表中的字段数,也能降低存储量,提升查询效率。 水平分库(分表),就是把一个库(表)中包含所有数据,水平的分散到不同的库(表)中,比如原来单库(表)中有1000万数据,那么我们分成5分,每一份库(表)中就只有200万数据了。 一般来说,在订单系统中,垂直分表、水平分库、水平分表都是会做的。 分表数量 然后就是要考虑一下分表数量了,一个表,拆分成多个表,拆多少张合适呢? 这个一般是结合订单存量数据,以及增量数据的情况来看要分多少张表。我给一个公式(如有雷同,纯属抄袭): <font style="background-color:#FBDE28;">分表数量= (订单存量总数 + 预计年增长量 * 保留年限)/2000万 => 向上取最接近的2的幂</font> 假设存量数据已经有2000万了,预计每年增长500万,我们需要保留10年,那么就得出: (2000 + 500 * 10) / 2000 = 3.5 => 4 所以,在做分库分表的时候,可以根据这个公式,大致计算一下需要分多少张表,然后再根据并发的情况,大致算一下需要分多少个库,库的数量和表的数量之间没有必然联系,但是需要有倍数关系,如果你实在是不太好根据并发去预估库的数量,那么我给你个经验值的公式(如有雷同,纯属抄袭): <font style="background-color:#FBDE28;">分库数量 = 分表数量 / 8</font> 比如我们常用的分库分表数量: 128库,1024张表 64库,512张表 16库,128张表 8库,64张表 当然,如果你分表数量本身是小于8的,那要么是2,要么是4,那么我建议你就直接分库数量=分表数量即可。 分表字段 接下来就是分库分表中最重要的分表字段了。 我建议有两种方案:按照时间分、按照买家 ID 分。 ✅分表字段如何选择? 按照时间分表 订单的分库分表中,比较常见的就是按照订单时间去分表,或者按照确认收货时间去分表,总之就是按照时间字段来分。这个时间可以是固定时间,也可以是相对时间。比如说我可以按照年份,2022年、2023年、2024年每年一张分表,这就是固定时间分表。你也可以分成 2年前的订单、6个月前的订单、最近6个月内的订单 这样的方式去分表。 ...

March 22, 2026 · 1 min · santu

大量的手机号码被标记成骚扰电话,如何存储这些号码_

典型回答 手机号被标记成骚扰电话,这是一个典型的名单识别的场景,大量手机号,说明存储量大。所以,需要考虑的就是数据量大而带来的查询效率低下的问题。 数据库 这个方案的设计,首先需要有数据库,因为最终数据库的持久化肯定是要存在基于磁盘的关系型数据库的,这里的数据库可以是 MySQL等其他数据库都可以。 只需要按照如下方式建一张表,标记上手机号和标记时间即可。为了方便查询,提升效率,针对手机号增加一个唯一索引: 1 2 3 4 5 CREATE TABLE spam_numbers ( id BIGINT AUTO_INCREMENT PRIMARY KEY, phone_number VARCHAR(15) UNIQUE NOT NULL, marked_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); 有了数据库之后,按照如上方式,存储3000万左右的手机号,因为建了索引、都可以在1s 以内查询出结果。 分布式缓存 但是因为骚扰电话号其实是相对不变的,那么这里为了进一步提升性能,是非常适合使用缓存的。所以,可以在数据库之上加一层分布式缓存,如 Redis,把黑手机号保存在 Redis 中。 但是这里需要注意一个存储量的问题,大量的手机号保存在 Redis 中,会占用内存,浪费资源,所以可以考虑以下两个优化方案: 1、只保存一部分热点手机号 即只针对比较热的手机号做缓存,比如只缓存出现频率比较高的前20%的手机号,放到缓存中,并定期的更新缓存,或者用 LRU、LFU 等算法来做缓存的淘汰和更新。这里建议用 LFU。 ✅LRU 和 LFU 有啥区别? ✅MySQL 里有 2000W 数据,Redis 中只存 20W 的数据,如何保证 Redis 中的数据都是热点数据? 2、使用 bloomFilter 来保存 通过使用 BloomFilter 来保存数据,利用 bitmap 来减少内存的占用。这个可以看以下文章介绍,就不展开了: ...

March 22, 2026 · 1 min · santu

如何做SQL调优:用了主键索引反而查询很慢?

问题现象 线上定时任务执行失败,排查日志发现有慢SQL,分页扫表扫不动了。 具体SQL如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 SELECT * FROM `table_name` WHERE `DELETED` = 0 AND `STATE` = "INIT" AND `ID` >= 474968311 AND event_type = "" ORDER BY id LIMIT 100 问题分析 这个SQL用到了4个查询条件,分别是DELETED、STATE、event_type和ID。并且使用ID排序。这条SQL的执行耗时大概在16000ms左右。 先看了一下执行计划: 字段名 值 type range possible_keys PRIMARY,idx_event,idx_state_next_time,idx_state_event_deleted key PRIMARY rows 8121269 Extra Using where 通过这个执行计划可以看出,这里其实有多个索引可以选择,其中idx_state_event_deleted中包含了DELETED、STATE、event_type三个字段,但是最终优化器还是选择了PRIMARY主键索引。 ...

March 22, 2026 · 2 min · santu

如何做平滑的数据迁移_

数据迁移指的是把一批数据从一个数据源迁移到另一个数据源中。要做好一个平滑的数据迁移,首先需要做一下整体的设计,主要包括数据评估、迁移目标等。 首先我们要知道数据为什么要做迁移,是因为做了分库分表、还是要替换数据库,还是技术架构发生变化,以及历史数据太多了要做归档? 当我们知道目的了之后,就可以进一步的看一下我们要迁移的数据量有多大。是几十万,几百万,千万级还是亿级。不同的数据量,迁移的方案肯定是不一样的。如果只是几十万,不需要搞那么复杂,直接写代码同步过来再做一下数据核对都能搞得定。 还有一个关键的东西需要考虑,那就是是否需要做平滑迁移,也就是说是不是迁移过程中需要对业务没有任何感知。如果可以停机迁移,那就随便搞了。 假设我们以上都确定了,按照比较复杂的情况来说吧,假设我们的技术架构发生了改变,原来的表要废弃还到新表,并且表结构还不一样,数据量大概有1个亿,并且每天的增量也有几十万,并且要求平滑迁移。 数据类型 对于数据迁移来说,一般包含两部分数据,一部分是存量数据,一部分是增量数据。 存量数据指的是已经在原库中的数据,增量数据指的是程序运行过程中新产生的数据。 也就是说我们定义一个时间点,从这个时间节点之后,新insert的数据叫做增量数据,但是新update和delete的数据不一定是存量也不一定是增量,要看他insert的时候是增量还是存量。 关键问题 我们在做数据迁移的时候,有很多问题需要重点考虑,只有这些问题都考虑到了,并且解决了,才能算得上是真的做到了平滑数据迁移。 数据完整性:确保迁移过程中所有数据都能准确无误地迁移到新系统,没有数据丢失。 数据实时性:对于实时性要求高的业务,需要保证旧系统和新系统数据的实时同步,直至完全切换到新系统。 数据校验:需要有一个数据核对和校验的机制,这样我们才能知道数据是否做好了迁移。 灰度控制:在迁移的过程中,我们需要逐步进行,一旦有问题尽量通过灰度的方式让对业务影响最小。 回滚机制:需要尽可能的支持回滚,一旦有问题,可以快速的做回滚。 可用工具 在做数据迁移的过程中,我们不一定全部都要自己通过写代码来实现迁移,尤其是存量数据,我们其实是可以通过一些工具来做迁移的。 DataX:是阿里云 DataWorks数据集成 的开源版本,在阿里巴巴集团内被广泛使用的离线数据同步工具/平台。DataX 实现了包括 MySQL、Oracle、OceanBase、SqlServer、Postgre、HDFS、Hive、ADS、HBase、TableStore(OTS)、MaxCompute(ODPS)、Hologres、DRDS, databend 等各种异构数据源之间高效的数据同步功能。 DataX在阿里巴巴集团内被广泛使用,承担了所有大数据的离线同步业务。 canal:要用途是基于 MySQL 数据库增量日志解析,提供增量数据订阅和消费 早期阿里巴巴因为杭州和美国双机房部署,存在跨机房同步的业务需求,实现方式主要是基于业务 trigger 获取增量变更。从 2010 年开始,业务逐步尝试数据库日志解析获取增量变更进行同步,由此衍生出了大量的数据库增量订阅和消费业务。 Kettle:是一款开源的ETL(Extract, Transform, Load)工具,主要用于数据的抽取、转换和加载。通过Kettle,我们可以轻松地进行数据迁移,实现不同数据库之间的数据传输和整合。它允许你管理来自不同数据库的数据,通过提供一个图形化的用户环境来进行操作。 Flink CDC(Change Data Capture,即数据变更抓取)是一个开源的数据库变更日志捕获和处理框架,它可以实时地从各种数据库(如MySQL、PostgreSQL、Oracle、MongoDB等)中捕获数据变更并将其转换为流式数据。Flink CDC 可以帮助实时应用程序实时地处理和分析这些流数据,从而实现数据同步、数据管道、实时分析和实时应用等功能。 迁移流程 基于前面的讨论,如果我们想要实现迁移过程中的数据的完整性、实时性,预计对用户无感,通常是通过以下步骤进行迁移: 以上就是一个数据迁移的完整流程,从最开始的读写都从旧表进行,最终变成读写都从新表进行。 双写 数据双写是指同时进行新库和老库的写入,让新老库中同时都有最近的数据,这么做的主要原因一方面是让新数据不丢,另外也是随时可以做回滚。 双写的方案有很多,主要有以下这么几种: 借助工具增量双写,在增量双写过程中,我们可以用canal、flink cdc等这种增量数据更新的工具来进行双写,他们的原理就是在原库写入数据后,基于产生的binlog,自动把数据再写入新库。这个方案的好处是无需编码,支持各种异构的数据库,也支持表结构的各种不一致的转换。缺点就是有可能存在失败或者延迟的风险,一旦过程中出问题了,中间的数据可能会丢失。 通过代码实现双写,我们可以自己编码,在代码中实现双写,即写完旧库之后再写入新库。如果是同一个数据库的不同表的话,这里还可以借助事务保证数据的完整性。但是一般都是跨表或者跨库,所以这里需要考虑好重试的机制。优点就是代码实现的可靠性更强一些,逻辑可以自己定制,缺点就是需要写代码,比较复杂。 通过代码实现异步双写,这里同样是编码,但是我们不是直联数据库做写入,而是在写入新库的时候,通过MQ来做异步写入。 通过上面的流程图,其实可以发现,双写是几乎伴随着整个迁移的完整生命周期的,但是实际上,在第一次双写读旧(增量数据迁移) 和双写读新(增量数据迁移)的两个节点上,双写是不一样的,因为第一个阶段我们要保证旧库成功,新库可以失败(因为读发生在旧库上,要保证写入成功,否则会读不到数据)。但是第二个阶段,我们则需要保证新库必须成功,旧库可以失败。(因为读发生在新库上,要保证写入成功,否则会读不到数据) 所以,这就涉及到一个切换的问题,如果通过工具的话,就需要配置两个同步任务,最开始是旧表到新表,后面要改成新表到旧表。而这里如果用代码的话就好控制一点,我们可以提前把两个分支逻辑都写好,然后只需要在运行过程中通过配置做一下切换就好了,过程中有问题也可以随时切回来。 所以,建议在做增量同步的时候,采用写代码的方案,一般来说是直接在DAO层做代理去改代码。至于直接调数据库做双写,还是中间加一层MQ做双写,其实都可以。就看数据量了,如果比较大的话,可以用MQ做一下削峰填谷。 增量数据核对 在数据迁移过程中,做数据核对是至关重要的。尤其是增量数据,因为增量数据需要双写,双写是有可能失败的,所以需要有一定的机制可以发现这些双写失败的数据。 在做数据核对方面,有很多方式,https://www.yuque.com/hollis666/ec96i7/vh0msbr3qrqzfrfm 这里讲过了,就不再赘述。 但是这里给大家建议一个方案,那就是做旁路验证。 当我们在做读操作的时候,不管是双写一阶段的读旧库,还是双写二阶段的读新库。我们都可以同时做一下旁路验证。 比如一阶段在做旧库的读的时候,我们可以通过旁路(可以起一个异步线程,或者通过MQ等)进行一次新库的读取,然后把拿到的结果和旧库的读取作比对,当发现不一致的时候,报警报出来,进行人工核对。 增量数据的更新 在数据迁移过程中,业务数据是会发生不断地变化的,前面的双写我们提了,只把新发生的insert当做增量。那这个过程中,如果发生了update,怎么办呢? 这里需要我们做一个逻辑,那就是在update/delete的时候,判断一下这个数据是增量的还是存量的,其实就是判断新表中是否有这个数据,如果有,就需要做双写更新,如果没有,就说明这是个存量数据,只更新旧表就行了。不用担心他们会丢,因为后续我们会做存量同步,这部分更新也会带过来的。 1 2 3 4 5 6 7 8 9 publiv viod update(){ if(增量数据){ 同时更新旧表和新表(); }else{ 更新旧表(); } } 存量数据迁移 做好了增量数据迁移和核对之后,我们就可以把存量的数据也迁移过来了。 ...

March 22, 2026 · 1 min · santu

如何实现一个点赞系统?

典型回答 关于点赞的方案设计,我看最近讨论的还挺多的。很多人认为点赞不需要用Redis,直接数据库干就行了。这么说对不对呢?我们分析下点赞的难点在哪? 对于点赞来说,最大的难点可能就是高并发情况下如何确保点赞数量的准确性了。 那么也就是说,如果你没有高并发场景(大部分点赞业务都没有),或者你不在乎准确性(大部分点赞也不在乎),那么就没任何难度。 既然没难度,那就最简单的实现方式,在数据库做累加就行了。一条SQL搞定: 1 update table set like = like + 1 where id = xxx; 但是,如果涉及到高并发,比如说视频直播间的点赞,就会有高并发的情况,还记得黄子韬直播间点赞数变成负数的事么,既然都能超过int的最大值,那么说明点赞的并发一定不低的。 **高并发情况下的点赞,其实就是个热点更新问题了。**因为你要对同一个记录,即同一个直播间的点赞数做累加。最大的问题就是如果只靠数据库的update的话,这就和秒杀的商品库存扣减是一回事儿了。 如果要用数据库去扣减的话,可以考虑拆分、合并、以及hint的方式。详见下面这几个。 ✅高并发的库存系统,在数据库扣减库存,怎么实现? ✅阿里的库存秒杀是如何实现的? 但是实际上,在点赞这个业务中,我们就拿直播间点赞来说,他比较适合的其实是合并的这个方案。因为直播间点赞的典型场景就是一个用户会在直播间不断的点赞,但是其实我们不需要每一次用户点赞都传递到后端去做点赞的累加。只需要前端做个批次,一次性把用户一段时间内的总点赞数传递到后端去,后端再去更新到数据库中就行了,这其实就可以大大的降低并发。 还有就是Redis到底能不能用,我认为点赞场景用redis肯定是可以的,但是要看有没有必要,如果是高并发, 并且对性能也有比较高的要求,是可以先在Redis中保存一下点赞数,利用Redis的高并发特性做累加,然后再定期的把点赞结果同步到数据库中的。 这里面带来的缓存和数据库的一致性问题,以及redis挂了数据丢了怎么办的问题,其实在点赞这个场景中,对一致性要求并不高,你说一个直播间,到底是点赞10086次,还是11111次,其实只要量级上差别不大即可,他并不是一个对一致性要求极高的场景。 以下就是个最完善的点赞场景的数据更新方案,结合了批量提交、Redis存储以及数据库拆分的方案。 查询的时候,先去Redis查询直接返回即可,如果Redis中没有数据(一般不会,除非是Redis挂了之后换了节点之类的),再去数据库中查询,但是因为数据库中做了拆分,所以要把同一个直播间在不同的地区中的点赞数做个汇总,把结果返回给前端并保存在Redis中。

March 22, 2026 · 1 min · santu

如何实现敏感词过滤?

典型回答 敏感词过滤是非常常见的一种手段,避免出现一些违规词汇。在实现上,有很多种方案。 字符串匹配 字符串匹配是最简单、直观的方法,直接在文本中查找是否存在敏感词列表中的词汇。如在Java中使用contains方法或者正则表达式都可以判断。 但是他只适合小规模文本或敏感词较少的场合下使用,比如我们在避免SQL注入的时候,需要对用户输入的内容进行一些特殊字符的过滤,就可以用这种方式,又或者是我们在做数据脱敏的时候,也可以用这种方式实现。 他的优点就是,实现简单,易于理解。但是缺点也很明显,就是效率比较低,特别是在处理大量文本或敏感词库较大时。还有就是无法处理变体词汇,如错别字、同音字等。 因为字符串匹配的基本原理是遍历待检测的文本,对于每一个可能的起始位置,检查是否有敏感词与之匹配。如以下是String中contains方法的主要实现: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public boolean contains(CharSequence s) { return indexOf(s.toString()) > -1; } public int indexOf(int ch, int fromIndex) { final int max = value.length; if (fromIndex < 0) { fromIndex = 0; } else if (fromIndex >= max) { // Note: fromIndex might be near -1>>>1. return -1; } if (ch < Character.MIN_SUPPLEMENTARY_CODE_POINT) { // handle most cases here (ch is a BMP code point or a // negative value (invalid code point)) final char[] value = this.value; for (int i = fromIndex; i < max; i++) { if (value[i] == ch) { return i; } } return -1; } else { return indexOfSupplementary(ch, fromIndex); } } 可以看到它是通过一个for循环从头开始进行遍历的。 ...

March 22, 2026 · 1 min · santu

如何用Redis实现朋友圈点赞功能?

典型回答 首先我们需要分析下朋友圈点赞需要有哪些功能,首先记录某个朋友圈的点赞数量,并且支持点赞数数量的查看,支持点赞和取消点赞操作。并且支持查看哪些人点过赞,并且点赞的顺序是可以看得到的。 那么,基于以上信息,我们可以这样实现: 在数据结构上,我们可以采用ZSet来实现,KEY就是这个具体的朋友圈的ID,ZSET的value表示点赞用户的ID,score表示点赞时间的时间戳。这样可以方便地按照时间顺序查询点赞信息,并支持对点赞进行去重, 使用字符串存储每篇朋友圈的ID,作为有序集合的KEY。 使用zset存储每篇朋友圈的点赞用户信息,其中value为点赞用户的ID,score为点赞时间的时间戳。 点赞操作:将用户的ID添加到zset中,score为当前时间戳。如果用户已经点过赞,则更新其点赞时间戳。 取消点赞操作:将用户的ID从有序集合中删除。 查询点赞信息:使用有序集合的ZREVRANGEBYSCORE命令,按照score(即时间戳)逆序返回zset的value,即为点赞用户的ID。 以下是代码实现: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 import redis.clients.jedis.Jedis; import redis.clients.jedis.Transaction; import redis.clients.jedis.ZParams; /** * @author Hollis **/ public class RedisLikeDemo { private static final String LIKE_PREFIX = "like:"; private static final String USER_PREFIX = "user:"; //点赞 public static void likePost(String postId, String userId, Jedis jedis) { String key = LIKE_PREFIX + postId; Long now = System.currentTimeMillis(); jedis.zadd(key, now.doubleValue(), userId);// 将用户ID及当前时间戳加入有序集合 } //取消点赞 public static void unlikePost(String postId, String userId, Jedis jedis) { String key = LIKE_PREFIX + postId; jedis.zrem(key, userId);// 将用户ID从有序集合中移除 } //查看点赞列表 public List<String> getLikes(String postId, Jedis jedis) { String key = LIKE_PREFIX + postId; ZParams zParams = new ZParams().desc(); return jedis.zrangeByScoreWithScores(key, "+inf", "-inf", 0, -1, zParams) .stream() .map(tuple -> { String userId = tuple.getElement(); return userId; }).collect(Collectors.toList()); } } 在上述代码中,likePost方法用于点赞,unlikePost方法用于取消点赞,getLikes方法用于查询点赞信息。 ...

March 22, 2026 · 1 min · santu

如何解决消息重复消费、重复下单等问题?

典型回答 重复消费、重复下单的问题,主要的解决办法就是做好幂等,因为在分布式系统中,我们是没办法保证消息不会重新投递的,也没办法保证用户一定不会快速的点击两次进行下单。 所以,对于服务的提供者来说,我们需要在接口中做好幂等控制,来避免因为重复而导致的脏数据。 对于消息的重复消费问题,比较常见的解决方式,就是通过消息中定义的一个幂等号,来做防重判断。这个幂等号一般是约定好的一个业务字段,如果没有这样一个字段的话,也可以用消息中间件的msg_id来做幂等控制,但是可能存在一个情况,那就是发送者重复发送了多次消息,这就会导致几次消息的msg_id不一样,但是消息内容一致。所以,一般都需要在消息中约定一个唯一的幂等字段或者业务字段。 而对于重复下单的场景,这个幂等号应该怎么产生呢?有一个好的办法就是生成token,当用户每一次访问页面的时候(比如订单详情页,或者下单页),都向后端接口请求获取一个token,然后在之后本页面的操作中,都需要把这个token带过来。如果页面没有刷新,这个token应该是不变的。 有了这个token就可以用它来做防重控制,并且还能避免有人恶意的刷我们的接口。 在消息或者下单场景中,有了唯一的幂等字段之后,就可以基于一锁、二判、三更新来进行幂等控制了,详见: ✅如何解决接口幂等的问题? 扩展知识 token验证 使用Redis可以很方便地实现token的验证,并且可以让一个token只能用一次,具体的实现方式如下: 1 2 3 4 5 6 7 8 9 10 11 12 import redis.clients.jedis.Jedis; import java.util.UUID; public class RedisToken { public static void main(String[] args) { Jedis jedis = new Jedis("localhost"); String token = UUID.randomUUID().toString(); jedis.setex(token, 60 * 60, "1"); System.out.println("Token: " + token); jedis.close(); } } 这里只展示了用UUID的方式,而在分布式场景中,如果要生成一个全局唯一的ID,有很多其他方案,这里就不展开介绍了,详见: ...

March 22, 2026 · 2 min · santu

如果有1TB的数据需要排序,但只有32GB的内存如何排序处理?

典型回答 这是一个典型的海量数据处理的问题,1T数据排序,但是内存只有32G,那么就意味着,不可能一次性加载到内存中进行排序了。 解决这个问题,最常见的一种方案就是——外部归并排序,外部归并排序主要是把一个大的排序过程拆分成分块排序和多路归并两个步骤。 分块排序 将 1TB 数据分成多个小块 如果32G内存全都干满,刚好32*32 = 1024,但是实际上每个块不太可能把32G内存全部都用上,需要预留一部分给操作系统用,还要有些内存用来做排序操作。 所以,我们假设每个块需要30G左右内存吧,那么大概需要分成35个块,我们干脆直接取一个2的倍数,分成36块。 逐块读取到内存中,在内存中对每个小块进行高效排序(如快排、归并、Timsort)。 将每块排序结果写回磁盘,生成多个有序的临时文件(如chunk1.sorted, chunk2.sorted等)。 按照以上操作技术后,我们就得到了36个有序的文件,但是这个有序是局部有序(单文件内)的,我们还需要做全局排序。 但是做这个全局排序的时候,还是没办法都加载在内存中的,那么比较简单的的方案就是同时读取多个块(文件),每次取最小的元素。 多路归并 如果,我们针对36个文件进行排序的话,我们的内存需要分成两个职责,分别是输入缓冲区和输出缓冲区。 输出缓冲区:预留一部分内存用于缓存合并后的结果。 输入缓冲区:内存中均分给每个块的输入缓冲区。 假设我们给输入缓冲区和输出缓冲区都留1G的话,那么明显32G是无法同时读取36个文件的,因为这样的话需要至少36*1G + 1G = 37G内存。 这时候,我们就需要分批进行了。这就是所谓的多路归并。比如刚刚说的36个文件同时读取就是36路归并。 这里的多路到底怎么选呢?上面提到了要受内存限制的影响,还有就是要考虑磁盘IO的情况,因为如果给缓冲区留的内存越多的话,那么磁盘读取的次数就会更小。而缓冲区内存更大,意味着分路要更少。而路数更少,意味着归并的次数要越多。(一次干36个,和一次干2个,肯定后面的活干的次数多) 所以,综合以上考量,我们可以考虑做8-18分路。假设我们取其中的可以被36整除的9吧。 打开9个有序块(临时文件),为每个块分配文件句柄,并预读初始数据到输入缓冲区。 从每个块的输入缓冲区中取第一个元素,与块索引(就是来自哪个文件)一起插入最小堆。 弹出堆顶元素(当前最小),写入输出缓冲区。记录该元素来源的块索引(假设来自块 i)。 从块 i的输入缓冲区中取下一个元素。 若块 i 的缓冲区已空,从磁盘读取下一批数据到缓冲区,若块 i 的数据已全部读完,则关闭该块。 若块 i 仍有数据,将缓冲区的新元素插入堆中。若堆为空,归并结束。 当输出缓冲区满,将其写入最终结果文件,并清空缓冲区。 以上操作之后,9个文件就会被合并成一个文件,比如叫chunk_merge1.sorted,然后继续读取另外9个文件,这样分四次,就合并成了4个有序文件了。 然后就继续按照上面的顺序,读取这四个文件,进行排序即可,最终就可以输出为一个文件了。

March 22, 2026 · 1 min · santu

留言给博主