订单到期关闭如何实现

典型回答 在电商、支付等系统中,一般都是先创建订单(支付单),再给用户一定的时间进行支付,如果没有按时支付的话,就需要把之前的订单(支付单)取消掉。这种类似的场景有很多,还有比如到期自动收货、超时自动退款、下单后自动发送短信等等都是类似的业务问题。 订单的到期关闭的实现有很多种方式,分别有: 1、被动关闭(不推荐) 2、定时任务(推荐,适合时间精确度要求不高的场景) 3、DelayQueue(不推荐,基于内存,无法持久化) 4、时间轮(不推荐,基于内存,无法持久化) 5、kafka(MQ 方案不推荐,大量无效调度) 6、RocketMQ延迟消息(MQ 方案不推荐,大量无效调度) 7、RabbitMQ死信队列(MQ 方案不推荐,大量无效调度) 8、RabbitMQ插件(MQ 方案不推荐,大量无效调度) 9、Redis过期监听(不推荐,容易丢消息) 10、Redis的ZSet(不推荐,可能会重复消费) 11、Redisson(推荐,可以用) 实现的复杂度上(包含用到的框架的依赖及部署): Redisson > RabbitMQ插件 > RabbitMQ死信队列 > RocketMQ延迟消息 ≈ Redis的zset > Redis过期监听 ≈ kafka时间轮 > 定时任务 > Netty的时间轮 > JDK自带的DelayQueue > 被动关闭 不同的场景中也适合不同的方案: 自己玩玩:被动关闭 单体应用,业务量不大:Netty的时间轮、JDK自带的DelayQueue、定时任务 分布式应用,业务量不大:Redis过期监听、RabbitMQ死信队列、Redis的zset、定时任务 分布式应用,业务量大、并发高:Redisson、RabbitMQ插件、kafka时间轮、RocketMQ延迟消息、定时任务 业务量特别大:被动关闭+定时任务(结合分片任务+多线程+生产者消费者+时间轮等方案) 总体考虑的话,考虑到成本,方案完整性、以及方案的复杂度,还有用到的第三方框架的流行度来说,个人比较建议优先考虑定时任务、Redisson+Redis、RabbitMQ插件、RocketMQ延迟消息等方案。 但是,如果考虑到订单到期关闭的业务特点,如果在订单量特别大的时候,MQ其实并不适合: ✅为什么不建议使用MQ实现订单到期关闭? 扩展知识 一、被动关闭 在解决这类问题的时候,有一种比较简单的方式,那就是通过业务上的被动方式来进行关单操作。 简单点说,就是订单创建好了之后。我们系统上不做主动关单,什么时候用户来访问这个订单了,再去判断时间是不是超过了过期时间,如果过了时间那就进行关单操作,然后再提示用户。 这种做法是最简单的,基本不需要开发定时关闭的功能,但是他的缺点也很明显,那就是如果用户一直不来查看这个订单,那么就会有很多脏数据冗余在数据库中一直无法被关单。 还有一个缺点,那就是需要在用户的查询过程中进行写的操作,一般写操作都会比读操作耗时更长,而且有失败的可能,一旦关单失败了,就会导致系统处理起来比较复杂。 所以,这种方案只适合于自己学习的时候用,任何商业网站中都不建议使用这种方案来实现订单关闭的功能。 二、定时任务 定时任务关闭订单,这是很容易想到的一种方案。 具体实现细节就是我们通过一些调度平台来实现定时执行任务,任务就是去扫描所有到期的订单,然后执行关单动作。 这个方案的优点也是比较简单,实现起来很容易,基于Timer、ScheduledThreadPoolExecutor、或者像xxl-job这类调度框架都能实现,但是有以下几个问题: 1、时间不精准。 一般定时任务基于固定的频率、按照时间定时执行的,那么就可能会发生很多订单已经到了超时时间,但是定时任务的调度时间还没到,那么就会导致这些订单的实际关闭时间要比应该关闭的时间晚一些。 2、无法处理大订单量。 定时任务的方式是会把本来比较分散的关闭时间集中到任务调度的那一段时间,如果订单量比较大的话,那么就可能导致任务执行时间很长,整个任务的时间越长,订单被扫描到时间可能就很晚,那么就会导致关闭时间更晚。 3、对数据库造成压力。 定时任务集中扫表,这会使得数据库IO在短时间内被大量占用和消耗,如果没有做好隔离,并且业务量比较大的话,就可能会影响到线上的正常业务。 4、分库分表问题。 订单系统,一旦订单量大就可能会考虑分库分表,在分库分表中进行全表扫描,这是一个极不推荐的方案。 这些问题的解决方案如下: ✅定时任务扫表的方案有什么缺点? ...

