接口幂等性问题，对于开发人员来说，是一个跟语言无关的公共问题。本文分享了一些解决这类问题非常实用的办法，绝大部分内容我在项目中实践过的，给有需要的小伙伴一个参考。不知道你有没有遇到过这些场景：有时我们在填写某些form表单时，保存按钮不小心快速点了两次，表中竟然产生了两条重复的数据，只是id不一样。我们在项目中为了解决接口超时问题，通常会引入了重试机制。第一次请求接口超时了，请求方没能及时获取返回结果（此时有可能已经成功了），为了避免返回错误的结果（这种情况不可能直接返回失败吧？），于是会对该请求重试几次，这样也会产生重复的数据。mq消费者在读取消息时，有时候会读取到重复消息（至于什么原因这里先不说，有兴趣的小伙伴，可以找我私聊），如果处理不好，也会产生重复的数据。没错，这些都是幂等性问题。接口幂等性是指用户对于同一操作发起的一次请求或者多次请求的结果是一致的，不会因为多次点击而产生了副作用。这类问题多发于接口的：insert操作，这种情况下多次请求，可能会产生重复数据。update操作，如果只是单纯的更新数据，比如：update user set status=1 where id=1，是没有问题的。如果还有计算，比如：update user set status=status+1 where id=1，这种情况下多次请求，可能会导致数据错误。那么我们要如何保证接口幂等性？本文将会告诉你答案。1. insert前先select通常情况下，在保存数据的接口中，我们为了防止产生重复数据，一般会在insert前，先根据name或code字段select一下数据。如果该数据已存在，则执行update操作，如果不存在，才执行 insert操作。该方案可能是我们平时在防止产生重复数据时，使用最多的方案。但是该方案不适用于并发场景，在并发场景中，要配合其他方案一起使用，否则同样会产生重复数据。我在这里提一下，是为了避免大家踩坑。2. 加悲观锁在支付场景中，用户A的账号余额有150元，想转出100元，正常情况下用户A的余额只剩50元。一般情况下，sql是这样的：update user amount = amount-100 where id=123;如果出现多次相同的请求，可能会导致用户A的余额变成负数。这种情况，用户A来可能要哭了。于此同时，系统开发人员可能也要哭了，因为这是很严重的系统bug。为了解决这个问题，可以加悲观锁，将用户A的那行数据锁住，在同一时刻只允许一个请求获得锁，更新数据，其他的请求则等待。通常情况下通过如下sql锁住单行数据：select * from user id=123 for update;具体流程如下：具体步骤：多个请求同时根据id查询用户信息。判断余额是否不足100，如果余额不足，则直接返回余额不足。如果余额充足，则通过for update再次查询用户信息，并且尝试获取锁。只有第一个请求能获取到行锁，其余没有获取锁的请求，则等待下一次获取锁的机会。第一个请求获取到锁之后，判断余额是否不足100，如果余额足够，则进行update操作。如果余额不足，说明是重复请求，则直接返回成功。需要特别注意的是：如果使用的是mysql数据库，存储引擎必须用innodb，因为它才支持事务。此外，这里id字段一定要是主键或者唯一索引，不然会锁住整张表。悲观锁需要在同一个事务操作过程中锁住一行数据，如果事务耗时比较长，会造成大量的请求等待，影响接口性能。此外，每次请求接口很难保证都有相同的返回值，所以不适合幂等性设计场景，但是在防重场景中是可以的使用的。在这里顺便说一下，防重设计 和 幂等设计，其实是有区别的。防重设计主要为了避免产生重复数据，对接口返回没有太多要求。而幂等设计除了避免产生重复数据之外，还要求每次请求都返回一样的结果。3. 加乐观锁既然悲观锁有性能问题，为了提升接口性能，我们可以使用乐观锁。需要在表中增加一个timestamp或者version字段，这里以version字段为例。在更新数据之前先查询一下数据：select id,amount,version from user id=123;如果数据存在，假设查到的version等于1，再使用id和version字段作为查询条件更新数据：update user set amount=amount+100,version=version+1where id=123 and version=1;更新数据的同时version+1，然后判断本次update操作的影响行数，如果大于0，则说明本次更新成功，如果等于0，则说明本次更新没有让数据变更。由于第一次请求version等于1是可以成功的，操作成功后version变成2了。这时如果并发的请求过来，再执行相同的sql： update user set amount=amount+100,version=version+1where id=123 and version=1;该update操作不会真正更新数据，最终sql的执行结果影响行数是0，因为version已经变成2了，where中的version=1肯定无法满足条件。但为了保证接口幂等性，接口可以直接返回成功，因为version值已经修改了，那么前面必定已经成功过一次，后面都是重复的请求。具体流程如下：具体步骤：先根据id查询用户信息，包含version字段根据id和version字段值作为where条件的参数，更新用户信息，同时version+1判断操作影响行数，如果影响1行，则说明是一次请求，可以做其他数据操作。如果影响0行，说明是重复请求，则直接返回成功。4. 加唯一索引绝大数情况下，为了防止重复数据的产生，我们都会在表中加唯一索引，这是一个非常简单，并且有效的方案。alter table `order` add UNIQUE KEY `un_code` (`code`);加了唯一索引之后，第一次请求数据可以插入成功。但后面的相同请求，插入数据时会报Duplicate entry '002' for key 'order.un_code异常，表示唯一索引有冲突。虽说抛异常对数据来说没有影响，不会造成错误数据。但是为了保证接口幂等性，我们需要对该异常进行捕获，然后返回成功。如果是java程序需要捕获：DuplicateKeyException异常，如果使用了spring框架还需要捕获：MySQLIntegrityConstraintViolationException异常。具体流程图如下：具体步骤：用户通过浏览器发起请求，服务端收集数据。将该数据插入mysql判断是否执行成功，如果成功，则操作其他数据（可能还有其他的业务逻辑）。如果执行失败，捕获唯一索引冲突异常，直接返回成功。5. 建防重表有时候表中并非所有的场景都不允许产生重复的数据，只有某些特定场景才不允许。这时候，直接在表中加唯一索引，显然是不太合适的。针对这种情况，我们可以通过建防重表来解决问题。该表可以只包含两个字段：id 和 唯一索引，唯一索引可以是多个字段比如：name、code等组合起来的唯一标识，例如：susan_0001。具体流程图如下：具体步骤：用户通过浏览器发起请求，服务端收集数据。将该数据插入mysql防重表判断是否执行成功，如果成功，则做mysql其他的数据操作（可能还有其他的业务逻辑）。如果执行失败，捕获唯一索引冲突异常，直接返回成功。需要特别注意的是：防重表和业务表必须在同一个数据库中，并且操作要在同一个事务中。6. 根据状态机很多时候业务表是有状态的，比如订单表中有：1-下单、2-已支付、3-完成、4-撤销等状态。如果这些状态的值是有规律的，按照业务节点正好是从小到大，我们就能通过它来保证接口的幂等性。假如id=123的订单状态是已支付，现在要变成完成状态。update `order` set status=3 where id=123 and status=2;第一次请求时，该订单的状态是已支付，值是2，所以该update语句可以正常更新数据，sql执行结果的影响行数是1，订单状态变成了3。后面有相同的请求过来，再执行相同的sql时，由于订单状态变成了3，再用status=2作为条件，无法查询出需要更新的数据，所以最终sql执行结果的影响行数是0，即不会真正的更新数据。但为了保证接口幂等性，影响行数是0时，接口也可以直接返回成功。具体流程图如下：具体步骤：用户通过浏览器发起请求，服务端收集数据。根据id和当前状态作为条件，更新成下一个状态判断操作影响行数，如果影响了1行，说明当前操作成功，可以进行其他数据操作。如果影响了0行，说明是重复请求，直接返回成功。主要特别注意的是，该方案仅限于要更新的表有状态字段，并且刚好要更新状态字段的这种特殊情况，并非所有场景都适用。7. 加分布式锁其实前面介绍过的加唯一索引或者加防重表，本质是使用了数据库的分布式锁，也属于分布式锁的一种。但由于数据库分布式锁的性能不太好，我们可以改用：redis或zookeeper。鉴于现在很多公司分布式配置中心改用apollo或nacos，已经很少用zookeeper了，我们以redis为例介绍分布式锁。目前主要有三种方式实现redis的分布式锁：setNx命令set命令Redission框架每种方案各有利弊，具体实现细节我就不说了，有兴趣的朋友可以加我微信找我私聊。具体流程图如下：具体步骤：用户通过浏览器发起请求，服务端会收集数据，并且生成订单号code作为唯一业务字段。使用redis的set命令，将该订单code设置到redis中，同时设置超时时间。判断是否设置成功，如果设置成功，说明是第一次请求，则进行数据操作。如果设置失败，说明是重复请求，则直接返回成功。需要特别注意的是：分布式锁一定要设置一个合理的过期时间，如果设置过短，无法有效的防止重复请求。如果设置过长，可能会浪费redis的存储空间，需要根据实际业务情况而定。8. 获取token除了上述方案之外，还有最后一种使用token的方案。该方案跟之前的所有方案都有点不一样，需要两次请求才能完成一次业务操作。第一次请求获取token第二次请求带着这个token，完成业务操作。具体流程图如下：第一步，先获取token。第二步，做具体业务操作。具体步骤：用户访问页面时，浏览器自动发起获取token请求。服务端生成token，保存到redis中，然后返回给浏览器。用户通过浏览器发起请求时，携带该token。在redis中查询该token是否存在，如果不存在，说明是第一次请求，做则后续的数据操作。如果存在，说明是重复请求，则直接返回成功。在redis中token会在过期时间之后，被自动删除。以上方案是针对幂等设计的。如果是防重设计，流程图要改改：需要特别注意的是：token必须是全局唯一的。
前言在电商、外卖、票务等系统中，订单超时未支付自动取消是一个常见的需求。这个功能乍一看很简单，甚至很多初学者会觉得："不就是加个定时器么？" 但真到了实际工作中，细节的复杂程度往往会超乎预期。这里我们从基础到高级，逐步分析各种实现方案，最后分享一些在生产中常见的优化技巧，希望对你会有所帮助。1. 使用延时队列（DelayQueue）适用场景：订单数量较少，系统并发量不高。延时队列是Java并发包（java.util.concurrent）中的一个数据结构，专门用于处理延时任务。订单在创建时，将其放入延时队列，并设置超时时间。延时时间到了以后，队列会触发消费逻辑，执行取消操作。示例代码：import java.util.concurrent.*; public class OrderCancelService { private static final DelayQueue<OrderTask> delayQueue = new DelayQueue<>(); public static void main(String[] args) throws InterruptedException { // 启动消费者线程 new Thread(() -> { while (true) { try { OrderTask task = delayQueue.take(); // 获取到期任务 System.out.println("取消订单：" + task.getOrderId()); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }).start(); // 模拟订单创建 for (int i = 1; i <= 5; i++) { delayQueue.put(new OrderTask(i, System.currentTimeMillis() + 5000)); // 5秒后取消 System.out.println("订单" + i + "已创建"); } } static class OrderTask implements Delayed { private final long expireTime; private final int orderId; public OrderTask(int orderId, long expireTime) { this.orderId = orderId; this.expireTime = expireTime; } public int getOrderId() { return orderId; } @Override public long getDelay(TimeUnit unit) { return unit.convert(expireTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS); } @Override public int compareTo(Delayed o) { return Long.compare(this.expireTime, ((OrderTask) o).expireTime); } } }优点：实现简单，逻辑清晰。缺点：依赖内存，系统重启会丢失任务。随着订单量增加，内存占用会显著上升。2. 基于数据库轮询适用场景：订单数量较多，但系统对实时性要求不高。轮询是最容易想到的方案：定期扫描数据库，将超时的订单状态更新为“已取消”。示例代码：public void cancelExpiredOrders() { String sql = "UPDATE orders SET status = 'CANCELLED' WHERE status = 'PENDING' AND create_time < ?"; try (Connection conn = dataSource.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setTimestamp(1, new Timestamp(System.currentTimeMillis() - 30 * 60 * 1000)); // 30分钟未支付取消 int affectedRows = ps.executeUpdate(); System.out.println("取消订单数量：" + affectedRows); } catch (SQLException e) { e.printStackTrace(); } }优点：数据可靠性强，不依赖内存。实现成本低，无需引入第三方组件。缺点：频繁扫描数据库，会带来较大的性能开销。实时性较差（通常定时任务间隔为分钟级别）。优化建议：为相关字段加索引，避免全表扫描。结合分表分库策略，减少单表压力。3. 基于Redis队列适用场景：适合对实时性有要求的中小型项目。Redis 的 List 或 Sorted Set 数据结构非常适合用作延时任务队列。我们可以把订单的超时时间作为 Score，订单 ID 作为 Value 存到 Redis 的 ZSet 中，定时去取出到期的订单进行取消。例子：public void addOrderToQueue(String orderId, long expireTime) { jedis.zadd("order_delay_queue", expireTime, orderId); } public void processExpiredOrders() { long now = System.currentTimeMillis(); Set<String> expiredOrders = jedis.zrangeByScore("order_delay_queue", 0, now); for (String orderId : expiredOrders) { System.out.println("取消订单：" + orderId); jedis.zrem("order_delay_queue", orderId); // 删除已处理的订单 } }优点：实时性高。Redis 的性能优秀，延迟小。缺点：Redis 容量有限，适合中小规模任务。需要额外处理 Redis 宕机或数据丢失的问题。4. Redis Key 过期回调适用场景：对超时事件实时性要求高，并且希望依赖 Redis 本身的特性实现简单的任务调度。Redis 提供了 Key 的过期功能，结合 keyevent 事件通知机制，可以实现订单的自动取消逻辑。当订单设置超时时间后，Redis 会在 Key 过期时发送通知，我们只需要订阅这个事件并进行相应的处理。例子：设置订单的过期时间：public void setOrderWithExpiration(String orderId, long expireSeconds) { jedis.setex("order:" + orderId, expireSeconds, "PENDING"); }订阅 Redis 的过期事件：public void subscribeToExpirationEvents() { Jedis jedis = new Jedis("localhost"); jedis.psubscribe(new JedisPubSub() { @Override public void onPMessage(String pattern, String channel, String message) { if (channel.equals("__keyevent@0__:expired")) { System.out.println("接收到过期事件，取消订单：" + message); // 执行取消订单的业务逻辑 } } }, "__keyevent@0__:expired"); // 订阅过期事件 }优点：实现简单，直接利用 Redis 的过期机制。实时性高，过期事件触发后立即响应。缺点：依赖 Redis 的事件通知功能，需要开启 notify-keyspace-events 配置。如果 Redis 中大量使用过期 Key，可能导致性能问题。注意事项：要使用 Key 过期事件，需要确保 Redis 配置文件中 notify-keyspace-events 的值包含 Ex。比如：notify-keyspace-events Ex5. 基于消息队列（如RabbitMQ）适用场景：高并发系统，实时性要求高。订单创建时，将订单消息发送到延迟队列（如RabbitMQ 的 x-delayed-message 插件）。延迟时间到了以后，消息会重新投递到消费者，消费者执行取消操作。示例代码（以RabbitMQ为例）：public void sendOrderToDelayQueue(String orderId, long delay) { Map<String, Object> args = new HashMap<>(); args.put("x-delayed-type", "direct"); ConnectionFactory factory = new ConnectionFactory(); try (Connection connection = factory.newConnection(); Channel channel = connection.createChannel()) { channel.exchangeDeclare("delayed_exchange", "x-delayed-message", true, false, args); channel.queueDeclare("delay_queue", true, false, false, null); channel.queueBind("delay_queue", "delayed_exchange", "order.cancel"); AMQP.BasicProperties props = new AMQP.BasicProperties.Builder() .headers(Map.of("x-delay", delay)) // 延迟时间 .build(); channel.basicPublish("delayed_exchange", "order.cancel", props, orderId.getBytes()); } catch (Exception e) { e.printStackTrace(); } }优点：消息队列支持分布式，高并发下表现优秀。数据可靠性高，不容易丢消息。缺点：引入消息队列增加了系统复杂性。需要处理队列堆积的问题。6. 使用定时任务框架适用场景：订单取消操作复杂，需要分布式支持。定时任务框架，比如：Quartz、Elastic-Job，能够高效地管理任务调度，适合处理批量任务。比如 Quartz 可以通过配置 Cron 表达式，定时执行订单取消逻辑。示例代码：@Scheduled(cron = "0 */5 * * * ?") public void scanAndCancelOrders() { System.out.println("开始扫描并取消过期订单"); // 这里调用数据库更新逻辑 }优点：成熟的调度框架支持复杂任务调度。灵活性高，支持分布式扩展。缺点：对实时性支持有限。框架本身较复杂。7. 基于触发式事件流处理适用场景：需要处理实时性较高的订单取消，同时结合复杂业务逻辑，例如根据用户行为动态调整超时时间。可以借助事件流处理框架（如 Apache Flink 或 Spark Streaming），实时地处理订单状态，并触发超时事件。每个订单生成后，可以作为事件流的一部分，订单未支付时通过流计算触发超时取消逻辑。示例代码（以 Apache Flink 为例）：DataStream<OrderEvent> orderStream = env.fromCollection(orderEvents); orderStream .keyBy(OrderEvent::getOrderId) .process(new KeyedProcessFunction<String, OrderEvent, Void>() { @Override public void processElement(OrderEvent event, Context ctx, Collector<Void> out) throws Exception { // 注册一个定时器 ctx.timerService().registerProcessingTimeTimer(event.getTimestamp() + 30000); // 30秒超时 } @Override public void onTimer(long timestamp, OnTimerContext ctx, Collector<Void> out) throws Exception { // 定时器触发，执行订单取消逻辑 System.out.println("订单超时取消，订单ID：" + ctx.getCurrentKey()); } });优点：实时性高，支持复杂事件处理逻辑。适合动态调整超时时间，满足灵活的业务需求。缺点：引入了流计算框架，系统复杂度增加。对运维要求较高。总结每种方案都有自己的适用场景，大家在选择的时候，记得结合业务需求、订单量、并发量来综合考虑。如果你的项目规模较小，可以直接用延时队列或 Redis；而在大型高并发系统中，消息队列和事件流处理往往是首选。当然，代码实现只是第一步，更重要的是在实际部署和运行中进行性能调优，保证系统的稳定性。希望这篇文章能给大家一些启发，也欢迎讨论其他可能的实现思路！
延迟任务在我们日常生活中比较常见，比如订单支付超时取消订单功能，又比如自动确定收货的功能等等。所以本篇文章就来从实现到原理来盘点延迟任务的11种实现方式，这些方式并没有绝对的好坏之分，只是适用场景的不大相同。DelayQueueDelayQueue是JDK提供的api，是一个延迟队列DelayQueue泛型参数得实现Delayed接口，Delayed继承了Comparable接口。getDelay方法返回这个任务还剩多久时间可以执行，小于0的时候说明可以这个延迟任务到了执行的时间了。compareTo这个是对任务排序的，保证最先到延迟时间的任务排到队列的头。来个demo@Getter public class SanYouTask implements Delayed { private final String taskContent; private final Long triggerTime; public SanYouTask(String taskContent, Long delayTime) { this.taskContent = taskContent; this.triggerTime = System.currentTimeMillis() + delayTime * 1000; } @Override public long getDelay(TimeUnit unit) { return unit.convert(triggerTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS); } @Override public int compareTo(Delayed o) { return this.triggerTime.compareTo(((SanYouTask) o).triggerTime); } }SanYouTask实现了Delayed接口，构造参数taskContent：延迟任务的具体的内容delayTime：延迟时间，秒为单位测试@Slf4j public class DelayQueueDemo { public static void main(String[] args) { DelayQueue<SanYouTask> sanYouTaskDelayQueue = new DelayQueue<>(); new Thread(() -> { while (true) { try { SanYouTask sanYouTask = sanYouTaskDelayQueue.take(); log.info("获取到延迟任务:{}", sanYouTask.getTaskContent()); } catch (Exception e) { } } }).start(); log.info("提交延迟任务"); sanYouTaskDelayQueue.offer(new SanYouTask("三友的java日记5s", 5L)); sanYouTaskDelayQueue.offer(new SanYouTask("三友的java日记3s", 3L)); sanYouTaskDelayQueue.offer(new SanYouTask("三友的java日记8s", 8L)); } }开启一个线程从DelayQueue中获取任务，然后提交了三个任务，延迟时间分为别5s，3s，8s。测试结果：成功实现了延迟任务。实现原理offer方法在提交任务的时候，会通过根据compareTo的实现对任务进行排序，将最先需要被执行的任务放到队列头。take方法获取任务的时候，会拿到队列头部的元素，也就是队列中最早需要被执行的任务，通过getDelay返回值判断任务是否需要被立刻执行，如果需要的话，就返回任务，如果不需要就会等待这个任务到延迟时间的剩余时间，当时间到了就会将任务返回。TimerTimer也是JDK提供的api先来demo@Slf4j public class TimerDemo { public static void main(String[] args) { Timer timer = new Timer(); log.info("提交延迟任务"); timer.schedule(new TimerTask() { @Override public void run() { log.info("执行延迟任务"); } }, 5000); } }通过schedule提交一个延迟时间为5s的延迟任务实现原理提交的任务是一个TimerTaskpublic abstract class TimerTask implements Runnable { //忽略其它属性 long nextExecutionTime; }TimerTask内部有一个nextExecutionTime属性，代表下一次任务执行的时间，在提交任务的时候会计算出nextExecutionTime值。Timer内部有一个TaskQueue对象，用来保存TimerTask任务的，会根据nextExecutionTime来排序，保证能够快速获取到最早需要被执行的延迟任务。在Timer内部还有一个执行任务的线程TimerThread，这个线程就跟DelayQueue demo中开启的线程作用是一样的，用来执行到了延迟时间的任务。所以总的来看，Timer有点像整体封装了DelayQueue demo中的所有东西，让用起来简单点。虽然Timer用起来比较简单，但是在阿里规范中是不推荐使用的，主要是有以下几点原因：Timer使用单线程来处理任务，长时间运行的任务会导致其他任务的延时处理Timer没有对运行时异常进行处理，一旦某个任务触发运行时异常，会导致整个Timer崩溃，不安全ScheduledThreadPoolExecutor由于Timer在使用上有一定的问题，所以在JDK1.5版本的时候提供了ScheduledThreadPoolExecutor，这个跟Timer的作用差不多，并且他们的方法的命名都是差不多的，但是ScheduledThreadPoolExecutor解决了单线程和异常崩溃等问题。来个demo@Slf4j public class ScheduledThreadPoolExecutorDemo { public static void main(String[] args) { ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(2, new ThreadPoolExecutor.CallerRunsPolicy()); log.info("提交延迟任务"); executor.schedule(() -> log.info("执行延迟任务"), 5, TimeUnit.SECONDS); } }结果实现原理ScheduledThreadPoolExecutor继承了ThreadPoolExecutor，也就是继承了线程池，所以可以有很多个线程来执行任务。ScheduledThreadPoolExecutor在构造的时候会传入一个DelayedWorkQueue阻塞队列，所以线程池内部的阻塞队列是DelayedWorkQueue。在提交延迟任务的时候，任务会被封装一个任务会被封装成ScheduledFutureTask对象，然后放到DelayedWorkQueue阻塞队列中。ScheduledFutureTaskScheduledFutureTask实现了前面提到的Delayed接口，所以其实可以猜到DelayedWorkQueue会根据ScheduledFutureTask对于Delayed接口的实现来排序，所以线程能够获取到最早到延迟时间的任务。当线程从DelayedWorkQueue中获取到需要执行的任务之后就会执行任务。RocketMQRocketMQ是阿里开源的一款消息中间件，实现了延迟消息的功能，如果有对RocketMQ不熟悉的小伙伴可以看一下我之前写的RocketMQ保姆级教程和RocketMQ消息短暂而又精彩的一生 这两篇文章。RocketMQ延迟消息的延迟时间默认有18个等级。当发送消息的时候只需要指定延迟等级即可。如果这18个等级的延迟时间不符和你的要求，可以修改RocketMQ服务端的配置文件。来个demo依赖<dependency> <groupId>org.apache.rocketmq</groupId> <artifactId>rocketmq-spring-boot-starter</artifactId> <version>2.2.1</version> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>2.2.5.RELEASE</version> </dependency>配置文件rocketmq: name-server: 192.168.200.144:9876 #服务器ip:nameServer端口 producer: group: sanyouProducercontroller类，通过DefaultMQProducer发送延迟消息到sanyouDelayTaskTopic这个topic，延迟等级为2，也就是延迟时间为5s的意思。@RestController @Slf4j public class RocketMQDelayTaskController { @Resource private DefaultMQProducer producer; @GetMapping("/rocketmq/add") public void addTask(@RequestParam("task") String task) throws Exception { Message msg = new Message("sanyouDelayTaskTopic", "TagA", task.getBytes(RemotingHelper.DEFAULT_CHARSET)); msg.setDelayTimeLevel(2); // 发送消息并得到消息的发送结果，然后打印 log.info("提交延迟任务"); producer.send(msg); } }创建一个消费者，监听sanyouDelayTaskTopic的消息。@Component @RocketMQMessageListener(consumerGroup = "sanyouConsumer", topic = "sanyouDelayTaskTopic") @Slf4j public class SanYouDelayTaskTopicListener implements RocketMQListener<String> { @Override public void onMessage(String msg) { log.info("获取到延迟任务:{}", msg); } }启动应用，浏览器输入以下链接添加任务测试结果：实现原理生产者发送延迟消息之后，RocketMQ服务端在接收到消息之后，会去根据延迟级别是否大于0来判断是否是延迟消息如果不大于0，说明不是延迟消息，那就会将消息保存到指定的topic中如果大于0，说明是延迟消息，此时RocketMQ会进行一波偷梁换柱的操作，将消息的topic改成SCHEDULE_TOPIC_XXXX中，XXXX不是占位符，然后存储。在BocketMQ内部有一个延迟任务，相当于是一个定时任务，这个任务就会获取SCHEDULE_TOPIC_XXXX中的消息，判断消息是否到了延迟时间，如果到了，那么就会将消息的topic存储到原来真正的topic(拿我们的例子来说就是sanyouDelayTaskTopic)中，之后消费者就可以从真正的topic中获取到消息了。定时任务RocketMQ这种实现方式相比于前面提到的三种更加可靠，因为前面提到的三种任务内容都是存在内存的，服务器重启任务就丢了，如果要实现任务不丢还得自己实现逻辑，但是RocketMQ消息有持久化机制，能够保证任务不丢失。RabbitMQRabbitMQ也是一款消息中间件，通过RabbitMQ的死信队列也可以是先延迟任务的功能。demo引入RabbitMQ的依赖<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> <version>2.2.5.RELEASE</version> </dependency>配置文件spring: rabbitmq: host: 192.168.200.144 #服务器ip port: 5672 virtual-host: /RabbitMQ死信队列的配置类，后面说原理的时候会介绍干啥的@Configuration public class RabbitMQConfiguration { @Bean public DirectExchange sanyouDirectExchangee() { return new DirectExchange("sanyouDirectExchangee"); } @Bean public Queue sanyouQueue() { return QueueBuilder //指定队列名称，并持久化 .durable("sanyouQueue") //设置队列的超时时间为5秒，也就是延迟任务的时间 .ttl(5000) //指定死信交换机 .deadLetterExchange("sanyouDelayTaskExchangee") .build(); } @Bean public Binding sanyouQueueBinding() { return BindingBuilder.bind(sanyouQueue()).to(sanyouDirectExchangee()).with(""); } @Bean public DirectExchange sanyouDelayTaskExchange() { return new DirectExchange("sanyouDelayTaskExchangee"); } @Bean public Queue sanyouDelayTaskQueue() { return QueueBuilder //指定队列名称，并持久化 .durable("sanyouDelayTaskQueue") .build(); } @Bean public Binding sanyouDelayTaskQueueBinding() { return BindingBuilder.bind(sanyouDelayTaskQueue()).to(sanyouDelayTaskExchange()).with(""); } }RabbitMQDelayTaskController用来发送消息，这里没指定延迟时间，是因为在声明队列的时候指定了延迟时间为5s@RestController @Slf4j public class RabbitMQDelayTaskController { @Resource private RabbitTemplate rabbitTemplate; @GetMapping("/rabbitmq/add") public void addTask(@RequestParam("task") String task) throws Exception { // 消息ID，需要封装到CorrelationData中 CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString()); log.info("提交延迟任务"); // 发送消息 rabbitTemplate.convertAndSend("sanyouDirectExchangee", "", task, correlationData); } }启动应用，浏览器输入以下链接添加任务测试结果，成功实现5s的延迟任务实现原理整个工作流程如下:消息发送的时候会将消息发送到sanyouDirectExchange这个交换机上由于sanyouDirectExchange绑定了sanyouQueue，所以消息会被路由到sanyouQueue这个队列上由于sanyouQueue没有消费者消费消息，并且又设置了5s的过期时间，所以当消息过期之后，消息就被放到绑定的sanyouDelayTaskExchange死信交换机中消息到达sanyouDelayTaskExchange交换机后，由于跟sanyouDelayTaskQueue进行了绑定，所以消息就被路由到sanyouDelayTaskQueue中，消费者就能从sanyouDelayTaskQueue中拿到消息了上面说的队列与交换机的绑定关系，就是上面的配置类所干的事。其实从这个单从消息流转的角度可以看出，RabbitMQ跟RocketMQ实现有相似之处。消息最开始都并没有放到最终消费者消费的队列中，而都是放到一个中间队列中，等消息到了过期时间或者说是延迟时间，消息就会被放到最终的队列供消费者消息。只不过RabbitMQ需要你显示的手动指定消息所在的中间队列，而RocketMQ是在内部已经做好了这块逻辑。除了基于RabbitMQ的死信队列来做，RabbitMQ官方还提供了延时插件，也可以实现延迟消息的功能，这个插件的大致原理也跟上面说的一样，延时消息会被先保存在一个中间的地方，叫做Mnesia，然后有一个定时任务去查询最近需要被投递的消息，将其投递到目标队列中。监听Redis过期key在Redis中，有个发布订阅的机制生产者在消息发送时需要到指定发送到哪个channel上，消费者订阅这个channel就能获取到消息。图中channel理解成MQ中的topic。并且在Redis中，有很多默认的channel，只不过向这些channel发送消息的生产者不是我们写的代码，而是Redis本身。这里面就有这么一个channel叫做__keyevent@__:expired，db是指Redis数据库的序号。当某个Redis的key过期之后，Redis内部会发布一个事件到__keyevent@__:expired这个channel上，只要监听这个事件，那么就可以获取到过期的key。所以基于监听Redis过期key实现延迟任务的原理如下：将延迟任务作为key，过期时间设置为延迟时间监听__keyevent@__:expired这个channel，那么一旦延迟任务到了过期时间（延迟时间），那么就可以获取到这个任务来个demoSpring已经实现了监听__keyevent@__:expired这个channel这个功能，__keyevent@__:expired中的*代表通配符的意思，监听所有的数据库。所以demo写起来就很简单了，只需4步即可依赖<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <version>2.2.5.RELEASE</version> </dependency>配置文件spring: redis: host: 192.168.200.144 port: 6379配置类@Configuration public class RedisConfiguration { @Bean public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory connectionFactory) { RedisMessageListenerContainer redisMessageListenerContainer = new RedisMessageListenerContainer(); redisMessageListenerContainer.setConnectionFactory(connectionFactory); return redisMessageListenerContainer; } @Bean public KeyExpirationEventMessageListener redisKeyExpirationListener(RedisMessageListenerContainer redisMessageListenerContainer) { return new KeyExpirationEventMessageListener(redisMessageListenerContainer); } }KeyExpirationEventMessageListener实现了对__keyevent@*__:expiredchannel的监听当KeyExpirationEventMessageListener收到Redis发布的过期Key的消息的时候，会发布RedisKeyExpiredEvent事件所以我们只需要监听RedisKeyExpiredEvent事件就可以拿到过期消息的Key，也就是延迟消息。对RedisKeyExpiredEvent事件的监听实现MyRedisKeyExpiredEventListener@Component public class MyRedisKeyExpiredEventListener implements ApplicationListener<RedisKeyExpiredEvent> { @Override public void onApplicationEvent(RedisKeyExpiredEvent event) { byte[] body = event.getSource(); System.out.println("获取到延迟消息：" + new String(body)); } }代码写好，启动应用之后我直接通过Redis命令设置消息，就没通过代码发送消息了，消息的key为sanyou，值为task，值不重要，过期时间为5sset sanyou task expire sanyou 5成功获取到延迟任务虽然这种方式可以实现延迟任务，但是这种方式坑比较多任务存在延迟Redis过期事件的发布不是指key到了过期时间就发布，而是key到了过期时间被清除之后才会发布事件。而Redis过期key的两种清除策略，就是面试八股文常背的两种：惰性清除。当这个key过期之后，访问时，这个Key才会被清除定时清除。后台会定期检查一部分key，如果有key过期了，就会被清除所以即使key到了过期时间，Redis也不一定会发送key过期事件，这就到导致虽然延迟任务到了延迟时间也可能获取不到延迟任务。丢消息太频繁Redis实现的发布订阅模式，消息是没有持久化机制，当消息发布到某个channel之后，如果没有客户端订阅这个channel，那么这个消息就丢了，并不会像MQ一样进行持久化，等有消费者订阅的时候再给消费者消费。所以说，假设服务重启期间，某个生产者或者是Redis本身发布了一条消息到某个channel，由于服务重启，没有监听这个channel，那么这个消息自然就丢了。消息消费只有广播模式Redis的发布订阅模式消息消费只有广播模式一种。所谓的广播模式就是多个消费者订阅同一个channel，那么每个消费者都能消费到发布到这个channel的所有消息。如图，生产者发布了一条消息，内容为sanyou，那么两个消费者都可以同时收到sanyou这条消息。所以，如果通过监听channel来获取延迟任务，那么一旦服务实例有多个的话，还得保证消息不能重复处理，额外地增加了代码开发量。接收到所有key的某个事件这个不属于Redis发布订阅模式的问题，而是Redis本身事件通知的问题。当监听了__keyevent@__:expired的channel，那么所有的Redis的key只要发生了过期事件都会被通知给消费者，不管这个key是不是消费者想接收到的。所以如果你只想消费某一类消息的key，那么还得自行加一些标记，比如消息的key加个前缀，消费的时候判断一下带前缀的key就是需要消费的任务。Redisson的RDelayedQueueRedisson他是Redis的儿子（Redis son），基于Redis实现了非常多的功能，其中最常使用的就是Redis分布式锁的实现，但是除了实现Redis分布式锁之外，它还实现了延迟队列的功能。先来个demo引入pom<dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.13.1</version> </dependency>封装了一个RedissonDelayQueue类@Component @Slf4j public class RedissonDelayQueue { private RedissonClient redissonClient; private RDelayedQueue<String> delayQueue; private RBlockingQueue<String> blockingQueue; @PostConstruct public void init() { initDelayQueue(); startDelayQueueConsumer(); } private void initDelayQueue() { Config config = new Config(); SingleServerConfig serverConfig = config.useSingleServer(); serverConfig.setAddress("redis://localhost:6379"); redissonClient = Redisson.create(config); blockingQueue = redissonClient.getBlockingQueue("SANYOU"); delayQueue = redissonClient.getDelayedQueue(blockingQueue); } private void startDelayQueueConsumer() { new Thread(() -> { while (true) { try { String task = blockingQueue.take(); log.info("接收到延迟任务:{}", task); } catch (Exception e) { e.printStackTrace(); } } }, "SANYOU-Consumer").start(); } public void offerTask(String task, long seconds) { log.info("添加延迟任务:{} 延迟时间:{}s", task, seconds); delayQueue.offer(task, seconds, TimeUnit.SECONDS); } }这个类在创建的时候会去初始化延迟队列，创建一个RedissonClient对象，之后通过RedissonClient对象获取到RDelayedQueue和RBlockingQueue对象，传入的队列名字叫SANYOU，这个名字无所谓。当延迟队列创建之后，会开启一个延迟任务的消费线程，这个线程会一直从RBlockingQueue中通过take方法阻塞获取延迟任务。添加任务的时候是通过RDelayedQueue的offer方法添加的。controller类，通过接口添加任务，延迟时间为5s@RestController public class RedissonDelayQueueController { @Resource private RedissonDelayQueue redissonDelayQueue; @GetMapping("/add") public void addTask(@RequestParam("task") String task) { redissonDelayQueue.offerTask(task, 5); } }启动项目，在浏览器输入如下连接，添加任务静静等待5s，成功获取到任务。实现原理如下是Redisson延迟队列的实现原理SANYOU前面的前缀都是固定的，Redisson创建的时候会拼上前缀。redisson_delay_queue_timeout:SANYOU，sorted set数据类型，存放所有延迟任务，按照延迟任务的到期时间戳（提交任务时的时间戳 + 延迟时间）来排序的，所以列表的最前面的第一个元素就是整个延迟队列中最早要被执行的任务，这个概念很重要redisson_delay_queue:SANYOU，list数据类型，也是存放所有的任务，但是研究下来发现好像没什么用。。SANYOU，list数据类型，被称为目标队列，这个里面存放的任务都是已经到了延迟时间的，可以被消费者获取的任务，所以上面demo中的RBlockingQueue的take方法是从这个目标队列中获取到任务的redisson_delay_queue_channel:SANYOU，是一个channel，用来通知客户端开启一个延迟任务任务提交的时候，Redisson会将任务放到redisson_delay_queue_timeout:SANYOU中，分数就是提交任务的时间戳+延迟时间，就是延迟任务的到期时间戳Redisson客户端内部通过监听redisson_delay_queue_channel:SANYOU这个channel来提交一个延迟任务，这个延迟任务能够保证将redisson_delay_queue_timeout:SANYOU中到了延迟时间的任务从redisson_delay_queue_timeout:SANYOU中移除，存到SANYOU这个目标队列中。于是消费者就可以从SANYOU这个目标队列获取到延迟任务了。所以从这可以看出，Redisson的延迟任务的实现跟前面说的MQ的实现都是殊途同归，最开始任务放到中间的一个地方，叫做redisson_delay_queue_timeout:SANYOU，然后会开启一个类似于定时任务的一个东西，去判断这个中间地方的消息是否到了延迟时间，到了再放到最终的目标的队列供消费者消费。Redisson的这种实现方式比监听Redis过期key的实现方式更加可靠，因为消息都存在list和sorted set数据类型中，所以消息很少丢。上述说的两种Redis的方案更详细的介绍，可以查看我之前写的用Redis实现延迟队列，我研究了两种方案，发现并不简单这篇文章。Netty的HashedWheelTimer先来个demo@Slf4j public class NettyHashedWheelTimerDemo { public static void main(String[] args) { HashedWheelTimer timer = new HashedWheelTimer(100, TimeUnit.MILLISECONDS, 8); timer.start(); log.info("提交延迟任务"); timer.newTimeout(timeout -> log.info("执行延迟任务"), 5, TimeUnit.SECONDS); } }测试结果实现原理如图，时间轮会被分成很多格子（上述demo中的8就代表了8个格子），一个格子代表一段时间（上述demo中的100就代表一个格子是100ms），所以上述demo中，每800ms会走一圈。当任务提交的之后，会根据任务的到期时间进行hash取模，计算出这个任务的执行时间所在具体的格子，然后添加到这个格子中，通过如果这个格子有多个任务，会用链表来保存。所以这个任务的添加有点像HashMap储存元素的原理。HashedWheelTimer内部会开启一个线程，轮询每个格子，找到到了延迟时间的任务，然后执行。由于HashedWheelTimer也是单线程来处理任务，所以跟Timer一样，长时间运行的任务会导致其他任务的延时处理。前面Redisson中提到的客户端延迟任务就是基于Netty的HashedWheelTimer实现的。Hutool的SystemTimerHutool工具类也提供了延迟任务的实现SystemTimerdemo@Slf4j public class SystemTimerDemo { public static void main(String[] args) { SystemTimer systemTimer = new SystemTimer(); systemTimer.start(); log.info("提交延迟任务"); systemTimer.addTask(new TimerTask(() -> log.info("执行延迟任务"), 5000)); } }执行结果Hutool底层其实也用到了时间轮。QurtazQurtaz是一款开源作业调度框架，基于Qurtaz提供的api也可以实现延迟任务的功能。demo依赖<dependency> <groupId>org.quartz-scheduler</groupId> <artifactId>quartz</artifactId> <version>2.3.2</version> </dependency>SanYouJob实现Job接口，当任务到达执行时间的时候会调用execute的实现，从context可以获取到任务的内容@Slf4j public class SanYouJob implements Job { @Override public void execute(JobExecutionContext context) throws JobExecutionException { JobDetail jobDetail = context.getJobDetail(); JobDataMap jobDataMap = jobDetail.getJobDataMap(); log.info("获取到延迟任务:{}", jobDataMap.get("delayTask")); } }测试类public class QuartzDemo { public static void main(String[] args) throws SchedulerException, InterruptedException { // 1.创建Scheduler的工厂 SchedulerFactory sf = new StdSchedulerFactory(); // 2.从工厂中获取调度器实例 Scheduler scheduler = sf.getScheduler(); // 6.启动 调度器 scheduler.start(); // 3.创建JobDetail，Job类型就是上面说的SanYouJob JobDetail jb = JobBuilder.newJob(SanYouJob.class) .usingJobData("delayTask", "这是一个延迟任务") .build(); // 4.创建Trigger Trigger t = TriggerBuilder.newTrigger() //任务的触发时间就是延迟任务到的延迟时间 .startAt(DateUtil.offsetSecond(new Date(), 5)) .build(); // 5.注册任务和定时器 log.info("提交延迟任务"); scheduler.scheduleJob(jb, t); } }执行结果：实现原理核心组件Job：表示一个任务，execute方法的实现是对任务的执行逻辑JobDetail：任务的详情，可以设置任务需要的参数等信息Trigger：触发器，是用来触发业务的执行，比如说指定5s后触发任务，那么任务就会在5s后触发Scheduler：调度器，内部可以注册多个任务和对应任务的触发器，之后会调度任务的执行启动的时候会开启一个QuartzSchedulerThread调度线程，这个线程会去判断任务是否到了执行时间，到的话就将任务交给任务线程池去执行。无限轮询延迟任务无限轮询的意思就是开启一个线程不停的去轮询任务，当这些任务到达了延迟时间，那么就执行任务。demo@Slf4j public class PollingTaskDemo { private static final List<DelayTask> DELAY_TASK_LIST = new CopyOnWriteArrayList<>(); public static void main(String[] args) { new Thread(() -> { while (true) { try { for (DelayTask delayTask : DELAY_TASK_LIST) { if (delayTask.triggerTime <= System.currentTimeMillis()) { log.info("处理延迟任务:{}", delayTask.taskContent); DELAY_TASK_LIST.remove(delayTask); } } TimeUnit.MILLISECONDS.sleep(100); } catch (Exception e) { } } }).start(); log.info("提交延迟任务"); DELAY_TASK_LIST.add(new DelayTask("三友的java日记", 5L)); } @Getter @Setter public static class DelayTask { private final String taskContent; private final Long triggerTime; public DelayTask(String taskContent, Long delayTime) { this.taskContent = taskContent; this.triggerTime = System.currentTimeMillis() + delayTime * 1000; } } }任务可以存在数据库又或者是内存，看具体的需求，这里我为了简单就放在内存里了。执行结果：这种操作简单，但是就是效率低下，每次都得遍历所有的任务。
前言在 Java 开发中，解析 JSON 是一个非常常见的需求。不管是和前端交互、调用第三方接口，还是处理配置文件，几乎都绕不开 JSON。这篇文章总结了6种主流的 JSON 解析方法，希望对你会有所帮助。1. 使用 Jackson：业界标配功能特点强大的序列化和反序列化：支持将 JSON 字符串转为 Java 对象，也支持将 Java 对象转换为 JSON。支持复杂结构：处理嵌套对象、数组、泛型等场景非常轻松。支持注解：如 @JsonIgnore、@JsonProperty 等，能精细控制序列化与反序列化的行为。性能高：Jackson 的性能非常出色，是很多企业级项目的首选。代码示例1. JSON 转对象（反序列化）import com.fasterxml.jackson.databind.ObjectMapper; public class JacksonExample { public static void main(String[] args) throws Exception { String json = "{\"id\":1,\"name\":\"张三\"}"; ObjectMapper objectMapper = new ObjectMapper(); User user = objectMapper.readValue(json, User.class); System.out.println(user.getName()); // 输出：张三 } } class User { private int id; private String name; // Getters 和 Setters 省略 }2. 对象转 JSON（序列化）User user = new User(); user.setId(1); user.setName("李四"); ObjectMapper objectMapper = new ObjectMapper(); String json = objectMapper.writeValueAsString(user); System.out.println(json); // 输出：{"id":1,"name":"李四"}高级功能日期格式化：@JsonFormat(pattern = "yyyy-MM-dd")忽略字段：@JsonIgnore重命名字段：@JsonProperty("custom_name")优缺点功能全面，支持复杂场景配置较多，学习成本稍高性能高，社区活跃，企业级项目首选过于强大，部分功能用不上丰富的注解支持，便于控制序列化行为库体积较大，对于小型项目略显笨重2. 使用 Gson：轻量好用功能特点轻量级：Gson 的设计非常简洁，代码量少，适合中小型项目。支持泛型：可以轻松解析带泛型的 JSON。注解控制：支持通过注解控制序列化行为，如 @Expose。易扩展：通过自定义序列化器和反序列化器，可以处理复杂的场景。代码示例1. JSON 转对象import com.google.gson.Gson; public class GsonExample { public static void main(String[] args) { String json = "{\"id\":1,\"name\":\"王五\"}"; Gson gson = new Gson(); User user = gson.fromJson(json, User.class); System.out.println(user.getName()); // 输出：王五 } }2. 对象转 JSONUser user = new User(); user.setId(2); user.setName("赵六"); Gson gson = new Gson(); String json = gson.toJson(user); System.out.println(json); // 输出：{"id":2,"name":"赵六"}高级功能忽略字段：@Expose@Expose private String name;自定义序列化器/反序列化器：Gson gson = new GsonBuilder() .registerTypeAdapter(CustomClass.class, new CustomSerializer()) .create();优缺点轻量级，简单易用，适合中小型项目性能稍逊于 Jackson学习曲线平滑，新手容易上手功能不如 Jackson 丰富提供良好的扩展能力复杂对象处理起来较为麻烦3. 使用 FastJSON：高性能功能特点性能优异：FastJSON 的解析速度非常快，适合大数据量场景。支持动态字段：可以轻松处理动态 JSON 数据。强大的类型支持：支持嵌套对象、泛型、数组等复杂结构。注解控制：类似 Jackson 和 Gson，支持注解控制字段的序列化和反序列化。代码示例1. JSON 转对象import com.alibaba.fastjson.JSON; public class FastJsonExample { public static void main(String[] args) { String json = "{\"id\":1,\"name\":\"小明\"}"; User user = JSON.parseObject(json, User.class); System.out.println(user.getName()); // 输出：小明 } }2. 对象转 JSONUser user = new User(); user.setId(3); user.setName("小红"); String json = JSON.toJSONString(user); System.out.println(json); // 输出：{"id":3,"name":"小红"}高级功能自动驼峰转下划线：JSON.DEFFAULT_DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";动态字段解析：Map<String, Object> map = JSON.parseObject(json, Map.class);优缺点性能极高，解析速度快曾有历史安全漏洞的争议支持复杂的动态字段解析社区活跃度稍逊于 Jackson 和 Gson功能全面，适合大规模数据处理场景配置选项和 API 比较多，稍显复杂4. 使用 JsonPath：快速提取嵌套字段功能特点高效字段提取：通过路径表达式（类似 XPath）快速提取嵌套字段。灵活性强：支持动态字段和条件过滤。轻量级：专注于字段提取，功能简单明确。代码示例import com.jayway.jsonpath.JsonPath; public class JsonPathExample { public static void main(String[] args) { String json = """ { "store": { "book": [ {"title": "书1", "price": 10}, {"title": "书2", "price": 20} ] } } """; // 提取第一个书籍的标题 String title = JsonPath.read(json, "$.store.book[0].title"); System.out.println(title); // 输出：书1 // 提取所有书籍价格 List<Integer> prices = JsonPath.read(json, "$.store.book[*].price"); System.out.println(prices); // 输出：[10, 20] } }优缺点字段提取简洁高效不支持序列化和反序列化动态字段处理能力强依赖 JsonPath 语法适合快速提取嵌套字段不适合全量 JSON 转换5. 使用 org.json：轻量工具类功能特点轻量级：核心是一个工具类，适合简单场景。构造和解析简单：适合快速创建 JSON 或提取字段。灵活性一般：不支持复杂对象映射。代码示例import org.json.JSONObject; public class OrgJsonExample { public static void main(String[] args) { String json = "{\"id\":1,\"name\":\"张三\"}"; // 提取字段 JSONObject jsonObject = new JSONObject(json); System.out.println(jsonObject.getString("name")); // 输出：张三 // 构造 JSON JSONObject newJson = new JSONObject(); newJson.put("id", 2); newJson.put("name", "李四"); System.out.println(newJson.toString()); // 输出：{"id":2,"name":"李四"} } }优缺点轻量级，适合简单场景不支持复杂嵌套对象使用简单，学习成本低功能简单，扩展性差6. 手动解析 JSON：灵活度最高功能特点完全自由：不依赖第三方库，自己解析 JSON。动态处理：适合不规则字段结构的 JSON。代码复杂度高：适合特殊场景。代码示例import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import java.util.Map; public class ManualParsing { public static void main(String[] args) throws Exception { String json = "{\"id\":1,\"name\":\"动态字段\"}"; ObjectMapper objectMapper = new ObjectMapper(); Map<String, Object> map = objectMapper.readValue(json, new TypeReference<Map<String, Object>>() {}); System.out.println(map.get("name")); // 输出：动态字段 } }优缺点灵活性高，适合动态字段代码复杂度高，不易维护不依赖第三方库性能和效率低于专业 JSON 库总结最后给大家对比一下文章中提到的6种方法各自的优缺点：工具千千万，场景最重要！选对了工具，才能省时省力，少踩坑多摸鱼。
种1. 引言在上篇文章中，我们已经谈到了分布式中至关重要的两个理论：CAP 理论和 BASE 理论。今天将带大家探讨分布式事务里的“八奇技”，帮助大家在实际的分布式系统中更好地运用事务。2. 分布式事务常见的解决分案分布式事务是在分布式系统中，跨越多个计算机节点或数据存储系统进行的事务，在这种环境下保证事务的ACID（原子性、一致性、隔离性、持久性）属性是一大挑战。对此，业界有以下 8 种常见的解决方案，俗称 “八奇技”。奇技1）2PC二阶段提交协议（Two-phase commit protocol），简称 2PC。两阶段提交是一种强一致性事务协议，它分为准备阶段和提交阶段。在准备阶段，协调者节点询问所有参与者是否准备好提交事务，如果所有参与者都答应准备好了，那么在提交阶段，协调者会通知所有参与者提交事务。如果有任何一个参与者在准备阶段没有准备好，那么协调者会通知所有参与者回滚事务。有熟悉 MySQL 的同学可能马上想到了，MySQL 的事务提交就是通过几种日志来实现二阶段提交的。不了解MySQL执行流程的可以看我之前写的这篇文章：一张图看懂SQL执行流程优点原子性保证：2PC 协议可以保证所有参与者要么全部提交成功，要么全部失败回滚，从而实现跨多个分布式节点的事务的原子性。简单直观：2PC 的设计思路简单，逻辑清晰，容易理解，这使得它在很多传统的数据库和分布式系统中得到了广泛的应用，比如 MySQL 从 5.5 版本开始支持。缺点同步阻塞：在 2PC 的第一阶段，所有参与者在响应协调者的准备请求后，必须等待最终的提交或回滚指令。这期间，所有参与者都处于阻塞状态，无法进行其他操作，导致资源锁定时间较长，在高并发场景下很明显不太适用。单点故障：如果协调者在第二阶段崩溃，参与者可能会无限期地等待指令，因为它们不知道应该提交还是回滚。这使得整个系统容易受到单点故障的影响。数据不一致：如果在第二阶段中协调者向某些参与者发送了提交指令，而其他参与者因为网络问题没有收到指令，那么这些没有收到指令的参与者可能会选择回滚，导致数据不一致。复杂的恢复机制：当系统崩溃后，恢复过程非常复杂，所有参与者必须保持足够的信息以便在系统恢复后能够继续完成 2PC 协议。奇技2）3PC三阶段提交协议（Three-phase commit protocol），简称 3PC。三阶段提交（3PC）是两阶段提交（2PC）的改进版本，它旨在减少在协调者和参与者之间的阻塞时间，同时增加系统在某些故障情况下的容错能力，以下是 3PC 的三个阶段：CanCommit 阶段协调者行动: 发送 CanCommit 请求到所有参与者，并等待回应。参与者行动: 如果参与者可以提交事务，它就返回 Yes，并进入预备状态；如果不能提交，则返回 No。PreCommit 阶段协调者行动: 如果所有参与者回答 Yes，协调者发送 PreCommit 请求给所有参与者，并进入 Prepared 阶段；如果有任何参与者回答 No，或者等待超时，协调者发送 abort 请求。参与者行动: 在收到 PreCommit 请求后，参与者会执行事务操作，写入日志，但不提交，然后响应 ACK，并等待最终指令。如果参与者在这个阶段超时没有收到协调者的消息，它将中止事务。DoCommit 阶段协调者行动: 一旦协调者收到所有参与者的 ACK，它会进入 DoCommit 阶段，发送 commit 请求给所有参与者。参与者行动: 参与者在收到 commit 请求后，提交事务，释放所有事务锁定的资源，并向协调者发送完成消息。与 2PC 相比，3PC 在 PreCommit 阶段引入了超时机制，允许参与者在没有接收到协调者的最终指令时自行决定中止事务，这减少了协调者成为单点故障的可能性。实际业务场景3PC通常用于需要较高可靠性的分布式系统中，尤其是在那些不能接受长时间锁定资源的场景。例如：分布式数据库系统：分布式数据库可能使用 3PC 来确保跨多个数据中心的事务一致性。例如，一个全球性的银行可能需要在不同国家的分支机构之间处理账户转账，这时3PC可以减少在网络延迟或某个分支机构失去响应时的影响。电信网络：在电信运营商的计费系统中，可能会使用 3PC 来同步跨多个服务点的账单信息，这些系统通常要求高可用性和快速响应，因此不能长时间阻塞。大型分布式系统：对于需要跨多个服务和组件协调工作的大型分布式系统，比如云计算平台，3PC可以在保持事务一致性的同时，减少参与者等待协调者指令的时间。使用 3PC 的考虑因素虽然 3PC 提供了比 2PC 更好的容错性和减少了阻塞的时间，但它仍然有一些缺点：复杂性：3PC 比 2PC 更复杂，需要更多的消息交换和更多的状态管理。性能开销：3PC 引入了额外的阶段和网络通信，可能会导致更大的性能开销。极端情况：即使是 3PC，在某些极端的网络分区或多点故障情况下也可能无法保证事务的正确性。因此，在实际应用中，需要权衡 3PC 带来的好处与其复杂性和性能开销之间的关系，确保它适合特定的业务场景和系统需求。在某些情况下，其他的事务模型，如最大努力通知等最终一致性模型，可能会是更合适的选择。奇技3）TCCTCC（Try-Confirm-Cancel）是一种应用层的分布式事务解决方案，它将事务分为三个步骤：尝试（Try）、确认（Confirm）和取消（Cancel）：在 Try 阶段，会预留必要的业务资源；在 Confirm 阶段，如果所有相关的业务操作都成功了，则正式执行业务操作；如果有操作失败，则在 Cancel 阶段执行补偿操作，回滚之前的预留资源。假设我们买一张从深圳到北京的火车票，票价为 360 元，TCC 分为这三个步骤：Try：检查钱包的钱是否大于等于 360，并锁住资源（360 元和这张车票）；Cancel：如果有一个资源锁定失败，则进行 cancel 释放资源，这个过程中无论 cancel 还是其它操作失败都进行重试 cancel，所以需要保证幂等性；Confirm：如果资源锁定都成功，则进行 confirm，资源交换，这个过程中无论 confirm 还是其它操作失败都进行重试 confirm，都需保证幂等性。优势TCC 的出现解决二阶段提交的几个缺点：单点故障问题：引入了多个业务活动管理器，集群下高可用；数据不一致问题：引入超时补偿机制，由业务活动管理器来控制一致性；同步阻塞问题：引入超时补偿机制，不会锁定同步，将资源转换为业务逻辑形式，粒度更小。奇技4）分布式补偿事务（Saga）Saga 是一种长事务的解决方案，它将一个大的分布式事务拆分成多个较小的本地事务，这些本地事务通过异步消息传递串联起来。每个本地事务执行成功后，会发送消息触发下一个事务的执行。如果某个本地事务失败，Saga 会执行一系列补偿操作（回滚之前的操作）来保持数据的一致性。假设有一个旅游网站，用户可以通过它预订机票、酒店和租车服务。每个预订步骤都可以视为一个 Saga 中的小事务：用户预订机票。用户预订酒店。用户预订租车服务。如果用户成功完成了所有预订步骤，那么整个旅行预订就完成了。但如果在预订租车服务时失败了，那么 Saga 会开始执行补偿操作：取消酒店预订。取消机票预订。通过这种方式，Saga 确保了用户不会因为部分服务预订失败而损失金钱或留下未处理的预订。优点灵活性：Saga 允许每个小事务独立管理，提高了系统的灵活性。减少资源锁定：由于 Saga 不需要在事务执行过程中持续占用资源，因此可以减少长时间的资源锁定，提高系统的并发能力。容错性：Saga 通过定义补偿操作来处理失败，增强了系统的容错能力。适用于微服务架构：在微服务架构中，Saga 可以跨服务边界管理事务，每个服务独立处理自己的事务和补偿逻辑。缺点复杂性：实现 Saga 需要定义每个小事务的补偿操作，这可能会增加系统的复杂性。数据一致性：Saga 不能提供 2PC 那样的即时一致性保证，它只能保证最终一致性，这在某些业务场景中可能是不够的。补偿操作的难度：在某些情况下，补偿操作可能很难实现，尤其是当事务有副作用时（比如发送了一个不可撤销的通知）。测试和调试：由于 Saga 涉及多个服务和补偿逻辑，测试和调试可能会更加困难。在选择使用 Saga 模式时，需要仔细考虑业务场景是否适合最终一致性，以及是否能够有效地实现和管理补偿逻辑。对于那些需要高度一致性保证的场景，可能需要考虑其他事务管理机制。奇技5）分布式锁在某些情况下，可以使用分布式锁来确保多个分布式节点不会同时操作同一资源。这可以通过 Redis、ZooKeeper 等分布式协调服务来实现。不知道分布式锁或者不了解如何实现的，可以看这篇文章：说出来你可能不信，分布式锁竟然这么简单...项目/公司: 使用 Redis、ZooKeeper 等实现分布式锁的系统。实用场景: 在电商秒杀活动中，防止超卖现象，确保同一时间只有一个请求能够对库存数量进行修改。推荐场景: 当需要协调多个节点对共享资源进行访问控制时，分布式锁是一个有效的解决方案。奇技6）本地消息表本地消息表是一种确保分布式事务最终一致性的方法。它的工作原理是：在执行本地事务的同时，将需要异步执行的远程服务调用相关信息存储在同一个本地数据库的消息表中。本地事务和消息表的写入操作在同一个数据库事务中完成，这样可以保证要么都成功，要么都失败，从而保证了数据的一致性。本地事务提交后，一个独立的消息发布程序会定期扫描消息表，对于未处理的消息，发布到消息队列或直接调用远程服务。远程服务处理完成后，消息会被标记为已处理，从而确保每条消息只被处理一次。本地消息表是 ebay 公司提出的事务解决方案，它的核心原理是将需要分布式处理的任务通过消息日志的方式来异步执行。消息日志可以存储到本地文件、数据库或消息队列，再通过业务规则或人工发起重试。本地消息表基于 BASE 理论，实现数据的最终一致性，实现过程中需要注意幂等性原则。奇技7）可靠消息最终一致性通过可靠消息服务保证消息的可靠传输，并在消息消费者那里进行本地事务处理，从而实现最终一致性，所以又被称作消息事务。如果消息处理失败，可以重试或者进行人工干预。执行流程：发送 prepare 消息到消息中间件发送成功后，执行本地事务如果事务执行成功，则 commit，消息中间件将消息下发至消费端如果事务执行失败，则回滚，消息中间件将这条 prepare 消息删除消费端接收到消息进行消费，如果消费失败，则不断重试这种方案也是实现了「最终一致性」，和本地消息表类似，但是对比本地消息表实现方案，消息事务不需要再建消息表，而是将消息中间件的机制去做的，「不再依赖本地数据库事务」了。所以这种方案更适用于高并发的场景，目前市面上实现该方案的「只有阿里的 RocketMQ」。奇技8）最大努力通知原则最大努力通知也是一种基于消息的分布式事务解决方案，但它不保证 100% 的消息传递成功。它的工作原理是：在本地事务执行成功后，系统会尝试通知其他的参与者或服务。通知操作会尽最大努力去执行，但如果失败，系统不会无限重试。该方案通常结合人工干预，例如，如果通知失败，系统可能会记录日志、发送报警、或者提供管理界面供操作人员手动处理。本地消息表，或者通过 MQ 对事务进行通知都可以算作最大努力。本地消息表通过后台定时任务去异步保证数据的一致性，就是一种最大努力通知的思想：代表系统各模块之间已经最大程度地保证事务的最终一致性了。3. 小结在选择分布式事务解决方案时，需要根据业务需求、系统复杂度、性能要求等因素进行权衡。例如，对于业务场景要求数据的一致性非常高，且可以接受一定程度的性能损失时，2PC 或者 3PC 是很好的选择。对于复杂业务流程中的分布式事务，需要在业务层进行更细粒度控制时，TCC 是一个好的选择。比如，用户在电商平台下单购买商品，涉及到库存、账户余额、积分等多个服务的数据变更。而对于可容忍短时间内数据不一致的业务，则可以考虑最终一致性相关的解决方案，如：本地消息表、消息事务及最大努力通知方案等等。因此，当我们探讨分布式事务时，不仅要把握好用户痛点和实际需求，还要结合每个分布式事务解决方案的特点，才能把 “八奇技” 用到出神入化之境。
前言最近有球友问：“三哥，我们团队在做AI客服，对话一长token消耗扛不住。有没有一种方案，既能保留完整上下文记忆，又能省token？”这位朋友的问题，恰恰戳中了当下AI应用开发最头疼的痛点。既要马儿跑得快，又要马儿不吃草。这听起来像是矛盾，但经过这两年的摸索，我发现在某些条件下，确实存在“相对两全”的解法。今天这篇文章就专门跟大家一起聊聊这个话题，希望对你会有所帮助。一、为什么记忆必然消耗token？很多小伙伴可能觉得，大模型就像一个人，你说过的话它应该天然记得住。错！大模型本质上是一个无状态的函数。每次调用都是独立的，它没有任何“记忆细胞”。为了让AI记住之前聊过什么，唯一的办法就是：把历史对话拼接到下一次请求里。这就是所谓的“上下文注入”。看到没？第N次请求携带的历史，是前N-1轮的总和。token消耗随着对话轮数线性增长——更准确地说，是O(n)级别的增长。但事情没这么简单。Transformer模型的核心是自注意力机制，它的计算复杂度是O(n²)。也就是说，输入长度翻一倍，计算量翻四倍。更可怕的是，当输入过长时，模型会患上“中间迷失症”——位于长文本中间的信息被严重忽略。所以，我们的真实困境是：保留全部历史 → token爆炸 + 注意力稀释 → 又贵又笨丢弃历史 → 信息丢失 → AI变“金鱼脑”有没有一条中间道路？有。下面我会介绍8种方案，从简单到复杂，从廉价到智能，你可以根据自己的场景按需选择。二、方案一：全量记忆简单粗暴，但不推荐。这是最直觉的实现：把所有对话都存下来，每次请求全部带上。public class FullMemorySession { // 使用LinkedList存储全部对话轮次 private List<Message> fullHistory = new ArrayList<>(); public void addTurn(String userMsg, String assistantMsg) { fullHistory.add(new Message("user", userMsg)); fullHistory.add(new Message("assistant", assistantMsg)); } public String buildContext() { StringBuilder sb = new StringBuilder(); for (Message msg : fullHistory) { sb.append(msg.getRole()).append(": ").append(msg.getContent()).append("\n"); } return sb.toString(); } // 消息实体 staticclass Message { String role; String content; // 构造器、getter省略 } }代码解析：逻辑非常直接——fullHistory列表保存所有消息，buildContext()把它们全部拼接成字符串。没有任何优化。优点：信息零损失，完美保留每句话实现极简单，5分钟写完缺点：token消耗随轮数线性增长，100轮可能几万token达到模型上下文上限后（比如8K/128K），旧消息会被截断响应时间越来越慢，账单越来越高适用场景：只适合Demo演示、调试测试，或者保证对话不超过10轮的极短场景。生产环境慎用。三、方案二：滑动窗口省token，但记性差。滑动窗口只保留最近N轮对话，超出窗口的直接丢弃。public class SlidingWindowMemory { privatefinalint windowSize; // 窗口大小，比如5轮 privatefinal Queue<Message> window; // 用队列自动淘汰旧数据 public SlidingWindowMemory(int windowSize) { this.windowSize = windowSize; this.window = new ArrayDeque<>(); } public void addTurn(String userMsg, String assistantMsg) { // 加入新消息 window.offer(new Message("user", userMsg)); window.offer(new Message("assistant", assistantMsg)); // 如果超过窗口大小（注意：一轮=2条消息），就移除头部 while (window.size() > windowSize * 2) { window.poll(); } } public String buildContext() { return window.stream() .map(m -> m.getRole() + ": " + m.getContent()) .collect(Collectors.joining("\n")); } }代码解析：ArrayDeque作为队列，offer()在尾部添加，当大小超出windowSize * 2（因为一轮包含用户和助手两条消息）时，poll()移除最旧的。这样窗口始终保持固定大小。优点：token消耗严格可控，不会无限增长实现简单，性能高响应速度快缺点：早期信息永久丢失。用户第一句说“我是VIP会员”，第20轮问“我的会员权益”，AI已经忘了无法处理需要长期记忆的任务适用场景：客服快速问答、闲聊机器人、临时对话——那些不需要记住早期信息的场景。四、方案三：摘要压缩让AI自己总结记忆。这个方案的想法很巧妙：不保留原始对话，而是定期让大模型把旧对话“压缩”成一段摘要，只保留关键信息。public class SummaryMemory { private String summary = ""; // 历史摘要 private List<Message> recentMessages = new ArrayList<>(); // 未压缩的新消息 privatefinalint compressThreshold; // 触发压缩的token阈值 privatefinal LLMClient llm; // 大模型客户端 public SummaryMemory(LLMClient llm, int compressThreshold) { this.llm = llm; this.compressThreshold = compressThreshold; } public void addTurn(String userMsg, String assistantMsg) { recentMessages.add(new Message("user", userMsg)); recentMessages.add(new Message("assistant", assistantMsg)); // 估算当前token数，超过阈值则触发压缩 if (estimateTokenCount() > compressThreshold) { compress(); } } private void compress() { // 构建待压缩的内容：旧摘要 + 新消息 String toCompress = summary + "\n" + formatMessages(recentMessages); String prompt = """ 请将以下对话历史压缩成一段简洁的摘要，保留： 1. 用户的关键信息（姓名、偏好、身份、已做出的决定） 2. 重要的任务状态和上下文 3. 任何后续对话可能需要的事实 原始对话： %s 摘要： """.formatted(toCompress); // 调用大模型生成摘要 String newSummary = llm.chat(prompt); this.summary = newSummary; // 压缩后清空近期消息，但可以保留最后一轮作为锚点（防止断层） this.recentMessages.clear(); } public String buildContext() { // 上下文 = 摘要 + 最近未压缩的消息 if (summary.isEmpty()) { return formatMessages(recentMessages); } return"【历史摘要】\n" + summary + "\n\n【最近对话】\n" + formatMessages(recentMessages); } private int estimateTokenCount() { // 简单估算：中文1字≈2token，英文1词≈1.3token，这里粗略用字符数/2 int totalChars = summary.length() + formatMessages(recentMessages).length(); return totalChars / 2; } private String formatMessages(List<Message> msgs) { return msgs.stream() .map(m -> m.getRole() + ": " + m.getContent()) .collect(Collectors.joining("\n")); } }代码详解：summary字段存储压缩后的历史摘要，初始为空。recentMessages存储尚未被压缩的新消息。每次添加消息后，估算总token数（摘要+新消息），如果超过阈值，调用compress()。压缩时，将旧摘要和新消息一起发给大模型，让它生成一个新的、更精炼的摘要。压缩完成后清空recentMessages，但也可以选择保留最后1-2轮，防止摘要丢失近期细节。buildContext()返回摘要+最近消息的拼接，作为下次请求的上下文。优点：压缩比极高：100轮对话可能压缩成200字摘要，token节省90%以上关键信息被提炼出来，比滑动窗口聪明得多成本可控，摘要生成的调用次数不多（每隔N轮一次）缺点：摘要可能失真：模型可能漏掉重要细节或产生幻觉每次压缩需要额外调用LLM，增加几十毫秒延迟摘要的“信息密度”随着压缩次数增加而下降（反复压缩会丢失细节）适用场景：长周期对话（几十到几百轮），对信息完整性要求不是100%严谨的场景，比如教育辅导、角色扮演。五、方案四：向量记忆（RAG）检索相关而不是保留全部。这是目前工业界最主流的方案。思路是：不保存全部历史，而是把历史消息向量化后存入数据库，每次只检索最相关的几条历史。public class VectorMemory { privatefinal EmbeddingClient embeddingClient; // 向量化模型，如OpenAI embedding privatefinal VectorDatabase vectorDb; // 向量数据库，如Milvus、Chroma privatefinalint topK = 5; // 每次检索几条最相关的历史 public VectorMemory(EmbeddingClient embeddingClient, VectorDatabase vectorDb) { this.embeddingClient = embeddingClient; this.vectorDb = vectorDb; } // 存储一条历史消息（每个消息单独存储，带上元数据） public void saveMessage(String role, String content, Map<String, Object> metadata) { // 1. 调用embedding接口，将文本转为向量 float[] vector = embeddingClient.embed(content); // 2. 存入向量数据库 VectorRecord record = new VectorRecord(vector, content, metadata); vectorDb.insert(record); } // 检索与当前问题最相关的历史记忆 public List<Message> retrieveRelevantHistory(String currentQuestion) { // 将当前问题向量化 float[] queryVector = embeddingClient.embed(currentQuestion); // 在数据库中做相似度搜索，返回topK条最相似的历史消息 List<VectorRecord> results = vectorDb.search(queryVector, topK); // 转换成Message对象 return results.stream() .map(r -> new Message((String)r.getMetadata().get("role"), r.getContent())) .collect(Collectors.toList()); } // 构建上下文：检索结果 + 最近几轮（可选） public String buildContext(String userQuestion) { List<Message> relevantHistory = retrieveRelevantHistory(userQuestion); StringBuilder context = new StringBuilder("相关历史记忆：\n"); for (Message msg : relevantHistory) { context.append(msg.getRole()).append(": ").append(msg.getContent()).append("\n"); } return context.toString(); } }代码详解：每条消息单独存储，调用embeddingClient.embed()将其转为高维向量（比如1536维）。向量数据库存储向量+原始文本+元数据（角色、时间戳等）。当用户发来新问题时，同样将问题向量化，然后到数据库中做余弦相似度搜索，找到最相似的topK条历史消息。这些检索出的历史消息就是“与当前问题最相关的记忆”，拼接进上下文。优点：token消耗极低：每次只带5-10条最相关的历史，而不是几百条可以访问非常久远的记忆：只要存储了，就能检索到，不受窗口限制灵活性高：可以混入知识库、FAQ等外部知识缺点：检索可能不准确：如果embedding模型质量差，或者问题与历史的相关性未被捕捉到，就会漏掉关键信息需要额外组件：向量数据库、embedding服务，增加系统复杂度有延迟：embedding调用+向量检索，大约增加50-200ms适用场景：绝大多数生产环境——智能客服、AI助手、个性化推荐等。这是目前最推荐的方案。六、方案五：分层混合记忆它是工业级最强方案。没有单一方案是完美的。真正的工业级系统，往往会组合多种策略，形成分层记忆。下面这张图展示了一个典型的混合记忆架构：Java实现的核心骨架：public class HierarchicalMemory { private SlidingWindowMemory shortTerm; // L1 短期 private SummaryMemory midTerm; // L2 中期摘要 private VectorMemory longTerm; // L3 长期向量 privatefinalint SUMMARY_INTERVAL = 10; // 每10轮触发一次摘要压缩 privateint turnCount = 0; public HierarchicalMemory(LLMClient llm, EmbeddingClient embed, VectorDatabase db) { this.shortTerm = new SlidingWindowMemory(5); // 保留最近5轮 this.midTerm = new SummaryMemory(llm, 2000); // token超2000压缩 this.longTerm = new VectorMemory(embed, db); } public void addTurn(String userMsg, String assistantMsg) { turnCount++; // 存入三层记忆 shortTerm.addTurn(userMsg, assistantMsg); midTerm.addTurn(userMsg, assistantMsg); longTerm.saveMessage("user", userMsg, Map.of("turn", turnCount)); longTerm.saveMessage("assistant", assistantMsg, Map.of("turn", turnCount)); // 每10轮额外触发一次摘要同步（可选） if (turnCount % SUMMARY_INTERVAL == 0) { midTerm.compress(); // 强制压缩 } } public String buildContext(String currentQuestion) { // 1. 短期记忆（最近对话）—— 最重要，直接拼接 String shortContext = shortTerm.buildContext(); // 2. 检索长期记忆（基于当前问题） List<Message> longMemory = longTerm.retrieveRelevantHistory(currentQuestion); String longContext = formatMessages(longMemory); // 3. 中期摘要（如果摘要非空） String midContext = midTerm.getCurrentSummary(); // 4. 按重要性组装：短期 > 检索结果 > 摘要 StringBuilder finalContext = new StringBuilder(); finalContext.append("【最近对话】\n").append(shortContext).append("\n"); if (!longContext.isEmpty()) { finalContext.append("【相关历史】\n").append(longContext).append("\n"); } if (midContext != null && !midContext.isEmpty()) { finalContext.append("【历史摘要】\n").append(midContext).append("\n"); } return finalContext.toString(); } }代码解析：L1 短期：滑动窗口保留最近5轮，保证对话连贯性，延迟最低。L2 中期：摘要压缩，当消息积累到一定程度（比如token超2000或每10轮）就压缩一次，保留全局脉络。L3 长期：向量数据库存储每条消息，支持按语义检索，解决“长尾记忆”问题。构建上下文时：优先保证短期（最可靠），然后加上向量检索出的相关历史（弥补窗口丢弃的），最后补充摘要（作为兜底）。优点：兼具短时连贯、长时检索、全局摘要，覆盖几乎所有场景token消耗可控（短期固定+检索topK+摘要）即使检索失败，摘要和短期窗口也能兜底缺点：实现复杂，需要维护多个组件需要精细调参（窗口大小、摘要频率、检索数量）适用场景：大型生产系统、企业级AI应用，对体验和成本都有高要求的场景。七、方案六：状态变量提取该方案需要极致的结构化压缩。有些场景下，真正需要记忆的不是整个对话，而是几个关键状态变量。比如订票机器人只需要知道：{目的地: "北京", 日期: "2026-05-01", 人数: 2}。public class StateVariableMemory { private Map<String, Object> state = new HashMap<>(); // 核心状态 // 通过LLM从对话中提取结构化状态 public void updateState(String userMsg, String assistantMsg, LLMClient llm) { String extractPrompt = """ 从以下对话中提取关键状态变量，以JSON格式输出。 当前已有状态：%s 用户最新消息：%s AI回复：%s 请更新状态，只输出JSON，不要其他内容。 """.formatted(toJson(state), userMsg, assistantMsg); String jsonResponse = llm.chat(extractPrompt); Map<String, Object> newState = parseJson(jsonResponse); state.putAll(newState); // 合并更新 } public String buildContext() { // 上下文只需要展示当前状态，而不是历史对话 return"当前会话状态：" + toJson(state); } }优点：极致省token：几KB的状态就能代替几十KB的对话结构化，模型更容易理解缺点：只适合高度结构化的任务（订票、填表、参数收集）提取状态本身需要调用LLM，有额外成本适用场景：任务型对话、表单填写、配置向导。八、方案七：工具/函数调用把记忆“外包”给外部系统。大模型不是万能的，记忆完全可以交给外部数据库。模型只需要学会调用“保存记忆”和“查询记忆”的工具。public class ToolBasedMemory { // 定义两个工具函数 @Tool(name = "save_memory", description = "保存一条重要信息到长期记忆") public void saveMemory(String key, String value) { externalDB.put(key, value); } @Tool(name = "recall_memory", description = "根据关键词回忆之前保存的信息") public String recallMemory(String key) { return externalDB.get(key); } // 在对话循环中，让模型自主决定何时调用这些工具 }这种方案让模型自主管理记忆——它觉得重要就存，需要就用。这是目前AI Agent的主流做法。优点：极其灵活，模型可以按需存取；token消耗几乎为零（只传工具调用结果）。缺点：依赖模型自身的函数调用能力，容易出错或漏存。适用场景：Agent系统、自主决策类应用。九、终极对比总结回到最初的问题：有没有方案既能保留上下文记忆，又能省token？我的答案是：有，但不存在“免费午餐”。每一分token的节省，都换来了系统复杂度的增加或记忆精度的下降。根据我的实战经验，给你几条直接的建议：如果你刚开始做MVP：直接用滑动窗口（最近10轮），上线跑起来再说。先验证产品价值，再优化成本。如果你做的是通用客服/AI助手：首选向量检索（RAG）。这是当前最成熟、性价比最高的方案。配合一个小的滑动窗口（3-5轮）保证对话连贯性，效果已经很好了。如果你的对话轮次非常长（100+）且信息密度高：上分层混合记忆。短期窗口+中期摘要+长期向量，三者配合才能既省token又不丢信息。如果你做的是表单/订票/参数收集：状态变量提取是王道。几十个字段就能代表整个会话，token几乎不增长。永远不要在生产环境用全量记忆——除非你预算无限且用户只聊5句话。
前言最近经常有小伙伴问我：三哥，你文章中的图片，是用什么工具画的？挺好看的。今天这篇文章跟大家一起聊聊，程序员在日常工作中，最常用的10个画图神器，希望对你会有所帮助。一、AI智能画图工具它们让画图进入“一句话时代”。传统的画图工具需要手动拖拽组件、对齐线条，而AI画图工具的核心价值在于：你只需要用大白话描述需求，AI就能在几秒内生成完整的专业图表。1. next-ai-draw-io用它能一句话生成专业架构图。GitHub上近期爆火的AI画图开源项目next-ai-draw-io，短短时间内星标已飙升至2.5万颗，日均增长超500星。它把Draw.io的专业绘图能力与AI完美融合，让画图从“动手”变成了“动嘴”。核心亮点：一句话出图：用大白话描述需求，比如“画一个基于K8s的电商微服务架构图”，AI就能在10秒内自动生成完整的专业图表。自然语言交互：底层基于Next.js和React构建，用户只需输入文字指令，AI即可理解并生成图表。与传统Draw.io无缝集成：生成的是标准draw.io图表，可在Draw.io中继续手动编辑，实现“AI初稿+人工精修”的工作流。图片/PDF上传：支持上传现有图片或PDF，AI可自动识别并生成可编辑图表。官网：https://github.com/DayuanJiang/next-ai-draw-io优点：效率提升10倍以上，开源免费，可私有部署。缺点：需要一定的网络环境；复杂业务场景生成的图表可能需要微调。适用场景：日常架构图绘制、方案汇报、技术文档配图。2. Eraser它是为工程团队打造的AI设计协作平台。Eraser是一款专为工程团队打造的AI技术设计和文档协作工具，开创性地将协作画布与Markdown编辑器、“图表即代码”功能相结合。核心亮点：自然语言生成图表：通过DiagramGPT，用户只需输入自然语言提示，就能输出架构图、流程图和ERD等图表的代码，并可保存和编辑。图表即代码：图表内容以代码形式管理，完美适配Git版本控制。与GitHub深度集成：图表可直接嵌入PR描述中，代码审查时团队就能看到架构变化。工程化设计平台：图表可以与API设计、系统文档、代码变更相关联，让技术设计真正融入日常开发工作流。官网：https://www.eraser.io优点：工程化能力强，完美融入开发工作流，适合团队协作。缺点：免费版功能有限（20个AI图表），高级功能需付费。适用场景：微服务架构设计、系统文档撰写、代码审查配图。二、架构图与流程图3. Draw.io它是画架构图的“万能瑞士军刀”。Draw.io（diagrams.net）是技术圈最流行的免费开源绘图工具。它的特点是非常灵活——打开网页就能用，不用注册就能画图、导出，甚至支持离线使用和私有化部署。核心亮点：零门槛启动：打开网页即用，无需注册，无需下载。海量形状库：AWS、Azure、Kubernetes等云架构图标应有尽有。多格式导出：支持SVG、PNG、PDF、EPS等格式导出，矢量图线条清晰。云端集成：与Google Drive、Dropbox、Confluence、Jira等深度集成。官网：https://www.drawio.com在线版：https://app.diagrams.net优点：完全免费开源、跨平台、形状库极其丰富。缺点：大图性能会略有下降，团队协同功能较弱。适用场景：技术文档配图、微服务架构设计、数据库ER图。4. Excalidraw它是极富亲和力的“手绘风”白板。Excalidraw是一款备受追捧的开源“手绘风”画图工具，主打极具亲和力的“涂鸦”风格。每个图形元素都被抽象为带有type、位置、样式等属性的JSON对象，天然适合序列化和网络传输。核心亮点：独特手绘风格：涂鸦质感让技术方案在评审时降低心理负担，更有亲和力。实时协作零门槛：生成链接即可协作，无需注册账号。智能吸附连线：移动节点时连线自动调整，零学习成本。AI赋能：支持自定义AI Token，可用自己的OpenAI、Claude、Gemini API密钥驱动AI功能。官网：https://excalidraw.com优点：开源免费、GitHub上接近10万星标、协作便捷、风格辨识度高。缺点：功能偏向轻量级，不适合大型复杂的工程图。适用场景：方案讨论、需求梳理、头脑风暴、教学演示。5. Mermaid.js：代码即图表如果你和我一样喜欢用Markdown写文档，那Mermaid一定是你的最爱。它本质是一种文本标记语言，你只需写代码，Mermaid就能把它渲染成流程图、时序图或甘特图。核心亮点：代码即图表：用文本描述图形，完美适配Git版本控制。与Markdown无缝集成：GitHub/GitLab/Notion/Obsidian原生支持，文档一键渲染。10+种图表类型：流程图、时序图、类图、状态图、甘特图、Git图、看板图、C4图等。AI友好：DeepSeek等AI可自动将自然语言描述转换为Mermaid代码。官网：https://mermaid.js.org在线编辑：https://mermaid.live优点：与Git版本管理完美兼容、支持AI自动生成、文档展示一体。缺点：不适合极度复杂的图形排版，布局调试较耗时。适用场景：技术博客写作、API设计文档、README配图、内部Wiki。6. ProcessOn它是专业的云端UML建模工具。ProcessOn是很多程序员首选的国产云端绘图工具。它无需安装，打开浏览器就能使用，且内置AI一键创作功能，支持标准思维导图、会议提纲、待办列表等多个场景。核心亮点：AI一键生成：输入主题，AI可快速生成流程图和思维导图。多图形支持：支持流程图、UML、ER图、网络拓扑图、BPMN、组织结构图等。实时协作：多人同时在线编辑，提升团队工作效率。知识分享社区：海量模板可直接复用。官网：https://www.processon.com优点：国产工具网络稳定、AI辅助能力强、模板库丰富。缺点：免费版文件数量和空间有限，高级功能需付费。适用场景：UML建模、业务流程梳理、产品原型设计、甘特图绘制。三、专业设计与建模工具7. Figma它是UI/UX设计的王者。Figma最初是为设计师开发的，但现在越来越多的程序员发现，用它画高保真原型、定义组件库，简直太方便了。核心亮点：云端协作：多人实时在线编辑，无需导出或版本控制。自动布局（Auto Layout）：完美模拟了前端的弹性盒子模型。开发模式：设计师标注的样式和间距可直接生成代码片段。插件生态：数千款插件，涵盖图标、插图、设计系统等。官网：https://www.figma.com优点：设计专业度高，团队协作丝滑，生态丰富。缺点：需要一定的学习成本，适合需要前端视觉还原的复杂项目。适用场景：产品原型、UI界面设计、图标设计、交付前端。8. Visual Paradigm用它可以做专业软件建模与架构设计。当项目进入详细设计阶段，需要绘制专业的UML类图、数据流图时，Visual Paradigm是不可忽视的强手。它比一般的画图工具更深入软件工程领域。核心亮点：UML全系列建模：支持用例图、类图、时序图、状态图、活动图等所有UML图表类型。代码逆向工程：Java代码可自动转换为类图。AI图形生成器：通过自然语言描述即可生成专业模型。企业级特性：支持320,000+用户，包括中小企业、咨询公司、大学和政府机构。官网：https://www.visual-paradigm.com在线版：https://online.visual-paradigm.com优点：功能极其专业，支持从设计到代码的双向工程。缺点：本地版较为笨重，许可费用较高。适用场景：大型软件工程、架构师详细设计、敏捷项目管理。9. StarUML：它是UML逆向工程的利器。很多架构师电脑里必备的专业UML工具——StarUML。它支持从现有的Java代码逆向生成UML类图，对于梳理老系统的业务逻辑非常有用。核心亮点：代码逆向工程：Java代码自动转类图，解析依赖关系。支持多种UML图：用例图、类图、序列图、状态图、活动图、部署图等。导出多种格式：支持jpg、bmp、wmf等格式导出。轻量快速：相比重型工具，启动和运行速度快。官网：http://staruml.io优点：专业化程度高，对UML标准支持完整，轻量快速。缺点：界面风格较为传统，缺乏云端协作。适用场景：遗留系统分析、代码结构可视化、软件架构文档。四、AI协同白板与轻量工具10. BoardMix（博思白板）它是AI赋能的协同白板。BoardMix是一款国产的智能协作白板，集成了AI创作、一键生成PPT、思维导图等功能。在这个白板上，你可以把流程图、脑图、文档和图片全扔进去，适合在头脑风暴阶段进行发散思考。核心亮点：AI智能创作：输入需求，AI自动生成PPT大纲、思维导图，一键排版。无限画布：不限空间，灵感自由铺展。多格式一体化：思维导图、流程图、文档、笔记统一呈现。实时协作：支持多人同时在线编辑。官网：https://boardmix.cn优点：AI辅助能力强，国内访问快，团队协作门槛低。缺点：产品功能处于快速迭代期，部分高级功能需付费。适用场景：头脑风暴、团队会议、项目复盘、在线教学。五、总结画图是为了沟通，只要能帮助团队减少误解、提升交付效率的工具，就是最适合你的“神器”。希望今天的分享能帮你找到最趁手的那一把！
引言在工作的这些年中，我见证过太多团队在实现排行榜功能时踩过的坑。今天我想和大家分享 6 种不同的排行榜实现方案，从简单到复杂，从单机到分布式，希望能帮助大家在实际工作中做出更合适的选择。有些小伙伴在工作中可能会觉得：不就是个排行榜吗？搞个数据库排序不就完了？但实际情况远比这复杂得多。当数据量达到百万级、千万级时，简单的数据库查询可能就会成为系统的瓶颈。接下来，我将为大家详细剖析 6 种不同的实现方案，希望对你会有所帮助。方案一：数据库直接排序适用场景：数据量小（万级以下），实时性要求不高这是最简单直接的方案，几乎每个开发者最先想到的方法。示例代码如下：public List<UserScore> getRankingList() { String sql = "SELECT user_id, score FROM user_scores ORDER BY score DESC LIMIT 100"; return jdbcTemplate.query(sql, new UserScoreRowMapper()); }优点：实现简单代码维护成本低适合数据量小的场景缺点：数据量大时性能急剧下降每次查询都需要全表扫描高并发下数据库压力大架构图如下：方案二：缓存+定时任务适用场景：数据量中等（十万级），可以接受分钟级延迟这个方案在方案一的基础上引入了缓存机制。示例代码如下：@Scheduled(fixedRate = 60000) // 每分钟执行一次 public void updateRankingCache() { List<UserScore> rankings = userScoreDao.getTop1000Scores(); redisTemplate.opsForValue().set("ranking_list", rankings); } public List<UserScore> getRankingList() { return (List<UserScore>) redisTemplate.opsForValue().get("ranking_list"); }优点：减轻数据库压力查询速度快（O(1)）实现相对简单缺点：数据有延迟（取决于定时任务频率）内存占用较高排行榜更新不及时架构图如下：方案三：Redis有序集合适用场景：数据量大（百万级），需要实时更新Redis的有序集合(Sorted Set)是实现排行榜的利器。示例代码如下：public void addUserScore(String userId, double score) { redisTemplate.opsForZSet().add("ranking", userId, score); } public List<String> getTopUsers(int topN) { return redisTemplate.opsForZSet().reverseRange("ranking", 0, topN - 1); } public Long getUserRank(String userId) { return redisTemplate.opsForZSet().reverseRank("ranking", userId) + 1; }优点：高性能（O(log(N))时间复杂度）支持实时更新天然支持分页可以获取用户排名缺点：单机Redis内存有限需要考虑Redis持久化分布式环境下需要额外处理架构图如下：方案四：分片+Redis集群适用场景：超大规模数据（千万级以上），高并发场景当单机Redis无法满足需求时，可以采用分片方案。示例代码如下：// public void addUserScore(String userId, double score) { RScoredSortedSet<String> set = redisson.getScoredSortedSet("ranking:" + getShard(userId)); set.add(score, userId); } private String getShard(String userId) { // 简单哈希分片 int shard = Math.abs(userId.hashCode()) % 16; return "shard_" + shard; }在这里我们以Redisson客户端为例。优点：水平扩展能力强可以支持超大规模数据高并发下性能稳定缺点：架构复杂度高跨分片查询困难需要维护分片策略架构图如下：方案五：预计算+分层缓存适用场景：排行榜更新不频繁，但访问量极大这种方案结合了预计算和多级缓存。示例代码如下：@Scheduled(cron = "0 0 * * * ?") // 每小时计算一次 public void precomputeRanking() { Map<String, Integer> rankings = calculateRankings(); redisTemplate.opsForHash().putAll("ranking:hourly", rankings); // 同步到本地缓存 localCache.putAll(rankings); } public Integer getUserRank(String userId) { // 1. 先查本地缓存 Integer rank = localCache.get(userId); if (rank != null) return rank; // 2. 再查Redis rank = (Integer) redisTemplate.opsForHash().get("ranking:hourly", userId); if (rank != null) { localCache.put(userId, rank); // 回填本地缓存 return rank; } // 3. 最后查DB return userScoreDao.getUserRank(userId); }优点：访问性能极高（本地缓存O(1)）减轻Redis压力适合读多写少场景缺点：数据实时性差预计算资源消耗大实现复杂度高架构图如下：方案六：实时计算+流处理适用场景：需要实时更新且数据量极大的社交平台这种方案采用流处理技术实现实时排行榜。使用Apache Flink示例如下：DataStream<UserAction> actions = env.addSource(new UserActionSource()); DataStream<Tuple2<String, Double>> scores = actions .keyBy(UserAction::getUserId) .process(new ProcessFunction<UserAction, Tuple2<String, Double>>() { private MapState<String, Double> userScores; public void open(Configuration parameters) { MapStateDescriptor<String, Double> descriptor = new MapStateDescriptor<>("userScores", String.class, Double.class); userScores = getRuntimeContext().getMapState(descriptor); } public void processElement(UserAction action, Context ctx, Collector<Tuple2<String, Double>> out) { double newScore = userScores.getOrDefault(action.getUserId(), 0.0) + calculateScore(action); userScores.put(action.getUserId(), newScore); out.collect(new Tuple2<>(action.getUserId(), newScore)); } }); scores.keyBy(0) .process(new RankProcessFunction()) .addSink(new RankingSink());优点：真正的实时更新可处理超高并发支持复杂计算逻辑缺点：架构复杂度高运维成本高需要专业团队维护架构图如下：方案对比与选择总结在选择排行榜实现方案时，我们需要综合考虑以下几个因素：数据规模：数据量大小直接决定了我们选择哪种方案实时性要求：是否需要秒级更新，还是分钟级甚至小时级都可以接受并发量：系统的预期访问量是多少开发资源：团队是否有足够的技术能力维护复杂方案业务需求：排行榜的计算逻辑是否复杂对于大多数中小型应用，方案二（缓存+定时任务）或方案三（Redis有序集合）已经足够。如果业务增长迅速，可以逐步演进到方案四（分片+Redis集群）。而对于社交平台等需要实时更新的场景，则需要考虑方案五（预计算+分层缓存）或方案六（实时计算+流处理），但要做好技术储备和架构设计。最后，无论选择哪种方案，都要做好监控和性能测试。排行榜作为高频访问的功能，其性能直接影响用户体验。建议在实际环境中进行压测，根据测试结果调整方案。希望这六种方案的详细解析能帮助大家在工作中做出更合适的选择。记住，没有最好的方案，只有最适合的方案。
引言在开发中，往往会遇到一些关于延时任务的需求。例如生成订单30分钟未支付，则自动取消生成订单60秒后,给用户发短信对上述的任务，我们给一个专业的名字来形容，那就是延时任务 。那么这里就会产生一个问题，这个延时任务 和定时任务 的区别究竟在哪里呢？一共有如下几点区别定时任务有明确的触发时间，延时任务没有定时任务有执行周期，而延时任务在某事件触发后一段时间内执行，没有执行周期定时任务一般执行的是批处理操作是多个任务，而延时任务一般是单个任务下面，我们以判断订单是否超时为例，进行方案分析方案分析该方案通常是在小型项目中使用，即通过一个线程定时的去扫描数据库，通过订单时间来判断是否有超时的订单，然后进行update或delete等操作简单介绍一下 maven项目引入一个依赖如下所示：<dependency> <groupId>org.quartz-scheduler</groupId> <artifactId>quartz</artifactId> <version>2.2.2</version> </dependency>调用Demo类MyJob如下所示：public class MyJob implements Job { public void execute(JobExecutionContext context) throws JobExecutionException { System.out.println("查询数据库"); } public static void main(String[] args) throws Exception { // 创建任务 JobDetail jobDetail = JobBuilder.newJob(MyJob.class) .withIdentity("job1", "group1").build(); // 创建触发器 每3秒钟执行一次 Trigger trigger = TriggerBuilder .newTrigger() .withIdentity("trigger1", "group3") .withSchedule( SimpleScheduleBuilder.simpleSchedule() .withIntervalInSeconds(3).repeatForever()) .build(); Scheduler scheduler = new StdSchedulerFactory().getScheduler(); // 将任务及其触发器放入调度器 scheduler.scheduleJob(jobDetail, trigger); // 调度器开始调度任务 scheduler.start(); } }运行代码，可发现每隔3秒，输出如下：查询数据库优点:简单易行，支持集群操作缺点:(1)对服务器内存消耗大(2)存在延迟，比如你每隔3分钟扫描一次，那最坏的延迟时间就是3分钟(3)假设你的订单有几千万条，每隔几分钟这样扫描一次，数据库损耗极大该方案是利用JDK自带的DelayQueue来实现，这是一个无界阻塞队列，该队列只有在延迟期满的时候才能从中获取元素，放入DelayQueue中的对象，是必须实现Delayed接口的poll():获取并移除队列的超时元素，没有则返回空take():获取并移除队列的超时元素，如果没有则wait当前线程，直到有元素满足超时条件，返回结果。定义一个类OrderDelay实现Delayed，代码如下：public class OrderDelay implements Delayed { private String orderId; private long timeout; OrderDelay(String orderId, long timeout) { this.orderId = orderId; this.timeout = timeout + System.nanoTime(); } public int compareTo(Delayed other) { if (other == this) return 0; OrderDelay t = (OrderDelay) other; long d = (getDelay(TimeUnit.NANOSECONDS) - t .getDelay(TimeUnit.NANOSECONDS)); return (d == 0) ? 0 : ((d < 0) ? -1 : 1); } // 返回距离你自定义的超时时间还有多少 public long getDelay(TimeUnit unit) { return unit.convert(timeout - System.nanoTime(), TimeUnit.NANOSECONDS); } void print() { System.out.println(orderId+"编号的订单要删除啦。。。。"); } }运行的测试Demo为，我们设定延迟时间为3秒public class DelayQueueDemo { public static void main(String[] args) { // TODO Auto-generated method stub List<String> list = new ArrayList<String>(); list.add("00000001"); list.add("00000002"); list.add("00000003"); list.add("00000004"); list.add("00000005"); DelayQueue<OrderDelay> queue = new DelayQueue<OrderDelay>(); long start = System.currentTimeMillis(); for(int i = 0;i<5;i++){ //延迟三秒取出 queue.put(new OrderDelay(list.get(i), TimeUnit.NANOSECONDS.convert(3, TimeUnit.SECONDS))); try { queue.take().print(); System.out.println("After " + (System.currentTimeMillis()-start) + " MilliSeconds"); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }输出如下:00000001编号的订单要删除啦。。。。 After 3003 MilliSeconds 00000002编号的订单要删除啦。。。。 After 6006 MilliSeconds 00000003编号的订单要删除啦。。。。 After 9006 MilliSeconds 00000004编号的订单要删除啦。。。。 After 12008 MilliSeconds 00000005编号的订单要删除啦。。。。 After 15009 MilliSeconds可以看到都是延迟3秒，订单被删除优点:效率高,任务触发时间延迟低。缺点:(1)服务器重启后，数据全部消失，怕宕机 (2)集群扩展相当麻烦 (3)因为内存条件限制的原因，比如下单未付款的订单数太多，那么很容易就出现OOM异常 (4)代码复杂度较高时间轮算法可以类比于时钟，按某一个方向按固定频率轮动，每一次跳动称为一个 tick。这样可以看出定时轮由个3个重要的属性参数，ticksPerWheel（一轮的tick数），tickDuration（一个tick的持续时间）以及 timeUnit（时间单位），例如当ticksPerWheel=60，tickDuration=1，timeUnit=秒，这就和现实中的始终的秒针走动完全类似了如果当前指针指在1上面，我有一个任务需要4秒以后执行，那么这个执行的线程回调或者消息将会被放在5上。那如果需要在20秒之后执行怎么办，由于这个环形结构槽数只到8，如果要20秒，指针需要多转2圈。位置是在2圈之后的5上面（20 % 8 + 1）我们用Netty的HashedWheelTimer来实现 给Pom加上下面的依赖：<dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> <version>4.1.24.Final</version> </dependency>测试代码HashedWheelTimerTest如下所示:public class HashedWheelTimerTest { static class MyTimerTask implements TimerTask{ boolean flag; public MyTimerTask(boolean flag){ this.flag = flag; } public void run(Timeout timeout) throws Exception { // TODO Auto-generated method stub System.out.println("要去数据库删除订单了。。。。"); this.flag =false; } } public static void main(String[] argv) { MyTimerTask timerTask = new MyTimerTask(true); Timer timer = new HashedWheelTimer(); timer.newTimeout(timerTask, 5, TimeUnit.SECONDS); int i = 1; while(timerTask.flag){ try { Thread.sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println(i+"秒过去了"); i++; } } }输出如下:1秒过去了 2秒过去了 3秒过去了 4秒过去了 5秒过去了 要去数据库删除订单了。。。。 6秒过去了优点:效率高,任务触发时间延迟时间比delayQueue低，代码复杂度比delayQueue低。缺点:(1)服务器重启后，数据全部消失，怕宕机(2)集群扩展相当麻烦(3)因为内存条件限制的原因，比如下单未付款的订单数太多，那么很容易就出现OOM异常我们可以采用rabbitMQ的延时队列。RabbitMQ具有以下两个特性，可以实现延迟队列RabbitMQ可以针对Queue和Message设置 x-message-tt，来控制消息的生存时间，如果超时，则消息变为dead letterlRabbitMQ的Queue可以配置x-dead-letter-exchange 和x-dead-letter-routing-key（可选）两个参数，用来控制队列内出现了deadletter，则按照这两个参数重新路由。结合以上两个特性，就可以模拟出延迟消息的功能优点: 高效,可以利用rabbitmq的分布式特性轻易的进行横向扩展,消息支持持久化增加了可靠性。缺点：本身的易用度要依赖于rabbitMq的运维.因为要引用rabbitMq,所以复杂度和成本变高