使用quartz定时任务实现支付单自动关单功能,并引入多线程+分段解决扫表延迟的问题

背景 我负责的订单系统模块,有一个功能就是需要实现订单的到期自动关闭,这功能以前其实是有的,但是后来我发现经常有一些订单,明明已经到期了,但是还是没有正常被关闭,就导致已超时的订单后来有支付成功的情况。 后来经过排查,是因为之前的实现方式比较简单,是基于JDK自带的delayQueue实现的,大致的代码如下: 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 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 import java.util.concurrent.DelayQueue; import java.util.concurrent.Delayed; import java.util.concurrent.TimeUnit; class Order implements Delayed { private String orderId; private long createTime; private long closeTime; public Order(String orderId, long delayInMinutes) { this.orderId = orderId; this.createTime = System.currentTimeMillis(); this.closeTime = this.createTime + TimeUnit.MINUTES.toMillis(delayInMinutes); } public String getOrderId() { return orderId; } @Override public long getDelay(TimeUnit unit) { long delay = closeTime - System.currentTimeMillis(); return unit.convert(delay, TimeUnit.MILLISECONDS); } @Override public int compareTo(Delayed other) { if (this == other) return 0; long diff = getDelay(TimeUnit.MILLISECONDS) - other.getDelay(TimeUnit.MILLISECONDS); return (int) (diff); } } public class OrderAutoCloser { public static void main(String[] args) { DelayQueue<Order> delayQueue = new DelayQueue<>(); // 创建订单并将其添加到DelayQueue中 Order order1 = new Order("Order1", 30); // 30分钟后自动关闭 Order order2 = new Order("Order2", 15); // 15分钟后自动关闭 delayQueue.offer(order1); delayQueue.offer(order2); // 启动后台线程来处理订单关闭 Thread closerThread = new Thread(() -> { while (true) { try { Order order = delayQueue.take(); System.out.println("Closing order: " + order.getOrderId()); // 在这里执行订单关闭的逻辑 } catch (InterruptedException e) { e.printStackTrace(); } } }); closerThread.start(); } } 在创建订单的时候,就指定好自动关闭的时间,并且把订单放入delayQueue中,借助delayQueue来实现到期关闭的功能。 ...

March 22, 2026 · 2 min · santu

使用自定义注解+切面减少冗余代码,提升代码的鲁棒性

相信很多人对Java中的注解都很熟悉,比如我们经常会用到的一些如@Override、@Autowired、@Service等,这些都是JDK或者诸如Spring这类框架给我们提供的。 在以往的面试过程中,我发现,关于注解的知识很多程序员都仅仅停留在使用的层面上,很少有人知道注解是如何实现的,更别提使用自定义注解来解决实际问题了。 但是其实,我觉得一个好的程序员的标准就是懂得如何优化自己的代码,那在代码优化上面,如何精简代码,去掉重复代码就是一个至关重要的话题,在这个话题领域,自定义注解绝对可以算得上是一个大大的功臣。 介绍几个,作者在开发中实际用到的几个例子,向你介绍下如何使用注解来提升你代码的逼格。 一、使用自定义注解做日志记录 不知道大家有没有遇到过类似的诉求,就是希望在一个方法的入口处或者出口处做统一的日志处理,比如记录一下入参、出参、记录下方法执行的时间等。 如果在每一个方法中自己写这样的代码的话,一方面会有很多代码重复,另外也容易被遗漏。 这种场景,就可以使用自定义注解+切面实现这个功能。 假设我们想要在一些web请求的方法上,记录下本次操作具体做了什么事情,比如新增了一条记录或者删除了一条记录等。 首先我们自定义一个注解: 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 /** * Operate Log 的自定义注解 */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface OpLog { /** * 业务类型,如新增、删除、修改 * * @return */ public OpType opType(); /** * 业务对象名称,如订单、库存、价格 * * @return */ public String opItem(); /** * 业务对象编号表达式,描述了如何获取订单号的表达式 * * @return */ public String opItemIdExpression(); } 因为我们不仅要在日志中记录本次操作了什么,还需要知道被操作的对象的具体的唯一性标识,如订单号信息。 ...

March 22, 2026 · 4 min · santu

为了防止接口被恶意调用,设计API秘钥方式提升接口安全性,并通过滑动窗口粗实现接口调用限流。

背景 在一个典型的业务场景中,我们提供了一个API ,被用来允许外部或内部客户端调用。这些 API 可能会暴露敏感数据或业务逻辑,因此需要确保只有授权的用户才能访问。同时,为了防止系统过载,需要对 API 调用进行限制。 技术选型 API 认证:使用 API 密钥或 OAuth 令牌。 API 限流:使用滑动窗口算法实现限流。 限流存储:使用 Redis 作为存储和计算滑动窗口的工具。 具体实现 API 认证 生成 API 密钥:为每个用户生成唯一的 API 密钥。当用户创建账户时,后端生成密钥并提供给用户。 客户端请求:客户端在发起请求时需在 HTTP 头部附带 API 密钥。 服务器端验证:服务器接收到请求后,提取并验证 API 密钥。如果密钥无效或缺失,请求将被拒绝。 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 import org.springframework.stereotype.Component; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Component public class ApiKeyAuthenticationFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) request; HttpServletResponse httpResponse = (HttpServletResponse) response; String apiKey = httpRequest.getHeader("X-API-KEY"); if (!isValidApiKey(apiKey)) { httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED); httpResponse.getWriter().write("Unauthorized"); return; } chain.doFilter(request, response); } private boolean isValidApiKey(String apiKey) { // 实现 API 密钥的验证逻辑 return true; } } API 限流 滑动窗口算法:使用 Redis 来存储和计算每个用户的请求计数。 请求计数:每个请求到达时,使用 Redis 记录该请求的时间戳。 窗口计算:检查当前时间窗口内的请求数量,如果超过阈值,则拒绝请求。 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 38 39 40 41 42 43 44 45 46 import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.time.Instant; import java.util.List; import java.util.concurrent.TimeUnit; @Component public class RateLimitingFilter implements Filter { @Autowired SlidingWindowRateLimiter limiter; private final RedisTemplate<String, String> redisTemplate; private static final int LIMIT = 100; // 设置每分钟的请求限制 private static final int WINDOW_SIZE_IN_SECONDS = 60; // 时间窗口大小 public RateLimitingFilter(RedisTemplate<String, String> redisTemplate) { this.redisTemplate = redisTemplate; } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) request; HttpServletResponse httpResponse = (HttpServletResponse) response; String apiKey = httpRequest.getHeader("X-API-KEY"); String key = "rate_limit:" + apiKey; long currentTime = Instant.now().getEpochSecond(); long windowStart = currentTime - WINDOW_SIZE_IN_SECONDS; boolean result = limiter.allowRequest(key); if (result) { httpResponse.setStatus(HttpServletResponse.SC_TOO_MANY_REQUESTS); httpResponse.getWriter().write("Too Many Requests"); return; } chain.doFilter(request, response); } } 这里面的SlidingWindowRateLimiter实现如下: ...

March 22, 2026 · 3 min · santu

基于本地消息表实现分布式事务保证最终一致性

背景 这种背景的话一般都是在分布式场景中,需要保证各个系统之间的数据的最终一致性,比如交易下单环节,保证订单系统和用户积分系统之间的最终一致。 也就是说,用户下单后,订单需要创建成功,用户积分也要增加成功,如果有一个失败了,这都是没满足一致性。 技术选型 保证分布式事务的方案有很多,比如本地消息表、MQ的事务消息、TCC、Seata等、2PC等 这些方案中各自都有优缺点,首先比较重的就是TCC、Seata和2PC,因为他们要么需要引入一个单独的协调者,要么需要代码做改造,要么对分布式系统之间有很强的侵入性。 比如TCC需要下游提供Try、Confirm和Cancel三种操作,2PC也是,需要把一个业务操作拆成2个阶段。 那么相对来说比较轻量级的方案就是依赖可靠消息,实现最终一致性。尤其是我们这个场景中,积分的增加其实不需要强一致性,只需要保证几秒钟之后积分增加成功就行,而且是一旦下单成功,积分增加必须成功,所以就比较适合使用可靠消息来保证最终一致性。 那么也就是说我们可以在创订单系统创建订单成功之后,发一个MQ消息,然后积分系统接收这个MQ消息即可。 1 2 3 4 5 6 7 @Transactional public void order(OrderDTO orderDTO){ orderServive.createOrder(orderDTO); mqService.send(orderDTO); } 但是这个方案存在一个问题,那就是第二步,发送消息其实是有可能失败的。那么就有以下几种情况: 1、消息发送失败,MQ没接到消息 这种情况,messageService.send(orderDTO);会抛异常,那么本地事务捕获到这个异常之后,把createOrder回滚了就行了 2、MQ接到了消息,但是客户端因为网络延迟以为失败了 这种情况比较复杂了,就是说客户端因为接到了失败的response,会直接回滚createOrder。但是MQ收到了消息之后,会投递给积分系统,积分系统会直接消费消息,然后增加积分 上面的第二种情况,就导致了数据不一致。 那么想要解决这个问题,要么就是用MQ的事务消息,要么就是引入本地消息表。因为不是所有的MQ都支持事务消息,所以这里我们选择本地消息表。 具体实现 在这个环节中,在订单服务的数据库中创建一张本地消息表 id long gmt_create datetime gmt_modified datetime message_type varchar biz_type varchar identifier varchar content text state varchar 这样表就用来记录本地消息的。这样我们就可以把以上代码做一下调整: 1 2 3 4 5 6 7 @Transactional public void order(OrderDTO orderDTO){ orderServive.createOrder(orderDTO); messageService.createMessage(orderDTO); } 这样我们就在一个事务中,创建两条数据库记录,因为加了事务,那么就可以保证,如果order创建成功,message也一定能写入成功。否则就都失败。 ...

March 22, 2026 · 1 min · santu

通过热点数据预热、多级缓存、异步化编程等方式解决热门数据接口耗时长问题

背景 在很多大型网站中,某些数据(比如热门文章、热点商品详情等)可能会成为热点,经常被大量用户请求。这可能导致数据库压力过大,进而使接口响应时间变长。为了应对这一问题,我们采取了一系列措施来减轻数据库负担,并提高接口的响应速度。 主要采用了有效的缓存策略和异步处理技术来减轻数据库负担并提高接口响应速度。 以下演示的是我们需要提供一个电商平台的商品详情接口,该接口需要执行以下操作: 获取商品的基本信息。 获取商品的用户评论。 获取推荐商品列表。 技术选型 这里面商品的基本信息和商品的评论信息我们可以适当的做一些数据预热。 数据预热基本上是要基于缓存的,缓存有很多种,本地缓存,分布式缓存等,这里我们选择了目前市面上比较常用的本地缓存+分布式缓存实现二级缓存。 本地缓存主要选择Caffeine,主要是因为Caffeine 提供了接近最优的性能,是当前 Java 中最快的缓存库之一。支持时间和大小驱逐策略,以及同步和异步加载等。同时也是Spring官方推荐的缓存框架。 分布式缓存采用的是主流的Redis。 异步化编程这里直接使用 Java 的 CompletableFuture 或 Spring 的 @Async 注解实现异步处理,以优化耗时操作。 具体实现 Caffeine 缓存配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Configuration public class CacheConfig { @Bean public CacheManager cacheManager() { CaffeineCacheManager cacheManager = new CaffeineCacheManager("dataCache"); cacheManager.setCaffeine(caffeineCacheBuilder()); return cacheManager; } Caffeine<Object, Object> caffeineCacheBuilder() { return Caffeine.newBuilder() .initialCapacity(100) .maximumSize(1000) .expireAfterWrite(10, TimeUnit.MINUTES) .weakKeys() .recordStats(); } } 二级缓存实现 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 @Service public class DataService { @Autowired private CacheManager caffeineCacheManager; @Autowired private StringRedisTemplate redisTemplate; public Object getData(String key) { // 尝试从 Caffeine 缓存获取 Cache caffeineCache = caffeineCacheManager.getCache("dataCache"); Object value = caffeineCache.getIfPresent(key); if (value == null) { // 尝试从 Redis 缓存获取 value = redisTemplate.opsForValue().get(key); if (value == null) { // 缓存未命中,从数据库加载 value = loadDataFromDB(key); redisTemplate.opsForValue().set(key, value); } caffeineCache.put(key, value); } return value; } private Object loadDataFromDB(String key){ //自己实现的从数据库中查询数据 } } 整个ProductService的实现: ...

March 22, 2026 · 4 min · santu

基于EasyExcel+线程池+批量插入实现百万级数据导入

这个问题是https://www.yuque.com/hollis666/ec96i7/pq601cwrcmznni0x 的衍生,内容基本上是来自这篇的,不是为了凑数,是很多人不知道这种其实可以当做项目亮点来用的,所以在这里单独再加一下。 背景 项目中有一个数据迁移,原来的数据存储在旧的系统,现在系统做了重构,需要迁移到新的系统中,老系统的数据被加工到Excel中了,需要基于Excel实现文件的导入,同时需要避免内存溢出以及性能太低的问题。 技术选型 在大文件的读取方面,EasyExcel更合适,因为他不会像POI一样耗内存,可以大大的减少内存占用。因为他并不会一次性把整个Excel都加载到内存中,而是逐行读取的。 同时考虑使用多线程来读取,这里就需要用到线程池的技术,直接用ExecutorService就行了。 因为还涉及到数据的批量写入,需要依赖mybatis或者mybatis-plus。 具体实现 直接参考下面这篇就行了,代码都有的: ✅如何实现百万级数据从Excel导入到数据库? 学习资料 ✅如何针对大Excel做文件读取? ✅什么是线程池,如何实现的? ✅基于EasyExcel+线程池解决POI文件导出时的内存溢出及超时问题

March 22, 2026 · 1 min · santu

基于EasyExcel+线程池解决POI文件导出时的内存溢出及超时问题

背景 在一个后台管理功能中,需要导出Excel,但是当处理大数据量的Excel文件导出时,常用的Apache POI库可能因其内存占用较高而导致内存溢出问题。同时,数据处理过程可能非常耗时,导致用户等待时间过长或请求超时。为解决这些问题,采用了基于 EasyExcel 和线程池的解决方案。 ✅POI导致内存溢出排查 技术选型 Excel的导出很多种方案,包括了POI、EasyExcel还有Hutool中也有类似的功能。在市面上,用的最多的还是POI和EasyExcel,而在处理大文件这方面,EasyExcel更加适合一些。 在文件导出过程中,用异步的方式进行,用户不需要在页面一直等待。异步文件生成之后,把文件上传到云存储中,再通知用户去下载即可。 这里云存储选择阿里云的OSS,线程池异步处理采用@Async 用户通知这里就是用Spring Mail进行邮件发送即可。 具体实现 入口是一个Controller,主要接收用户的文件导出请求。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @RestController @RequestMapping("/export") public class DataExportController { @Autowired private ExcelExportService exportService; @GetMapping("/data") public ResponseEntity<String> exportData() { List<DataModel> data = fetchData(); String fileUrl = exportService.exportDataAsync(data); return ResponseEntity.ok("导出任务开始,文件生成后会通知您下载链接"); } private List<DataModel> fetchData() { // 获取需要导出的数据 } } 这里做了一些简化,比如筛选条件、以及具体的获取数据部分我都省略了,大家可以根据自己的业务情况来实现。 ...

March 22, 2026 · 3 min · santu

利用雪花算法+Redis 自增 ID,实现唯一订单号生成

(本项目亮点来自我的数藏项目文档中的最佳实践部分,更多项目亮点难点(50+),更详细的落地方案和讲解,可以在项目课中和我们一起学) 在我的数藏项目中,我给大家定义了一个全局的 ID 生成器——DistributeID,其中定义了方法——generateWithSnowflake 他就是借助雪花算法生成唯一 ID 的,这个方法的声明如下: 1 2 3 4 5 6 7 8 /** * 利用雪花算法生成一个唯一ID */ public static String generateWithSnowflake(BusinessCode businessCode,long workerId, String externalId) { long id = IdUtil.getSnowflake(workerId).nextId(); return generate(businessCode, externalId, id); } 这里面需要三个参数: BusinessCode businessCode 主要是区分业务的,比如订单号、支付单号、优惠券单号等等,不同的业务定义一个不同的 BusinessCode long workerId 用于区分不同的 worker,这个 woker 其实就是一个机器实例,我们需要能保证不同的机器上的 workerId 不一样。 String externalId 这个就是一个业务单号,比如买家 ID,这个字段会用于基于基因法进行订单号生成,在项目文档中讲过了,我们不展开了。 本文主要介绍下这个workerId我们如何获取。 这个workerId需要满足几个条件: 1、每台机器都不一样 2、是个正整数 3、能满足动态扩展,新增机器的时候不需要单独配置 在我们的项目中,workerId 的获取我们是通过WorkerIdHolder实现的。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Component public class WorkerIdHolder implements CommandLineRunner { @Autowired private RedissonClient redissonClient; public static long WORKER_ID; @Override public void run(String... args) throws Exception { RAtomicLong atomicLong = redissonClient.getAtomicLong("workerId"); WORKER_ID = atomicLong.incrementAndGet() % 32; } } 这个类实现了CommandLineRunner接口,那么在 Spring 容器启动的过程中,run方法就会被调用。 ...

March 22, 2026 · 1 min · santu

基于Token校验避免订单重复提交

(本方案来自我的数藏项目,相关视频讲解及完整项目代码,在项目课中均有讲解) 在很多秒杀场景中,用户为了能下单成功,会频繁的点击下单按钮,这时候如果没有做好控制的话,就可能会给一个用户创建重复订单。 那么,我们如何防止这个问题呢? 其实有一个好办法,那就是用户在下单的时候,带一个 token 过来,我们校验这个 token 的有效性,如果 token 有效,则允许下单,如果无效,则不允许用户下单。 这里的 token 也不是 sa-token(单点登录框架) 发放的,而是我们自己实现的一个发放和存储,以及后续的校验,都是我们自己做的。 那么,这个 token 是如何发放和校验的的呢? token 的发放比较简单,我们定义一个 controller,在下单页面渲染的时候从接口中获取一下就行了。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 /** * @author hollis */ @Slf4j @RequiredArgsConstructor @RestController @RequestMapping("token") public class TokenController { private static final String TOKEN_PREFIX = "token:"; @Autowired private RedisTemplate redisTemplate; @GetMapping("/get") public Result<String> get(@NotBlank String scene) { if (StpUtil.isLogin()) { String token = UUID.randomUUID().toString(); redisTemplate.opsForValue().set(TOKEN_PREFIX + scene + CACHE_KEY_SEPARATOR + token, "token", 30, TimeUnit.MINUTES); return Result.success(TOKEN_PREFIX + scene + CACHE_KEY_SEPARATOR + token); } throw new AuthException(AuthErrorCode.USER_NOT_LOGIN); } } 以上,就是一个 token 获取的接口,通过用户传入的scene ,我们生产了一个 token 并把它存储在 redis 中。并返回给前端。 ...

March 22, 2026 · 3 min · santu

基于TTL 解决线程池中 ThreadLocal 线程无法共享的问题

在Java并发编程中,ThreadLocal是用来解决线程安全问题的一个很好的工具,它可以为每个线程提供变量的独立副本,从而避免了线程间的数据共享问题。 然而,在使用线程池的场景下,在父子线程间传递线程局部变量是无法实现的,因为ThreadLocal设计上仅支持线程内部的数据隔离,而不支持线程之间的数据传递。 背景 在基于Java的应用开发中,尤其是在使用Spring框架、异步处理和微服务架构的系统中,经常需要在不同线程或服务间传递用户会话、数据库事务或其他上下文信息。 比如一个Web服务处理用户请求的过程中,需要记录日志,其中日志需要包含请求的唯一标识(如请求ID)。这个请求ID在进入服务时生成,并在后续的所有处理流程中使用,包括多个子任务可能会并发执行或被分配到线程池中的不同线程上执行。(分布式场景中一般是traceId) 在这种情况下,使用ThreadLocal来存储请求ID会遇到问题:并发执行的子任务无法访问到存储在父线程ThreadLocal中的请求ID,以及使用线程池时,线程的复用会导致请求ID的错误共享或丢失。 技术选型 为了解决这个问题,可以使用TransmittableThreadLocal(TTL),它是阿里巴巴开源的一个工具库,设计用来解决在使用线程池等会复用线程的场景下,ThreadLocal无法正确管理线程上下文的问题。 开源地址:https://github.com/alibaba/transmittable-thread-local TransmittableThreadLocal继承自ThreadLocal,提供了跨线程的数据传递能力,能够确保父线程到子线程的值传递,同时支持线程池等场景下线程间的数据隔离。 另外,还有一个JDK自带的InheritableThreadLocal,他是用于主子线程之间参数传递的,但是,这种方式有一个问题,那就是必须要是在主线程中手动创建的子线程才可以,而在线程池中,InheritableThreadLocal就不行了。 具体实现 引入依赖 首先,需要在项目中引入TransmittableThreadLocal的依赖。如果是Maven项目,可以添加如下依赖: 1 2 3 4 5 <dependency> <groupId>com.alibaba</groupId> <artifactId>transmittable-thread-local</artifactId> <version><!-- 使用最新版本 --></version> </dependency> 使用TransmittableThreadLocal存储请求ID 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class RequestContext { // 使用TransmittableThreadLocal来存储请求ID private static final ThreadLocal<String> requestIdTL = new TransmittableThreadLocal<>(); public static void setRequestId(String requestId) { requestIdTL.set(requestId); } public static String getRequestId() { return requestIdTL.get(); } public static void clear() { requestIdTL.remove(); } } **创建一个线程池,并使用TTL提供的工具类确保线程池兼容**TransmittableThreadLocal: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 import com.alibaba.ttl.threadpool.TtlExecutors; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ThreadPoolUtil { private static final ExecutorService pool = Executors.newFixedThreadPool(10); // 使用TtlExecutors工具类包装原始的线程池,使其兼容TransmittableThreadLocal public static final ExecutorService ttlExecutorService = TtlExecutors.getTtlExecutorService(pool); public static ExecutorService getExecutorService() { return ttlExecutorService; } } TtlExecutors是TransmittableThreadLocal(TTL)库中的一个工具类,它提供了一种机制来包装Java标准库中的ExecutorService,ScheduledExecutorService等线程池接口的实例。 ...

March 22, 2026 · 2 min · santu

留言给博主