March 22, 2026 · 1 min · santu

不用redis分布式锁, 如何防止用户重复点击?

典型回答 当不让使用redis分布式锁,或者集群不可用的时候,如何做到防止用户重复点击的功能呢? 有以下几个思路可以供大家参考: 1、首先就是前端需要做一些按钮置灰的动作,让用户点击一次之后,按钮就直接禁用调,让用户无法重复点击。但是有些情况可能没来得及置灰就重复点击了,或者有些用户自己绕过了置灰也可以点击。 2、可以通过token的机制避免重复提交,当用户访问页面的时候,请求后端服务拿到一个token,然后下一次接口点击的时候把token带过来,服务端对token进行验证,验证该token是否被使用过,如果没有被使用过才可以进行点击。验证的逻辑可以放在数据库中,通过数据库的悲观锁或者乐观锁都可以实现。 ✅Cookie,Session,Token的区别是什么? 比如,以下就是我自己的高并发实战项目中,通过token来进行订单防重复的一个具体的交互图: 3、滑动窗口限流,滑动窗口限流是一种流量控制策略,用于控制在一定时间内允许执行的操作数量或请求频率。我们可以限制一分钟或者一秒钟内用户只能发起一次请求来防止重复点击。 ✅什么是滑动窗口限流? 4、可以使用布隆过滤器,他可以快速判断某个元素是否存在于集合中。可以在服务器端使用布隆过滤器记录某个操作是否已经被执行过,从而防止重复执行。 如果布隆过滤器不存在,则一定不存在,所以,如果没查到,说明一定没有幂等操作,直接执行就行了。 如果查询布隆过滤器发现有命中,则需要在服务数据库做一次幂等判断。 大多数情况下,需要幂等的情况占比小,所以可以用布隆过滤器做一次fail-fast的快速校验。 ✅什么是布隆过滤器,实现原理是什么? Redis其实也是一个集中式的存储服务,在特殊情况下,如果无法使用,一般的做法都是降级成直接使用数据库。 5、还有种方式,那就是参考 ruoyi 框架中的防重复提交的实现方案,其实就是把表单信息做校验并保存在 REDIS 中,下次再提交的时候做校验,如果和上次提交的内容一样,并且时间小于一定的时间间隔,则拒绝请求,

March 22, 2026 · 1 min · santu

如何设计一个购物车功能?

典型回答 购物车系统的主要功能,是在用户选购商品之后,下单之前,先把用户的意向商品、数量等信息保存下来,方便用户进行统一的支付。 对于购物车的操作主要就是加入购物车、查看购物车以及通过购物车下单等 。 一般,我们在存储的时候,并不需要把商品的所有信息都保存下来,只需要保存一个SKUID就行了,然后再加上数量、时间等字段即可。 至于商品的库存、价格、介绍等信息,只需要在渲染购物车的时候实时反查和计算就行了。 未登录用户购物车 对于电商平台来说,用户一般有登录和未登录两种状态,一般购物车功能需要同时支持已登录用户的加购和未登录用户的加购。 而已登录和未登录的用户的购物车数据的存储其实是不同的。 对于未登录的用户,其实他的购物车的信息没必要存储在后端,只需要在客户端做临时缓存就行了。客户端存储可以选择Cookie 和 LocalStorage等技术。 在存储时,只需要设计一个JSON格式就可以了,因为用户没登录,所以也就不需要标识数据属于谁,那么只需要如下存储即可: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 { "cart": [ { "SKUID": 10086, "timestamp": 1666513990, "count": 2 }, { "SKUID": 10010, "timestamp": 1666513990, "count": 10 } ] } 已登录用户的购物车 对于已经登录的用户的购物车,我们就不能存储在客户端了,因为客户端的数据可能会超时、一旦换了设备也就没有了。我们需要用持久化存储,那么就可以使用数据库和Redis缓存。 ...

