什么是QPS,什么是RT?

典型回答 QPS,指的是系统每秒能处理的请求数(Query Per Second) ,在Web应用中我们更关注的是Web应用每秒能处理的request数量。这个是衡量系统性能的重要指标。 RT,指的是响应时间(Response Time),是指从客户端发一个请求开始计时,到客户端接收到从服务器端返回的响应结果结束所经历的时间。 扩展知识 RT 响应时间(Response Time),是指从客户端发一个请求开始计时,到客户端接收到从服务器端返回的响应结果结束所经历的时间。 当我们评价一个网站的"快"和"慢"的时候,其实说的就是他的RT时间的长和短。当我们访问某个网站,有时候我们会说这个网站很"卡",其实言下之意说的就是这个网站的RT很长。 如果一个网站的RT很长的话,就会特别的影响用户体验。所以,RT是很重要的一个指标。也是各个网站需要重点优化的。 拿一个游乐园的例子来说明一下可能会比较容易理解,比如我们去迪士尼乐园游玩时候,大多数的项目都是需要排队的。 为了让游客知道一个项目需要排队多久才能玩上,迪士尼做了很多事情,比如他们有一个App,专门可以提示每个项目的预计排队时间。再有就是每个项目的排队处都有一个小牌牌,上面写着预计排队时间。  但是,这个时间并不是凭空设定出来的,而是『计算』出来的。 迪士尼的排队时间计算方法: 1、迪士尼在每个项目的入口处和出口处都会设置工作人员。 2、入口处工作人员随机寻找游客,给游客一张小纸条,上面记录着游客开始排队的时间。 3、入口处工作人员提醒游客,项目游览玩之后,在出口处把小纸条交还给出口处的工作人员。 4、出口处工作人员在收到游客的小纸条后,会用:当前时间-游客开始排队的时间 = 排队时长。 5、为了尽量让数据准确,一般会收集多个排队时长之后,计算一个平均值。 以上就是迪士尼的排队时间计算法。其实,这也是RT的计算方法。在一个请求开始的时候记录时间,请求结束的时候再记录时间,两个时间的差值,就是RT了。 迪士尼的一个项目的RT包含了多个时间段:排队时间、听项目讲解时间、项目准备时间、项目游玩时间等。 服务器响应时间也有多部分组成,一般包含:请求发送时间、网络传输时间和服务器处理时间等三部分。 QPS QPS,指的是系统每秒能处理的请求数(Query Per Second) ,在Web应用中我们更关注的是Web应用每秒能处理的request数量。这个是衡量系统性能的重要指标。有时候,我们也称之为吞吐量。 QPS和RT几乎总是成对出现的。当我们评价迪士尼的一个项目的好坏的时候,通常会包含这几个指标:是否好玩、游玩时长以及可以同时容纳多少人。 这个可以同时容纳多少人,就可以简单的理解为QPS。很大程度上,一个项目同时可以容纳多少人,其实会大大的影响游客的游玩时长。 所以,QPS和RT之间是有着一定的关系的(单线程情况下,多线程的话还要再乘一个线程数): 1 2 RT= 并发数/QPS QPS= 并发数/RT 虽然上面的等式看上去,在并发数一定的情况下,想要提升QPS的话就只能降低RT。但其实并不是,以上只是QPS的计算方法。想要提升QPS往往有很多手段。 就像想要提升游乐设施的吞吐量,最首先想到的办法就是升级设备,比如增加游乐场地的面积,增加设备的座位数目,增加排队的队伍个数等。 在计算机系统中,想要提升QPS,主要可以在CPU、内存等硬件上面下功夫,比如提升CPU利用率、增加CPU数目、提升内存等。 并发用户数 并发用户数指的就是同时跑到一个项目前面排队的人数。  关于并发用户数有两种常见的错误观点。 一种错误观点是把并发用户数量理解为使用系统的全部用户的数量;(比如迪士尼的飞跃地平线项目一天可能会接纳50万人,我们不能说这个50万就是并发用户数) 还有一种错误观点是把用户在线数量理解为并发用户数量。(比如晚上六点的时候,迪士尼的飞跃地平线项目排队加观看人数共有1万人,我们不能说这个1万就是并发用户数) 并发用户数量的正确理解为:在同一时刻与服务器进行了交互的在线用户数量。(我们说,晚上六点的时候,共有8000人正在排队使用飞跃地平线这个项目。这才是并发用户数) 拿系统来说,我们说淘宝详情页的并发用户数,其实说的是同一时刻请求查看详情页的用户个数。有些用户虽然也在浏览详情页,但是它并没有在并发时刻和系统有交互,这就不算的。 最佳线程数 最佳线程数指的就是一个项目最多可以容纳的人数,这里的容纳可以包含排队的人数。  迪士尼每新开一个场馆或者一个游戏项目的时候,都会是一个试运营的阶段。在试运营阶段,通过不断调整并发用户数来观察整个场馆或者项目的运行情况。 除了上线新场馆和新项目以外,有的是在节假日之前也会有一些类似的实验。 这和计算机软件的压测很像。就是不断的提高请求数目,来观察系统的QPS和系统的其他指标,如CPU情况、内存情况等。 性能压测的情况下,起初随着用户数的增加,QPS会上升并对CPU等影响不大,当到了一定的阈值之后,用户数量增加QPS并不会增加,或者增加不明显,同时CPU Load有飙高、内存占用大等情况发生。随之而来的伴随着请求的响应时间大幅增加。这个阈值我们认为是最佳线程数。 如果并发请求数目,超过了系统的最佳线程数,那么就会导致激烈的资源竞争,随着资源的匮乏甚至枯竭,整个系统也就面临着灾难。

