Rainman's Blog - 技术选型
type: channel
link: https://blog.x2boot.com/category/technology_stack_selection/
description: 技术选型
items: 高并发下如何保证接口的幂等性？
items: 订单超时自动取消的7种方案
items: 订单自动取消的11种实现方式
items: 解析JSON的6种常用方案
items: 解决分布式事务的8个小技巧
items: 节省Token的8种方案
items: 程序员最常用的10个画图工具！
items: 排行榜的实现 6 种方案！
items: 排行榜的5种实现方案！
items: 延时处理几种方案

高并发下如何保证接口的幂等性？
type: item
date: 2026-06-06T01:16:13+08:00
description: 接口幂等性问题，对于开发人员来说，是一个跟语言无关的公共问题。本文分享了一些解决这类问题非常实用的办法，绝大部分内容我在项目中实践过的，给有需要的小伙伴一个参考。不知道你有没有遇到过这些场景：有时我们在填写某些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： up…

订单超时自动取消的7种方案
type: item
date: 2026-06-06T01:16:00+08:00
description: 前言在电商、外卖、票务等系统中，订单超时未支付自动取消是一个常见的需求。这个功能乍一看很简单，甚至很多初学者会觉得："不就是加个定时器么？" 但真到了实际工作中，细节的复杂程度往往会超乎预期。这里我们从基础到高级，逐步分析各种实现方案，最后分享一些在生产中常见的优化技巧，希望对你会有所帮助。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(); } }优点：数据…

订单自动取消的11种实现方式
type: item
date: 2026-06-06T01:15:49+08:00
description: 延迟任务在我们日常生活中比较常见，比如订单支付超时取消订单功能，又比如自动确定收货的功能等等。所以本篇文章就来从实现到原理来盘点延迟任务的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 implemen…

解析JSON的6种常用方案
type: item
date: 2026-06-06T01:15:39+08:00
description: 前言在 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 的解析速度非常快…

解决分布式事务的8个小技巧
type: item
date: 2026-06-06T01:15:26+08:00
description: 种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 引入了额外的阶段和网络通信，可能会导致更大…

节省Token的8种方案
type: item
date: 2026-06-06T01:15:17+08:00
description: 前言最近有球友问：“三哥，我们团队在做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 buildCo…

程序员最常用的10个画图工具！
type: item
date: 2026-06-06T01:14:59+08:00
description: 前言最近经常有小伙伴问我：三哥，你文章中的图片，是用什么工具画的？挺好看的。今天这篇文章跟大家一起聊聊，程序员在日常工作中，最常用的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原生支持，文档一键…

排行榜的实现 6 种方案！
type: item
date: 2026-06-06T01:14:50+08:00
description: 引言在工作的这些年中，我见证过太多团队在实现排行榜功能时踩过的坑。今天我想和大家分享 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…

排行榜的5种实现方案！
type: item
date: 2026-06-06T01:14:40+08:00
description: 引言在工作的这些年中，我见证过太多团队在实现排行榜功能时踩过的坑。今天我想和大家分享 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…

延时处理几种方案
type: item
date: 2026-06-06T01:14:32+08:00
description: 引言在开发中，往往会遇到一些关于延时任务的需求。例如生成订单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.…