March 22, 2026 · 1 min · santu

如果让你实现消息队列,会考虑哪些问题?

典型回答 这是一个比较常见的场景题,让实现某个中间件,其实主要就是拆解,基于自己对已有的消息中间件,如Kafka、RocketMQ等的理解,进行回答这个问题。 主要可以从以下几个方面来回答这个问题: 基本架构和功能 当设计一个消息队列的时候,需要考虑它的基本架构及功能,这是首先需要考虑的。 生产者、消费者、Broker:生产者负责发送消息,消费者负责接收消息,Broker作为服务端,处理消息的存储、备份、删除和消费关系维护。 主题和分区:主题(Topic)是消息分类的标识,而分区是主题的物理分割,有助于提高消息队列的吞吐量。 比如我们知道的Kafka、RocketMQ、RabbitMQ等等,都有各自的架构设计方案 ✅Kafka的架构是怎么样的? ✅RocketMQ的架构是怎么样的? ✅rabbitMQ的整体架构是怎么样的? 基本功能 消息存储方式:消息队列需要将消息存储在某种媒介中,一般采用内存或者磁盘存储。在内存存储的情况下,可以快速的读写消息,但是可能会丢失消息,因为内存中的消息没有持久化。而采用磁盘存储,可以持久化消息,但是读写速度相对慢一些。 消息传递协议:消息队列需要定义消息传递的协议,包括消息格式、消息队列的地址等信息**。我们可以使用成熟的RPC框架(如Dubbo或Thrift)实现生产者和消费者与Broker之间的通信。** 消息的持久化和确认机制:在消息队列中,需要实现消息的持久化和确认机制,确保消息不会丢失或重复消费。一般的做法是将消息存储在磁盘中,并在消费者确认消费完成后再删除消息。 消息的分发方式:消息队列需要实现消息的分发方式,包括点对点和广播两种方式。在点对点方式下,每个消费者只会接收到自己订阅的消息;在广播方式下,每个消费者都会接收到所有的消息。 ✅RocketMQ怎么实现消息分发的? 消息的传递方式:在消息队列中,有多种消息的传递方式,如轮询、长连接,还有长轮询。一般都是支持推拉结合的方式。或者基于拉实现推的机制。 ✅消息队列使用拉模式好还是推模式好?为什么? 消息的可靠性保证 消息队列的容错性和可用性:消息队列需要实现高可用和容错机制,以确保消息的可靠传输。一般的做法是采用主从复制、集群模式或者分布式架构来实现。 ✅Kafka如何保证消息不丢失? ✅RocketMQ如何保证消息不丢失? ✅RabbitMQ如何保证消息不丢 高性能设计 高性能这部分可以参考kafka,引入一些批量操作、顺序写入、零拷贝等技术。 ✅Kafka 为什么这么快? 功能扩展 除了一些基本的消息发送、投递以外,还需要考虑一些具体的业务场景。比如实现事务消息、实现延迟消息、实现顺序消息等等。 顺序消息 ✅Kafka如何实现顺序消费? ✅RocketMQ如何保证消息的顺序性? 延迟消息 ✅rabbitMQ如何实现延迟消息? ✅RocketMQ如何实现延时消息? 事务消息 ✅RocketMQ的事务消息是如何实现的? 还需要考虑一些MQ使用时候可能会出现的消息堆积、消息重复消费等问题。 重复消费 ✅Kafka怎么保证消费只消费一次的? ✅RabbitMQ如何防止重复消费 消息堆积 ✅RocketMQ消息堆积了怎么解决? 还有一些其他的问题,比如重平衡的问题,集群数据同步的问题等。

March 22, 2026 · 1 min · santu