March 22, 2026 · 1 min · santu

什么是布隆过滤器,实现原理是什么?

典型回答 布隆过滤器是一种数据结构,用于快速检索一个元素是否可能存在于一个集合(bit 数组)中。 它的基本原理是利用多个哈希函数,将一个元素映射成多个位,然后将这些位设置为 1。当查询一个元素时,如果这些位都被设置为 1,则认为元素**可能存在于集合中,否则肯定**不存在。 所以,布隆过滤器可以准确的判断一个元素是否一定不存在,但是因为哈希冲突的存在,所以他没办法判断一个元素一定存在。只能判断可能存在。 所以,布隆过滤器是存在误判的可能的,也就是当一个不存在的Hero元素,经过hash1、hash2和hash3之后,刚好和其他的值的哈希结果冲突了。那么就会被误判为存在,但是其实他并不存在。 想要降低这种误判的概率,主要的办法就是降低哈希冲突的概率及引入更多的哈希算法。 下面是布隆过滤器的工作过程: 初始化布隆过滤器 在初始化布隆过滤器时,需要指定集合的大小和误判率。布隆过滤器内部包含一个bit数组和多个哈希函数,每个哈希函数都会生成一个索引值。 添加元素到布隆过滤器 要将一个元素添加到布隆过滤器中,首先需要将该元素通过多个哈希函数生成多个索引值,然后将这些索引值对应的位设置为 1。如果这些索引值已经被设置为 1,则不需要再次设置。 查询元素是否存在于布隆过滤器中 要查询一个元素是否存在于布隆过滤器中,需要将该元素通过多个哈希函数生成多个索引值,并判断这些索引值对应的位是否都被设置为 1。如果这些位都被设置为 1,则认为元素可能存在于集合中,否则肯定不存在。 布隆过滤器的主要优点是可以快速判断一个元素是否属于某个集合,并且可以在空间和时间上实现较高的效率。但是,它也存在一些缺点,例如: 布隆过滤器在判断元素是否存在时,有一定的误判率。 布隆过滤器无法删除元素,因为删除一个元素需要将其对应的多个位设置为 0,但这些位可能被其他元素共享。 扩展知识 应用场景 布隆过滤器因为他的效率非常高,所以被广泛的使用,比较典型的场景有以下几个: 网页爬虫:爬虫程序可以使用布隆过滤器来过滤掉已经爬取过的网页,避免重复爬取和浪费资源。 缓存系统:缓存系统可以使用布隆过滤器来判断一个查询是否可能存在于缓存中,从而减少查询缓存的次数,提高查询效率。布隆过滤器也经常用来解决缓存穿透的问题。 分布式系统:在分布式系统中,可以使用布隆过滤器来判断一个元素是否存在于分布式缓存中,避免在所有节点上进行查询,减少网络负载。 垃圾邮件过滤:布隆过滤器可以用于判断一个邮件地址是否在垃圾邮件列表中,从而过滤掉垃圾邮件。 黑名单过滤:布隆过滤器可以用于判断一个IP地址或手机号码是否在黑名单中,从而阻止恶意请求。 如何使用 Java中可以使用第三方库来实现布隆过滤器,常见的有Google Guava库和Apache Commons库以及Redis。 如Guava: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import com.google.common.hash.BloomFilter; import com.google.common.hash.Funnels; public class BloomFilterExample { public static void main(String[] args) { // 创建布隆过滤器,预计插入100个元素,误判率为0.01 BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(), 100, 0.01); // 插入元素 bloomFilter.put("Hollis"); bloomFilter.put("666"); bloomFilter.put("八股文"); // 判断元素是否存在 System.out.println(bloomFilter.mightContain("Hollis")); // true System.out.println(bloomFilter.mightContain("王星星")); // false } } Apache Commons: ...

March 22, 2026 · 2 min · santu

什么是读写分离?如何实现?

典型回答 在分布式系统设计中,读写分离是一种常见的架构模式,可以提升系统的处理能力、扩展性和可用性。简单来说就是分开处理读和写操作。 读操作:通常指的是从数据库中检索数据的操作,比如 SQL 查询。 写操作:包括创建、更新或删除数据库中的数据,比如 SQL 的 INSERT、UPDATE、DELETE 语句。 读写分离的好处: 提高性能:一般来说,大型分布式应用中都是读多写少的。将读写操作分离可以显著提高数据库系统的整体性能。 提高可扩展性:读写分离允许系统按需增加从数据库实例,以应对读请求量的增长,从而提高系统的可扩展性。 增加可用性和容错性:在主-从复制架构中,如果主数据库出现故障,可以从从数据库中选举或提升一个为新的主数据库,从而减少系统的停机时间。 负载均衡:通过在多个从数据库之间分散读请求,可以实现负载均衡,避免单个数据库的过载,从而提高系统的响应速度和稳定性。 我们都知道MySQL提供了主从复制的能力,所以我们就可以基于MySQL自带的主从复制的能力来实现读写分离。 ✅MySQL主从复制的过程 在这种模式下,写操作只在主数据库(Master)上执行,而读操作则可以在从数据库(Slave)上执行。主库和从库通过主从复制来保持数据的同步。 在通过主从复制实现读写分离的架构中,从库可以是一个,也可以是多个。也就是说可以是一主一从、也可以是一主多从。 如何做读写的分流? 如何实现让写流量请求到主库,读流量请求到从库呢,这就涉及到具体的读写分流了。 一般来说,首先我们需要把接口从定义上或者从职责上划分清楚,读接口和写接口。如UserReadService就是专门负责提供读服务的,UserWriteService就是专门负责写服务的。 接下来,ReadService在操作的时候,只需要和从库进行交互,而WriteServie在操作的时候只需要和主库进行操作就行了。具体分流方式有以下集中: 1、代码分流 最简单直观的方式,就是我们自己编码实现,我们可以在DAO层定义多个数据源,然后在实际进行读或者写操作的时候,选择使用不同的数据源即可。 如以下方式定义两个不同的DataSource: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Configuration public class DataSourceConfig { @Bean @Primary public DataSource primaryDataSource() { // 配置主数据源 return DataSourceBuilder.create().url("jdbc:mysql://master_db:3306/mydb").username("user").password("pass").build(); } @Bean public DataSource replicaDataSource() { // 配置从数据源 return DataSourceBuilder.create().url("jdbc:mysql://replica_db:3306/mydb").username("user").password("pass").build(); } } 在定义一个动态数据源: ...

March 22, 2026 · 2 min · santu

如何进行线程池调优?

典型回答 Java的线程池调优是一个比较常见的问题,但是!虽然面试常见,但是其实工作中并不经常做(大部分公司都不太需要调优。。。 想知道怎么做线程池调优,首先要知道,线程池都能调哪些东西。 ✅什么是线程池,如何实现的? 上面这篇中介绍过,线程池的一些核心参数,其实调优就是围绕着这些参数来做的。 corePoolSize: 核心线程数量,可以类比正式员工数量,常驻线程数量。 maximumPoolSize: 最大的线程数量,公司最多雇佣员工数量。常驻+临时线程数量。 workQueue:多余任务等待队列,再多的人都处理不过来了,需要等着,在这个地方等。 keepAliveTime:非核心线程空闲时间,就是外包人员等了多久,如果还没有活干,解雇了。 threadFactory: 创建线程的工厂,在这个地方可以统一处理创建的线程的属性。每个公司对员工的要求不一样,恩,在这里设置员工的属性。 handler:线程池拒绝策略,什么意思呢?就是当任务实在是太多,人也不够,需求池也排满了,还有任务咋办?默认是不处理,抛出异常告诉任务提交者,我这忙不过来了。 线程数 线程池有两个重要的和线程数有关的参数,就是corePoolSize和maximumPoolSize,一般线程池调优的时候,大部分也都是调这两个参数。。 ✅线程数设定成多少更合适? 上面这篇中我们介绍过了,可以根据我们给过的公式,先设置一个核心线程数的初始值。简单的的话就是: 如果是CPU密集型应用,则核心线程数可以设置为N+1 如果是IO密集型应用,则核心线程数可以设置为2N+1,或者是2N-4N之间也都是可以的。 也就是说,IO密集型的应用,你的线程数可以设置的更多一些,因为对于I/O密集型任务,等待时间通常远大于计算时间,这意味着可以分配更多的线程。当一个线程在等待时(如等待网络响应),CPU可以切换到另一个线程进行计算,从而提高CPU利用率。 那如果想要一个更加准确一点的公式,那么可以用这个: 设置了初始的线程数之后,就可以根据线上的运行情况,或者直接做压测的情况,来看线程数是不是需要调整,怎么看呢?主要观察这几个指标(这些指标都可以通过prometheus来实现监控,这里不展开了,在我的项目课中有监控的实践。): 活动线程数:当前正在执行任务的线程数。通过它可以了解当前线程池的负载。如果这个值长期处于较高水平,可能意味着需要更多线程来处理任务。 **队列长度 **:阻塞队列的长度,代表等待执行的任务数。如果队列长度经常很长,说明任务的提交速度超过了线程池的处理能力。 已完成任务数:表示已处理完成的任务数,帮助你评估线程池处理任务的速度。 **任务拒绝数 **:如果任务数超过了线程池能处理的最大负载,拒绝策略会启动。如果拒绝的任务数较多,说明线程池容量不足,可能需要增加线程数或调整队列长度。 CPU利用率:表示CPU正在处理任务的时间占总时间的比例。通常,高 CPU 利用率意味着系统正在高效地使用计算资源,而低 CPU 利用率可能意味着计算资源没有得到充分利用,可能会有性能瓶颈。 线程上下文切换次数:操作系统切换 CPU 上运行的线程的过程。每当线程状态发生变化(比如从运行状态变为等待状态,或从等待状态变为运行状态),操作系统就会进行一次上下文切换。 一般会出现以下集中现象: 1、队列长度长且任务完成速度慢,说明线程池处理能力不足。可以考虑增加核心线程数或最大线程数,或者增大队列容量。 2、频繁的线程上下文切换,意味着系统在管理线程时花费了大量的 CPU 时间,这通常是由于线程数过多导致的。特别是在CPU 密集型任务中,线程数过多会导致CPU资源被上下文切换消耗,反而影响任务的执行效率。这时候要考虑降低线程数。 3、高CPU利用率,说明大部分 CPU 时间都在执行任务,通常表示系统在高负载运行。可以适当的降低线程数。 4、任务被拒绝的数量较多,说明当前线程池无法处理当前的负载。需要通过增加线程数、增加队列容量,或优化任务处理速度来解决。 对于最大线程数,一般就设置为核心线程数的2倍就行了,或者有的也可以直接设置成和核心线程数一样。 所以,核心线程数的调节可以遵守这个原则(个人总结,如有雷同,纯属缘分): 提升线程数:线程数不够了(队列长、任务执行慢、任务被拒绝), 并且提高也不会对系统造成影响(CPU利用率不高,load不高)的情况下。 降低线程数:用不上这么多线程(活跃线程数低)或者线程多反而导致执行慢(上下文切换多)或者线程数对系统造成影响了(CPU利用率高,load高)。 队列 线程池中的任务队列用于存放待处理的任务,直到有空闲线程来执行它们。常用的队列类型有: ArrayBlockingQueue:基于数组的有界队列,适用于任务数已知的情况。 LinkedBlockingQueue:基于链表的有界或无界队列,适用于任务数不确定的情况。 SynchronousQueue:一个没有内部容量的队列,每个插入操作必须等待一个相应的移除操作,适用于任务量大且短时间内高并发的情况。 PriorityBlockingQueue:优先级队列,适用于任务要按照优先级执行的情况。 对于负载较高的场景,可以使用 LinkedBlockingQueue 或 SynchronousQueue,它们能够适应动态的任务需求。而对于任务较为平稳且队列大小可以预测的场景,可以使用 ArrayBlockingQueue。 拒绝策略 当线程池中的线程数已达最大限制且队列也满了,就会执行拒绝策略。有以下几种: AbortPolicy(默认):抛出 RejectedExecutionException。 CallerRunsPolicy:由调用者线程执行任务。 DiscardPolicy:直接丢弃任务。 DiscardOldestPolicy:丢弃队列中最旧的任务。 一般来说默认的就行了,但是如果任务非常重要,推荐使用 CallerRunsPolicy,确保任务不会丢失。 ...

March 22, 2026 · 1 min · santu

说下什么是p90,p95,P99?

典型 P90、P95 和 P99 这些术语,常见于性能测试、响应时间分析、统计分析等领域,这是一种统计学中的百分位数(percentile)的表达方式,用来描述数据集中不同分布位置的数据点。 百分位数表示一个数值在一组数据中的相对位置。例如,第X百分位数(P_X)指的是一组数据中有X%的数据点小于或等于该数值,剩下的(100-X)%的数据点大于该数值。 P90(第90百分位数):表示90%的数据点小于或等于该值,剩下的10%的数据点大于这个值。比如在性能测试中,如果系统的响应时间的P90是500毫秒,那么意味着90%的请求响应时间小于或等于500毫秒,只有10%的请求响应时间超过500毫秒。 P95(第95百分位数):表示95%的数据点小于或等于该值,剩下的5%大于该值。P95常用于识别性能问题中的“尾部延迟”,即大部分请求响应速度良好,但有一小部分请求响应时间过长。 P99(第99百分位数):表示99%的数据点小于或等于该值,剩下的1%大于该值。P99通常用来衡量系统中极少数极端慢的请求,这些请求可能由资源瓶颈或特殊情况引起。 这些百分位数常用于衡量系统性能、延迟、响应时间等数据的分布,特别是为了了解系统在尾部表现如何,即关注那些极端的、不常见的、但会影响用户体验的慢请求或问题。 P50:也称为中位数,表示50%的数据点小于或等于这个值,用来衡量整体表现。 P90、P95:常用于识别系统的高延迟部分,能帮助发现大多数用户的体验情况和少数不良表现的趋势。 P99:通常用于观察极端情况,特别是在高并发环境中,尾部请求的响应时间可能显著拖慢系统性能。 举个例子: 假设有一组请求响应时间(单位:毫秒)为: [100, 120, 130, 140, 150, 160, 170, 180, 190, 200, 210, 220, 230, 240, 250] P90:第90百分位数是225毫秒,意味着90%的请求响应时间小于或等于225毫秒,10%的请求大于225毫秒。 P95:第95百分位数是235毫秒,意味着95%的请求响应时间小于或等于235毫秒,5%的请求大于235毫秒。 P99:第99百分位数是245毫秒,意味着99%的请求响应时间小于或等于245毫秒,只有1%的请求大于245毫秒。 通过这种方式,可以评估和优化系统的性能,并找到优化的瓶颈。

March 22, 2026 · 1 min · santu

如何设计一个高性能的分布式系统

典型回答 设计高性能的分布式系统需要考虑多个因素,简单介绍一些和性能优化、高性能有关的技术方案。 1、部署架构 选择合适的分布式系统架构,例如微服务架构、SOA架构等,可以有效地提高系统性能。 ✅分布式和微服务的区别是什么? 2、缓存 使用缓存技术可以减轻数据库的负载,提高系统性能。而且缓存也分很多,分布式缓存、本地缓存等等,理论上来说离用户越近的缓存效果越好,但是相对来说一致性可能也越差一点。 ✅本地缓存和分布式缓存有什么区别? 3、缓存预热 缓存可以提升性能,但是有的时候缓存如果没有做提前预热,也可能会导致一些热点问题和缓存击穿的问题,会导致性能下降,所以需要考虑预热的方案。 ✅如何实现缓存的预热? 4、异步 异步,这个是最容易理解的,通过异步的方案让代码不要同步执行,可以减少等待时间。不管使用消息队列,还是用线程池,亦或是定时任务,都是比较常见的异步的方案。 5、多线程 这个就很好理解了,多个线程一起干活,要比单个线程执行快的。当然,还可以考虑使用线程池来减少线程的创建和销毁过程的开销。 6、SQL优化 很多时候,一个系统的性能瓶颈是在数据库层面的,尤其是一些慢sql,会导致应用的整体性能下降,所以,需要做SQL优化才行。 ✅如何进行SQL调优? 7、单元化架构 在单元化架构中,分成很多个单元和一个中心。每个单元之间都是相互独立的。一个单元中完整的部署了一整套业务,如电商交易,金融支付,一次用户操作可以在一个单元内部完成,不需要跨单元执行。可以减少网络延迟。 ✅什么是单元化架构? 8、压测 压测是一种非常常见的帮我们做性能评估的手段。通过模拟用户请求,帮助我们发现系统的瓶颈以及评估系统的整体水位。 ✅什么是压测,怎么做压测? 9、限流&降级&熔断 这三个东西,一般是用来保障可用性的,但是其实性能也是可用性的一个重要指标,有的时候下游接口很慢,或者垮了,我们如果不做手段的话,会把我们的性能也拖垮的。所以需要考虑这些方案。 ✅限流、降级、熔断有什么区别? 10、架构方案 还有很多架构手段,比如分库分表、读写分离、多级缓存、并发消息等等,都是可以提升性能的重要手段。 ✅什么是读写分离?如何实现? 11、网络优化 有的时候,存在一些网络延迟是因为有跨机房,跨地区调用,或者是网络环境不好,这些都是可以优化的,比如做同单元、同机房调用,拉专线等等。 12、扩容 终极方案,扩容,能力不够,机器来凑。 扩展知识 交易主链路提供风控决策要求RT 5ms的技术方案

March 22, 2026 · 1 min · santu

服务端接口性能优化有哪些方案?

作为一个Java后端开发,我们写出的大部分代码都决定着用户的使用体验。如果我们的后端代码性能不好,那么用户在访问我们的网站时就要浪费一些时间等待服务器的响应。这就可能导致用户投诉甚至用户的流失。 关于性能优化是一个很大的话题。《Java程序性能优化》说性能优化包含五个层次:设计调优、代码调优、JVM调优、数据库调优、操作系统调优等。而每一个层次又包含很多方法论和最佳实践。本文不想大而广的概述这些内容。只是举几个常用的Java代码优化方案,读者看完之后可以真正的实践到自己代码中的方案。 使用单例 对于IO处理、数据库连接、配置文件解析加载等一些非常耗费系统资源的操作,我们必须对这些实例的创建进行限制,或者是始终使用一个公用的实例,以节约系统开销,这种情况下就需要用到单例模式。 批量操作 有100个请求,每个请求单独执行那肯定很慢,如果有办法把这个100个请求合并成一个请求,进行批量操作,那么效率就会高很多。 尤其是在数据库操作的时候,批量操作不仅比单条执行效率高,而且还能有效的降低数据库连接数,提升应用的QPS上限。 使用Future模式 假设一个任务执行起来需要花费一些时间,为了省去不必要的等待时间,可以先获取一个“提货单”,即Future,然后继续处理别的任务,直到“货物”到达,即任务执行完得到结果,此时便可以用“提货单”进行提货,即通过Future对象得到返回值。 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 public class RealData implements Callable<String> { protected String data; public RealData(String data) { this.data = data; } @Override public String call() throws Exception { //利用sleep方法来表示真是业务是非常缓慢的 try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } return data; } } public class Application { public static void main(String[] args) throws Exception { FutureTask<String> futureTask = new FutureTask<String>(new RealData("name")); ExecutorService executor = Executors.newFixedThreadPool(1); //使用线程池 //执行FutureTask,相当于上例中的client.request("name")发送请求 executor.submit(futureTask); //这里可以用一个sleep代替对其他业务逻辑的处理 //在处理这些业务逻辑过程中,RealData也正在创建,从而充分了利用等待时间 Thread.sleep(2000); //使用真实数据 //如果call()没有执行完成依然会等待 System.out.println("数据=" + futureTask.get()); } } 使用线程池 合理利用线程池能够带来三个好处。第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。 ...

March 22, 2026 · 2 min · santu

读写分离遇到主从延迟怎么办?

典型回答 ✅什么是读写分离?如何实现? 上文我们介绍过了读写分离。其实基于MySQL的主从复制实现读写分离的方案,最怕遇到的就是主从延迟。 ✅什么是数据库的主从延迟,如何解决? 因为写是发生在主库上的,而读是基于从库的,一旦主从之间数据复制出现延迟了,就会出现刚写入的数据读不到的问题。 正常情况下,MySQL的主从延迟都是非常小的,一般都不超过 1ms。但是在极端情况下也会出现查不到的情况。所以,我们需要想办法解决这个问题。 一般来说,为了减少主从延迟带来的影响,我们在实现读写分离时,可以采用以下几种方案做优化。 读请求分类 一般来说,虽然我们做了读写分离,但是也不是无脑分的,我们还会把读请求分成两类,一类是可以接受延迟的读,一类是不能接受延迟的读。 比如历史订单的查询、比如数据报表的生成、比如数据对账的查询、比如非关键业务的查询,比如评论信息等,这些都是可以延迟的读,这些读的话就可以完全从备库走。 而对于那些不能接受延迟的读,那么就需要注意了,就需要考虑进行强制读主库。 这种方案其实是用的比较多的,不要以为他是逃避了问题,有的时候,没必要给自己创造困难硬上! 强制读主库 上面我们提到了对于一些不能接受延迟的读请求,需要强制走主库。 还有一些情况,那就是一些核心的业务操作,或者是在一个事务上下文中的读请求,这时候也需要读主库的。 比如说我在创建订单的过程中,我会先插入一个订单,然后再查询订单信息进行后续操作,这个过程中,是要保证数据一定能查到的,这时候就也需要强制走主库。 具体如何实现强制读主库呢,如果是我们前面介绍的通过自己写代码分流的方案的话,就比较容易了,我们可以自己控制读写哪个数据源,那么就自己硬编码就好了。 如果是使用我推荐的中间件的方案的话,比如ShardingJDBC,他也是支持强制路由的(https://shardingsphere.apache.org/document/legacy/3.x/document/cn/manual/sharding-jdbc/usage/hint/ ),可以通过设置hint的方式让SQL只操作主库。 二次读取 除了上面我们说的强制读主库的方案,还有一个常见做法叫做二次读取。 啥意思呢,就是我的读取操作,默认读从库,但是如果我从库读取的时候没读到,那我为了避免因为数据延迟导致的,那么就再进行一次从主库读取。 这个实现方式的话也是需要我们定制的开发代码。但是这个方案我不太建议,因为这种一旦出现延迟,也会导致你的主库会有大量的请求过去,造成很大的压力的。 主备一致 除了上面说的方案之外,还有一些场景中,是采用了一些特殊的手段,来确保主备一致。 比如在极客时间的《MySQL 45讲》中,作者提到过一些方案(但是其实用的都不多,还是前面说的几个方案更多一点): Sleep方案:就是主库更新之后,读从库之前先sleep 1秒,然后再读从库。 判断主备无延迟方案:每次从库执行查询请求前,先判断 seconds_behind_master 是否已经等于 0。如果还不等于 0 ,那就必须等到这个参数变为 0 才能执行查询请求。 **等主库位点方案:**其核心思想是在从库上执行读操作前,确保从库已经同步了特定的主库位点(即主库的数据变更位置)。这样可以保证读操作获取的数据是最新的,避免了因主从复制延迟而导致的数据不一致问题。 **等 GTID 方案:**和位点原理一样,MySQL 5.7.6 版本开始,允许在执行完更新类事务后,把这个事务的 GTID 返回给客户端,在执行读操作前,应用检查从库是否已经应用了该GTID标识的事务。这通常涉及查询从库的复制状态,确认已经处理的GTID集合包含了特定的GTID。 GTID(Global Transaction Identifier)为每个事务提供了一个全局唯一的标识符,使得主从复制过程中的数据变更能够更加精确和容易追踪。

March 22, 2026 · 1 min · santu

什么是布谷鸟过滤器,实现原理是什么?

典型回答 **布谷鸟过滤器(Cuckoo Filter)**是一种更优秀的数据结构,它在设计上就支持删除操作。它的核心思想是 “每个项都有两个(或多个)‘桶’,并且它会携带一个唯一的‘指纹’” 这个名字源于 “布谷鸟筑巢” 的行为。布谷鸟不会自己筑巢,而是会把蛋下到其他鸟的巢里,并把原来的蛋踢出去。布谷鸟过滤器中的插入过程与此非常相似:如果一个新元素发现它的两个“家”都满了,它就会随机把其中一个“老住户”(已有的指纹)踢出去,然后为自己安家。那个被踢出去的“老住户”就得去寻找它的另一个“家”。 Redis 8.0中新增的数据结构中,有一个就是本文的主角——Cockoo Filter :https://redis.io/docs/latest/develop/data-types/probabilistic/cuckoo-filter/ 指纹 最开始看这个布谷鸟过滤器的时候,有点懵b,因为他引入了指纹和两个桶的概念。但是,这里可以先不考虑2个桶的事儿,只考虑指纹。 我们知道布隆过滤器之所以不能删除元素,是因为在某个位置上如果被设置为1,并不代表着只有当前元素的hash结果在这里,有可能hash冲突,其他元素也在这里,所以你不能直接设置为0。 但是,如果这个位置,只存一个元素呢?这样删除的时候我就可以检查,如果是和要删除的元素一样,就可以删除。 但是这样做就和Set、数据啥的都一样了,所以,布谷鸟过滤器提出了不要存数据的原值,而是存一个短小的值,也就是上面提到的指纹了。虽然指纹也会有冲突的可能,但是指纹又冲突了,最终存的桶有一样的概率就低很多。 指纹(Fingerprint):使用一个短小的、固定位数的哈希值(比如8位)来唯一代表一个元素。虽然不同元素的指纹可能冲突,但概率很低。 所以,指纹有2个作用: 1、指纹是存储在布谷鸟过滤器桶中的实际数据。过滤器中不存储庞大的原始数据,只存储这个短小的、固定长度(如8位)的哈希值。这极大地节省了空间。而且在查询和删除时,我们通过比对指纹来判断一个元素是否存在。 2、指纹的长度直接决定了过滤器的精度。指纹越长,不同元素产生相同指纹(冲突)的概率就越低。 那如果真的很不幸指纹冲突了,那么也可以借助接下来要介绍的2个桶来降低冲突的可能。 多个候选桶 在布谷鸟过滤器中,每个元素不是映射到位数组的一系列位上,而是根据其指纹被计算到两个确定的候选桶中(也可以扩展成多个)。当一个新元素要插入时,它有两个“桶”可以选择。如果第一个桶满了,它可以去第二个桶。这大大提高了过滤器的空间利用率和插入成功率。还避免了像布隆过滤器那样,所有元素都共享同一个大位数组,导致位冲突概率随元素增加而急剧上升的问题。 先看插入、查找和删除的过程。 执行过程 插入过程 对元素 x,首先计算指纹: f = fingerprint(x)然后 计算第一个桶索引:i1 = hash(x) % N,i1和指纹无关,和要存储的元素的hash有关 计算第二个桶索引:i2 = i1 XOR hash(f)(异或运算),i2和指纹有关,也和要存储的元素的hash有关 这里使用 **XOR**(异或)操作是关键。它确保了第二个桶的位置也依赖于指纹 **f**。这意味着,只要你知道 **f** 和 **i1**,你一定能计算出 **i2**;反之亦然。这个特性对查找和删除至关重要。 接下来开始插入, 如果桶 i1 或桶 i2 中还有空槽位,直接将**指纹 **f** **放入其中一个空位。插入成功! 如果两个桶都满了,随机选择其中一个桶(比如 i1),“踢出” 该桶中的一个随机指纹 f_old。然后将新的指纹 f 放入桶 i1 中。 被踢出去的元素先尝试向他的另一个桶中插入(如i2),如果能插入则直接插入,如果无法插入,就和前面一样,踢出其他元素。 ...

March 22, 2026 · 1 min · santu

布隆过滤器无法删除的问题如何解决?

典型回答 布隆过滤器是无法删除元素的,因为布隆过滤器无法准确的判断一个元素是否一定存在,所以他也就无法准确的删除这个元素。 其实就是一个元素,会通过多个hash函数哈希后存储在不同的bit上,而一个bit上存的的如果是1的话,只能说明有元素哈希后的结果在这里,但是具体有几个是不知道的。而我们想要删除这个元素的话,我们即使算出他的bit有哪几个,也不知道是不是应该把他从1设置为0,因为完全这个bit有可能是存在hash冲突,有多个元素的。 那么如何解决这个问题呢? 定期重建 其实,这个问题无法解决,只能换个思路,大家试想一下,无法删除的话会有什么问题呢? 无法删除就会导致某个元素已经不存在了,但是查询结果还是可以查到。那如果在布隆过滤器中查到了,我们为了避免误判,还是会去数据库查一次。 也就是说,虽然我们无法删除元素,也不会影响最终的结果,因为判断命中之后还会做二次校验。只不过随着元素删除的越来越多,会导致误判率的上升,进而导致数据库的请求量变大罢了。 那么,一般业内的解决办法是定期重建,就是每隔一段时间,基于最新的数据重新构建一个布隆过滤器,然后把他切换一下即可。 比如基于Guava的布隆过滤器,我们就可以在应用启动的时候构建,这样每次重启就是一个新的了。或者通过定时任务去构建一个新的。至于构建出新的之后,如何切换呢?很简单,我们自己维护一个map,只需要创建好新的之后,把他放到map里,key还有之前的key就可以了,。下次用的时候再来map查询,就得到一个新的了。 计数布隆过滤器 既然布隆过滤器因为每一个bit上存储为1的时候,不知道有多少个元素hash后存在了这里,所以不能删除,那么可以考虑采用计数布隆过滤器。即将布隆过滤器的位数组中的每一个位(bit),扩展为一个计数器(counter)。通常使用 3-4 位的计数器(可表示 0-15)****。 添加元素:不再是将位设置为 1,而是将对应位置的计数器加 1。 查询元素:检查所有对应位置的计数器是否都大于 0。 删除元素:将要删除元素对应位置的计数器减 1。 有点就是可以解决无法删除问题,实现起来也挺简单的。但是缺点是原本 1 个 bit 的位置,现在需要 3-4 bits(开销是原来的 300%-400%),带来了额外的开销(但相比用其他数据结构,比如 Set 存储所有元素,它仍然非常节省空间)。还有就是如果计数器只有 4 位,最大只能表示 15。如果同一个位置被添加超过 15 次,计数器会绕回(溢出),所以需要权衡占用空间和溢出的问题。 布谷鸟过滤器 ✅什么是布谷鸟过滤器,实现原理是什么?

March 22, 2026 · 1 min · santu

留言给博主