Redis的zset实现排行榜,实现分数相同按照时间顺序排序,怎么做?

典型回答 在Redis中,使用zset可以实现排行榜的功能这个大家都知道。 zset可以实现,将每个用户的得分作为zset中元素的score,将用户ID作为元素的value。使用zset提供的排序功能,可以按照分数从高到低排序,但是如果分数相同,按照默认的排序规则会按照value值排序,而不是按照时间顺序排序。 为了实现分数相同按照时间顺序排序,我们可以将分数score设置为一个浮点数,其中整数部分为得分,小数部分为时间戳,如下所示: score = 分数 + 时间戳/1e13 假设现在的时间戳是1680417299000,除以1e13得到0.1680417299000,再加上一个固定的分数(比如10),那么最终的分数就是10.1680417299000,可以将它作为zset中某个成员的分数,用来排序。 这么做了之后,假如有四个数字: 10.1680417299000、10.1680417299011、11.1680417299000、11.1680417299011 他们按照倒序拍完顺序之后,会是: 11.1680417299011>11.1680417299000>10.1680417299011>10.1680417299000 实现了分数倒序排列,分数相同时间戳大的排在了前面,这和我们的需求相反了,所以,就需要在做一次转换。 score = 分数 + 1-时间戳/1e13 因为时间戳是这种形式1708746590000 ,共有13位,而1e13是10000000000000,即1后面13个0,所以用时间戳/1e13就能得到一个小数 这样可以保证分数相同时,按照时间戳从小到大排序,即先得分的先被排在前面。 代码实现如下: 1 2 3 4 5 6 7 8 9 10 11 12 import redis.clients.jedis.Jedis; /** *@author Hollis **/ public class RedisZsetDemo { private static final String ZSET_KEY = "my_zset"; public static void addMember(String member,int score, long timestamp, Jedis jedis) { double final_score = score + 1 - timestamp / 1e13; jedis.zadd(ZSET_KEY, final_score, member); } }

March 22, 2026 · 1 min · santu

如何实现_查找附近的人_功能?

典型回答 实现"查找附近的人"功能,可以利用Redis的Geospatial数据类型,结合用户经纬度信息进行存储和查询。 ✅什么是GEO,有什么用? 使用Redis的GEOADD命令将用户经纬度信息存储在一个指定的键值中,然后再使用Redis的GEORADIUS命令可以查询指定经纬度附近一定范围内的用户信息就能简单实现这个功能了。 1 2 3 GEOADD user_location 121.57465 25.04100 user1 GEORADIUS user_location 121.57465 25.04100 1000 km 以上就是查询中国台湾附近1000km的人的两行命令。 代码实现: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import redis.clients.jedis.GeoRadiusResponse; import redis.clients.jedis.Jedis; import java.util.List; import java.util.stream.Collectors; public class RedisGeoDemo { private static final String USER_LOCATION_KEY = "user_location"; // 存储用户经纬度信息 public static void saveUserLocation(String userId, double longitude, double latitude, Jedis jedis) { jedis.geoadd(USER_LOCATION_KEY, longitude, latitude, userId); } // 查询附近的人 public static List<String> getNearbyUsers(double longitude, double latitude, double radius, Jedis jedis) { List<GeoRadiusResponse> responses = jedis.georadius(USER_LOCATION_KEY, longitude, latitude, radius, "km"); return responses.stream().map(GeoRadiusResponse::getMemberByString).collect(Collectors.toList()); } }

March 22, 2026 · 1 min · santu

Kafka,单分区单消费者实例,如何提高吞吐量

典型回答 在Kafka中,想要提升吞吐量,有效的手段就是增加消费者实例以及增加更多的partition,但是如果不能增加消费者实例,也不能新增更多的partition,那么如何通过技术手段提高吞吐量呢? 主要有以下几个方面可以做: 1、异步消费 2、增加消费的线程数 4、消息压缩或组合 4、调整Kafka参数 异步消费 想要提升消费者的消费速度,我们可以快速的处理消息,有一个办法就是在接收到消息之后,先本地落库,然后落库成功之后就直接提交偏移量。 然后在用异步的方式消费这些任务。为了提升速度,我们可以在消费消息的线程中,本地消息保存成功之后,直接起一个异步线程来处理这个消息,如果成功,则直接把消息删除掉,如果失败,那么依赖本地消息重试。 那有人就会问了,这样做,还有消息中间件有意义吗? 当然有了,消息中间件的目的有很多个,比如解耦、异步、削峰填谷。我们在消费者端,接到消息后本地存下来再执行,不影响我们依然做到了解耦、异步和削峰填谷。 多线程消费 如果不想用本地落库的方式,那么也可以直接用多线程来执行。 我们可以通过配置,让kafka一次性多拉取一些消息,在多个消息都拉取到之后,通过异步线程池的方式来并发执行。 这种方案可以借助多线程来并发消费一批消息,从而提升并发度,提高吞吐量。但是这个方案存在一个比较大的问题,那就是异步线程多个消息的处理时间不同,可能会导致偏移量的提交并不能按照顺序,那么这个过程可能就会存在消息的丢失和重复消费的情况。 那么,有什么办法可以解决这个问题呢? 第一种方案,我们可以把多个线程编排起来执行,当所有的线程都消费成功之后,把这其中最大的偏移量提交了就行了。比如我们可以使用CompletableFuture轻松的实现这个功能,CompletableFuture即自带线程池,又支持任务分片,非常适合这种场景,如: 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 36 37 import org.apache.kafka.clients.consumer.ConsumerRecord; import org.apache.kafka.clients.consumer.KafkaConsumer; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CompletableFuture; public class KafkaConsumerExample { private KafkaConsumer<String, String> consumer; private List<ConsumerRecord<String, String>> records = new ArrayList<>(); public void consume(int batchSize) { // 从 Kafka 中拉取 batchSize 条消息 records = consumer.poll(batchSize); // 使用 CompletableFuture 并发执行消息处理 CompletableFuture<Void> allFutures = CompletableFuture.allOf(records.stream() .map(detail -> CompletableFuture.supplyAsync(() -> { // 处理消息逻辑 // ... //如果消费失败,直接throw Exception return null; })).toArray(CompletableFuture[]::new)); // 等待所有任务执行完毕后,提交最大的偏移量 allFutures.whenComplete((v,e) -> { if(e == null){ //未发生异常,则提交最大的偏移量 long maxOffset = records.stream().mapToLong(ConsumerRecord::offset).max().orElse(-1L); consumer.commitSync(Collections.singletonMap(consumer.assignment().iterator().next(), new OffsetAndMetadata(maxOffset + 1))); }else{ //有消息消费失败,执行失败处理逻辑 } }); } } 这样可能存在一个问题,就是如果其中某个消息执行失败了,就会导致没办法正确提交偏移量,导致很多消息会重复消费。很多时候如果我们做好了幂等,重复消费的问题理论上可以忽略,但是如果要解决的话,我们也有一些办法。 ...

March 22, 2026 · 2 min · santu

如果让你实现一个RPC框架,会考虑用哪些技术解决哪些问题?

典型回答 这种问题,其实考察的就是你对Dubbo的理解程度了,一般可以从他的相关原理以及细节实现上面来回答,比如Dubbo作为一个RPC框架,需要考虑的问题有以下这几个: 1、RPC调用,需要通信吧,那就需要一个通信协议。 2、通信的过程中,需要做参数的序列化吧,那就需要一个序列化协议。 3、RPC框架中需要有注册中心吧,服务提供者和调用者需要和注册中心交互吧,这就需要解决服务的注册和发现的问题吧。 4、有了多个服务提供者之后,调用者在调用的时候,需要选择一个具体的服务调用吧,这时候是不是又需要负载均衡了。 5、RPC重要的是像调用本地服务一样调用远程服务,那么这就涉及到动态代理的技术实现了吧 6、除了以上这些重要的核心功能以外,还可以考虑,比如缓存、比如服务降级、比如泛化调用、比如优雅上下线、比如服务的高可用、比如异步回调、比如服务治理之类的了。 所以,把一个问题拆解出来之后,你会发现剩下这些问题可能都是你见过的——“八股文”! 如通信协议: ✅Dubbo支持哪些调用协议? 如序列化协议的话,可以参考: ✅Netty有哪些序列化协议? 如服务的注册和发现: ✅Dubbo实现服务调用的过程是什么样的? 如负载均衡: ✅什么是负载均衡,有哪些常见算法? 如动态代理: ✅Dubbo如何实现像本地方法一样调用远程方法的? 还有缓存、泛化调用、优雅上下线,在八股文中也都有,就不一一展示了,大家可以自行搜索一下。

March 22, 2026 · 1 min · santu

消息队列使用拉模式好还是推模式好?为什么?

典型回答 推和拉是两种消息传递的方式 推的模式就消费者端和消息中间件建立TCP长链接或者注册一个回调,当服务端数据发生变化,立即通过这个已经建立好的长连接(或者注册好的回调)将数据推送到客户端。 拉的模式就是消费者轮询,通过不断轮询的方式检查数据是否发生变化,变化的话就把数据拉回来。 如果使用拉的模式来实现消息队列的话,消费者可以完全自己掌控消息的数量及速度,这样可以大大的避免消息堆积的情况。但是,这种方案也有缺点,首先就是消费者需要不断的进行轮询,这种轮询也会对消息中间件造成一定的压力。 如果使用推的模式来实现,好处就是消息是实时的, 一旦有消息过来消费者马上就能感知到。而且对于消费者来说也比较简单,不需要轮询,只需要等推送就行了。但是缺点也比较明显,那就是如果消息的生产速度大于消费速度,可能会导致消息大量堆积在消费者端,会对消费者造成很大的压力,甚至可能把消费者压垮。 一般来说,推的模式适合实时性要求比较高的场景。而拉的模式适合实时性要求没那么高的场景。 还有需要注意的就是,在有些生产环境下,服务器环境只能单向通信,也就是只能通过一端访问另外一端,而不能在反方向通信,此时就需要消费方,使用拉模式,推模式是长链接,是双向通信,所以不行。 在很多中间件的实现上,可能并没有在直接用长连接或者轮询,而是把二者结合了一下,使用长轮询的方式进行拉消息的。 长轮询,就是消费者向消息中间件发起一个长轮询请求,消息中间件如果有消息就直接返回,如果没有也不会立即断开连接,而是等待一会,等待过程中如果有新消息到达,再把消息返回。如果超时还没有消息,那就结束了,等下次长轮询。 比如Kafka和RocketMQ都是支持基于长轮询进行拉取消息的。

March 22, 2026 · 1 min · santu

一个支付单,多个渠道同时支付成功了怎么办?

典型回答 在电商场景中,创建了一个支付单之后,会通过支付工具进行支付,比如微信支付、支付宝、银行卡等,那么在特殊场景中,会出现,用户先通过微信支付尝试支付,因为网络延迟导致一直未支付成功,后面用户又再用支付宝支付成功了,可是过了一会,微信支付那面又回调通知支付成功了,这种该怎么处理呢? 其实,这个问题和之前我们讨论的另一个问题的解决思路差不多,但是也有一定的差别。 ✅一个订单,在11:00超时关闭,但在11:00也支付成功了,怎么办? 要想解决这个问题,首先需要有明确的状态机和支付单状态的流转控制,这个不详细展开了,可以参考上面这个文章。 这个场景存在一个特殊的地方,那就是已经支付成功的单据,下一次支付成功的回调过来的时候,如何做幂等控制。 好的做法是,在支付单中冗余一个支付渠道和渠道支付单号,在支付成功的时候,把支付渠道返回的渠道支付单号记录下来。 在接收到支付成功回调的时候,先检查状态,如果发现已经支付成功,则比对支付渠道和渠道单号是否一致,如果一致,则幂等成功。如果不一致,说明发生了重复支付,则执行退款流程。 冲退的流程及异常情况处理,在上面的链接中也展开讲过,这里不再赘述了。

March 22, 2026 · 1 min · santu

留言给博主