基于 bitset 实现高效的商品预约

(本项目亮点来自我的数藏项目文档中的最佳实践部分,更多项目亮点难点(50+),更详细的落地方案和讲解,可以在项目课中和我们一起学) 针对一些热点商品,我们提供了预约功能,就像天猫超市卖茅台一样,需要提前预约才能购买,不约不能买。 同一个商品,我们想要记录哪些用户预约过,同时需要能够快速查询,并且可以尽可能的减少存储空间,避免浪费,我们选择了使用 BitSet。 为了快速的存取,我们使用 Redis,正好Redis 也支持 bitSet 数据结构,同时,我们为了避免Redis 挂了,我们也要存储一张预约表, 在数据库中做持久化。 Redis 的 BitSet 是一种特殊的字符串类型,它允许你对字符串中的每一位(bit)进行操作。每个位可以是 0 或 1,因此你可以将 BitSet 视为一个非常高效的布尔数组。使用 Redis BitSet 的好处 节省内存 每个位只占用 1 位(bit),相比于布尔数组或整数数组,BitSet 可以显著减少内存占用。这对于存储大量布尔值(如用户是否预约了某个商品)非常有用。 高效的操作 Redis 提供了丰富的命令来操作 BitSet,例如 SETBIT、GETBIT、BITCOUNT 等。这些命令可以高效地设置、获取和统计位的状态。 原子性操作 Redis 的 BitSet 操作是原子性的,这意味着多个客户端可以同时对同一个 BitSet 进行操作而不会产生竞争条件。这在高并发场景下非常重要。 因为我们的用户 ID 是一定不重复的,并且可以转换成integer,所以没一个用户可以映射到一个唯一的 bit 上面,这样预约过的用户对应的 bit 设置为1 ,没预约过的默认为0,就能实现存储预约信息了。 那么我们就可以把商品 id 作为 key,存储一个 bitset,bitset 中存储的是已经预约过的用户的id 列表。 具体实现方式如下: 1 2 3 4 // 因为用户id都是不重复的,并且可以转换成integer,所以这里可以使用BitSet来存储预约信息,减少存储量 RBitSet bookedUsers = redissonClient.getBitSet(BOOK_KEY + request.getGoodsType() + CacheConstant.CACHE_KEY_SEPARATOR + request.getGoodsId()); // 不报错则成功 bookedUsers.set(Integer.parseInt(request.getBuyerId())); 就这样,就可以把一个预约的信息保存下来了。 ...

March 22, 2026 · 1 min · santu

基于Redis的zset实现秒级排行榜

背景 假设我们有一个在线游戏平台,需要为每个游戏的玩家实现排行榜功能,以显示每个游戏的最高得分排名。 技术选型 在这个场景中,主要就是实现排行榜,对于排行榜,有很多种。 有周榜、日榜、小时榜以及分钟榜等,不同的排行榜可以采用不同的技术方案实现。 对于周榜、日榜、小时榜等,完全可以基于定时任务、离线任务等生成,直接统计数据然后做排序就行了,难度不大。 难度比较大的就是分钟级或者秒级的榜单的实现。 对于这种榜单,比较好的办法就是脱离数据库,直接用redis来实现,因为数据库的话大量数据的order by会性能很差。 借助redis的zset我们可以非常方便的实现排行榜的功能。 你做了什么 zset允许我们将每个玩家的得分作为score存储,并使用玩家的唯一标识符作为成员,利用zset自身的基于score排序的功能,我们就能很快实现这个排行榜的功能。 借助Redis,我们可以实现以下几个方法: 1、新用户加入排名: 1 2 3 4 // 新用户加入排名 private static void joinLeaderboard(Jedis jedis, String user, double score) { jedis.zadd("game:1_ranking", score, user); } zadd 命令用于将一个或多个成员元素及其分数值加入到有序集当中。 如果某个成员已经是有序集的成员,那么更新这个成员的分数值,并通过重新插入这个成员元素,来保证该成员在正确的位置上。 分数值可以是整数值或双精度浮点数 如果有序集合 key 不存在,则创建一个空的有序集并执行 ZADD 操作。 2、用户积分增加: 1 2 3 4 // 用户积分增加 private static void increaseUserScore(Jedis jedis, String user, double score) { jedis.zincrby("game:1_ranking", score, user); } zincrby 命令对有序集合中指定成员的分数加上增量 ...

March 22, 2026 · 1 min · santu

基于Redis的分布式锁,解决短信验证码重复发放等问题

背景 项目中,需要实现一个通过短信验证码进行注册的场景,为了这个过程中1分钟只能发放一次验证码,并且验证码发放后可以验证一次的功能,使用了Redis进行存储。 主要是为了避免盗刷,避免资金浪费。 技术选型 验证码生成 验证码的生成,代码中自己可以简单实现一个验证码,发送四位的字母+数字组合即可,这个场景对于全局唯一性要求并不高。也可以用一些开源的验证码生成的框架。 短信发送 短信验证码的发放,在阿里云上有很多服务,可以实现短信发送。 https://www.aliyun.com/search?spm=5176.21213303.J_6704733920.6.34b853c98er6CO&k=%E7%9F%AD%E4%BF%A1%E9%AA%8C%E8%AF%81%E7%A0%81&scene=market&page=1 对接都很简单,只需要通过HTTP接口调用即可实现短信发送。 验证码存储&验证 Redis是一个分布式的缓存,可以把验证码信息临时放到Redis中,借助redis的超时自动删除功能,可以实现5分钟内才能进行验证码的校验的功能。 防止重复发送 为了防止重复发送,可以在发送验证码的地方,增加一个分布式锁,保证一条消息只会被发送一次。 为什么要加锁?因为验证码发失败之后,有扫表任务会重新扫出待发送短信进行重试,这个过程中避免短信重新发送,所以对同一个记录加锁。(当然,这不步不做影响也不大,主要是因为短信需要花钱,所以就拦一下) 你做了什么 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public void sendSmsVerificationCode(@NotBlank String mobile, @NotBlank String type) { String smsSended = redisTemplate.opsForValue().get(getLimitKey(mobile, type)); if (StrUtil.isNotBlank(smsSended)) { throw new BizException("请勿频繁发送验证码"); } //记录手机号发送过验证码,记录1分钟 redisTemplate.opsForValue().set(getLimitKey(mobile, type), "1", 60, TimeUnit.SECONDS); //使用hutool的工具生成四位随机验证码 String verificationCode = RandomUtil.randomStringUpper(4); //把验证码存在Redis中 redisTemplate.opsForValue().set(type + mobile, verificationCode, 60 * 5, TimeUnit.SECONDS); //构建一条发送记录 SmsSendRecord smsSendRecord = new SmsSendRecord(mobile, type, verificationCode); //落库保存 smsSendRecordRepo.save(smsSendRecord); //发送短信验证码 smsService.sendSms(smsSendRecord); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Service public class SmsService{ @Autowired RedissonClient redisson; public void sendSms(SmsSendRecord smsSendRecord) { //加分布式锁 RLock lock = redisson.getLock("sendSms" + smsSendRecord.getId()); try { lock.lock(); //短信发送逻辑 } finally { //解锁 lock.unlock(); } } } 以上,就是一段简单的,短信验证码发送的功能 ...

March 22, 2026 · 1 min · santu

基于XXL-JOB的分片实现分库分表后的扫表

(本项目亮点来自我的数藏项目文档中的最佳实践部分,更多项目亮点难点(50+),更详细的落地方案和讲解,可以在项目课中和我们一起学) 在我的数藏项目中,需要通过定时任务来扫描订单表,进行超时未支付的订单的关单操作。 但是因为我们的表是做了分库分表的,并且数据量会很大,单机任务扫表的性能可能不能满足要求,会比较慢,导致任务堆积。 为了解决这个问题,我们采用多线程扫表的方案,这里基于 XXL-JOB 的分片功能,充分利用集群中的所有实例进行任务处理,并且在每个任务的执行过程中,采用生产者消费者模式,在基于线程池进行快速消费。本文介绍 XXL-JOB 的分片任务。(整体方案见数藏项目) XXL-JOB 的分片任务 在XXL-JOB中,分片任务指的是将一个大任务分解为多个小任务,这些小任务可以分布在不同的执行器上并行执行。这种方式可以有效地利用集群资源,加快任务执行速度,特别适用于数据量大的批处理任务。 在 XXL-JOB 的配置时,路由策略这里可以选择分片广播的方式进行调度。 在配置了这种调度方式后,任务调度的时候就可以同时调度多个实例进行处理。XXL-JOB 会给每个实例传递两个分片参数: 分片总数(Sharding Total):表示任务被分成多少片。例如,如果设置为3,则表示任务被分为3片。其实就是你集群中服务器的数量。 分片序号(Sharding Index):表示当前执行器执行的是哪一片。序号从0开始,最大值为分片总数减1。 在任务执行的代码中,可以通过调用XxlJobHelper.getShardIndex()和XxlJobHelper.getShardTotal()方法获取当前执行器的分片序号和总分片数。 然后就可以根据获取到的分片信息,编写相应的逻辑来确定该执行器需要处理的数据范围或任务。 如: 1 select * from table where user_id % 4 = 1 以上 SQL 只是举个例子,让你更容易理解,实际使用的时候,会考虑索引的使用。但是思想就是这样的。 如我们的超时任务执行器中,就通过XxlJobHelper取出了shardIndex和shardTotal,然后再把根据用户 ID 的末尾2位对shardTotal进行取模。如果取模的结果和shardIndex相等,则执行任务,不相等的,则交给其他分片执行即可。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @XxlJob("orderTimeOutExecute") @Deprecated public ReturnT<String> orderTimeOutExecute() { try { int shardIndex = XxlJobHelper.getShardIndex(); int shardTotal = XxlJobHelper.getShardTotal(); LOG.info("orderTimeOutExecute start to execute , shardIndex is {} , shardTotal is {}", shardIndex, shardTotal); List<String> buyerIdTailNumberList = new ArrayList<>(); for (int i = 0; i < 99; i++) { if (i % shardTotal == shardIndex) { buyerIdTailNumberList.add(StringUtils.leftPad(String.valueOf(i), 2, "0")); } } } } 举个例子,如果一共5台实例,那么每台机器的shardTotal都是5,然后他们的shardIndex是从0到4的。这样接收到shardIndex=0的这台机器,就会只处理 userId 的尾号 % 5 == 0的所有用户数据,而接收到shardIndex=1的这台机器,就会只处理 userId 的尾号 % 5 == 1的所有用户数据。。。 ...

March 22, 2026 · 1 min · santu

基于状态机+乐观锁解决订单支付和关单的并发问题

(本项目亮点来自我的数藏项目文档中的最佳实践部分,更多项目亮点难点(50+),更详细的落地方案和讲解,可以在项目课中和我们一起学) 假如,有一笔订单,在10:00下单成功,超时时间是30分钟,那么在10:30点的时候,支付成功和关单同时来了,这时候该如何处理? 我们的做法是基于状态机+乐观锁来确保只有一个线程能成功,后面的线程则执行失败。 明确终态 这是一个比较常见的一个并发处理的问题,而且也是业务中比较常见的问题,我们的订单的状态机如下: 在我们的订单状态机的控制中,可以看到 CONFIRM 的状态,只能推进到 PAID 或者 CLOSED。 那么也就是说,如果一个订单当前已经是PAID 了,就不能再 CLOSED 了,反之亦然,也就是说,不管是支付成功、还是取消(超时关单)哪个先执行了,另外一个动作都会执行失败,会被我们的状态机给限制住。 关于订单的状态机的使用,见:✅统一状态机设计(项目课文档) 有了这个之后,就可以完全避免并发吗?并不能,因为在极端的并发情况下,因为状态机是在从数据库查询出来数据之后做的判断,那么就有可能支付成功的线程和关单的线程从数据库查询的时候都是 CONFIRM,这时候状态机判断就可以通过。 那么这个问题如何解决呢?就靠我们的乐观锁了。 在我们的orderMapper.updateByOrderId方法中,是加了乐观锁的: 也就是说,虽然两个线程查询到的状态都是 CONFIRM,并且 lock_version 都是1的话,在最终更新的时候,因为做了乐观锁的判断,只会有一个线程能执行成功,因为他执行成功后会把 lock_version 改成2,另一个线程用lock_version=1当作 where 条件更新就会失败了。 就这样,就避免了并发。那也就是说一旦发生了并发,就会有一个成功了,就有一个会失败。这是必然的,这时候就有两种情况了: 1、支付成功处理成功,关单处理失败 2、关闭处理成功,支付成功处理失败 这两种情况如何处理呢? 逆向流程 先说简单的情况,假如支付成功处理成功,关闭处理失败,这种其实没啥问题,因为已经支付成功了,超时的请求直接拒绝掉就行了。这是业务上正常的逻辑。 第二种情况就不好处理了,因为对于支付超时处理成功了,但是支付成功处理失败这种,我们就需要考虑,钱怎么办? 用户把钱付完了,但是支付却没成功,这肯定是业务上接受不了的。那这时候怎么办呢? 办法就是:原路退回 当出现这种情况的时候,我们是可以识别出来的,也就是说在支付成功的处理过程中,如果发现支付单被关闭了,那么就触发原路退回的流程,把钱再给用户退回去。 为啥非要退款?而不是让订单推进到成功,或者再补一个支付单。 一方面,状态机中已取消一定是一个终态,终态再流转到其他状态不合理。 另一方面,在订单超时的业务逻辑中,可能直接把库存退回去了,营销券也释放了,那么这时候补一个支付单是不现实的。 实现逻辑如下: 这里的needChargeBack就是判断是否需要退款的,退款判断如下: 也就是说,如果orderFacadeService.pay返回的 responseCode 如果是ORDER_ALREADY_PAID或者ORDER_ALREADY_CLOSED就会执行退款。 ORDER_ALREADY_PAID是多付的情况,我们这里不讨论,详见:✅重复支付问题如何解决?(项目课文档) ORDER_ALREADY_CLOSED是什么情况才会有的呢,那么深入orderFacadeService.pay去看: 就是这里,当orderService.pay(request);失败之后,反查一下订单,如果状态是已经关闭了,则返回ORDER_ALREADY_CLOSED。 那么我们上面提到的,状态机校验不过,和更新失败(结果数为0)的时候,都会返回失败的。就会走到我们说的逻辑了。 (本项目亮点来自我的数藏项目文档中的最佳实践部分,更多项目亮点难点(50+),更详细的落地方案和讲解,可以在项目课中和我们一起学)

March 22, 2026 · 1 min · santu

你的项目有哪些难点&亮点?

这个问问题,在面试的时候经常被问到,很多人都不知道该如何回答,当我们在写简历上的项目时,可以从以下几个方面挖掘亮点和难点: 亮点 性能优化:描述你如何优化系统性能,比如缓存机制、数据库查询优化、异步处理等。 安全措施:实现了哪些安全特性?比如数据加密、防SQL注入、XSS攻击防御、水平权限漏洞解决等。 代码质量和维护:实现了代码的高可维护性和可读性,比如通过单元测试、代码复用、清晰的模块化、设计模式等。 用户体验优化:如减少页面加载时间、提高响应速度等。 创新技术的应用:比如使用了最新的框架、库或者算法。 难点 高并发处理:电商平台常面临高并发的挑战,描述你是如何解决这一问题的。 数据一致性:在分布式系统中保持数据一致性的策略和实现。 线上问题解决:解决了哪些复杂的bug或线上问题,以及采取的方法。 资源优化:在有限的资源下如何优化系统性能。 动态需求适应:项目需求变更频繁,如何快速适应并实施。比如配置中心、插件化等,或者平台化、组件化 技术选型挑战:选择合适技术栈的挑战和解决过程。 那么,在这个目录下,我会开始增加一些我整理的可以在简历&面试中作为技术难点、项目亮点的东西。大家可以把这些亮点&难点往自己的项目当中套用即可。

March 22, 2026 · 1 min · santu

引入分布式锁解决并发问题

背景 我的项目中,有一个场景会出现并发问题,就是我们有一个支付单,这个支付单会有到期的自动关闭,以及用户也可以主动取消订单进行关闭。 这两个操作,有的时候就会出现并发的问题,导致重复关单,重复发消息给下游,导致下游处理失败。 为了解决这个并发的问题,我在项目中采用了分布式锁来解决。 技术选型 其实分布式锁有很多种方案,无非就是借助一个第三方的系统,来做互斥性的协调。那么市面上主流的实现方式就是基于数据库、zookeeper以及redis。 我在选型的时候分别了解了一下这几个方案。 其实最简单的肯定是数据库的方式,在数据库中建一张表,加锁时创建一条记录,解锁时再把记录删除,通过唯一性约束来避免重复的记录。当要加锁的时候,判断下数据库中是否已经有这条记录了,没有就插入一条。 这个方案的话好处就是实现简单,缺点也比较明显,就是太依赖数据库,在高并发情况下,可能会对数据库造成压力,并且性能也不太好,而且也会占用很多数据库链接,影响正常的业务。 基于数据库的悲观锁当然也可以实现,但是悲观锁的机制,可能会导致锁表,也会导致线程阻塞的问题,所以这个方案我也放弃了。 另外在zookeeper和redis中,我选择了redis,因为redis我们现在系统中已经在用了,但是zk的话我们没有引入,并且搭建一个zk集群成本也挺高的。 你做了什么 在redis中呢,我们最开始使用了setnx的方案,但是后来经常会出现并发问题,后来发现是因为有的时候,我们设置的解锁的超时时间太短了,导致锁提前释放了。但是拉长超时时间又会使得接口的吞吐量降低,后来我发现redisson其实他实现了看门狗的机制,可以帮助我们做自动续期,我就选了redisson的方案。 但是,其实现在我们这个方案也存在一定的问题,那就是在redis的主节点如果出现问题的情况下,可能会导致锁丢失,但是因为我们这个目前业务量还没有那么大,所以暂时还没有遇到这个问题,不过我也了解过,其实可以基于redlock来解决这个问题,redlock可以借助集群的投票机制,超过半数以上写入成功才算加锁成功,这样可解决单点故障的问题。后面会考虑优化一下。 得到的结果 解决了并发的问题 学习资料 ✅分布式锁有几种实现方式? ✅如何用SETNX实现分布式锁? ✅什么是RedLock,他解决了什么问题? 如何用Redisson实现分布式锁? ✅如何用Zookeeper实现分布式锁?

March 22, 2026 · 1 min · santu

使用CompletableFuture完成并发编排,提升接口性能

背景 我的项目中,有这样一个场景,我们要发起一个用户主动还款之后,需要把在途的扣款单全部暂停,但是因为用户选择还款单据可能比较多,而且暂停是调用外部资金打款系统的接口实现的,这个地方如果一条一条执行就很慢,但是如果起多线程执行的话,我就没办法知道他们每一个暂定的返回结果。 我想可以实现一个这样的功能:多线程去执行暂停动作,如果都成功了,那么就推进我的主动扣款后续流程,如果暂停有任何一个接口调用失败了,那么先不推进后续流程,等下次重试。 技术选型 为了实现这个功能,我了解到CompletableFuture,他是Java 8中引入的一个新特性,它提供了一种简单的方法来实现异步编程和任务组合。 他的多线程编排的能力刚好可以用在我这个场景中,而且他底层是基于ForkJoinPool实现的,所以他的性能也比较高效,所以最终我选择了这个方案。 具体实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 //异步暂停扣款 CompletableFuture<Void> allFutures = CompletableFuture.allOf(noticeDetails.stream() .map(detail -> CompletableFuture.supplyAsync(() -> { pause(detail); return null; })).toArray(CompletableFuture[]::new)); //所有暂停扣款成功后,更新代还通知单 allFutures.whenComplete((v, e) -> { if (e == null) { //执行后续的还款操作 //... //... } else { log.error("notice failed", e); } }); 得到结果 在用了CompletableFuture做编排之前,原来50笔订单的暂停扣款,需要大概10s左右,但是用了CompletableFuture之后,50笔订单的暂停扣款只需要1秒钟左右。 ...

March 22, 2026 · 1 min · santu

基于Spring Event,实现同步转异步,解决定时任务扫表导致数据库连接池不够的问题

背景 有这样一个业务场景,作为一个金融产品,很多用户会在借款后发生逾期,每一笔逾期都是一笔单独的借据,在贷后催收环节中,需要基于用户维度做聚合,把多笔借据合并成一个案件进行统一的催收。 那么就需要一个把多笔借据合并成一个案件的操作。最开始采用的方案就是定时任务扫表,每天早上凌晨5-8点之间进行定时任务扫表,然后进行案件的合并。 但是随着业务量的增多,扫表经常会扫不完,于是业务上为了提效,把定时任务改为分布式任务,借助多实例进行批量扫表。 但是这样做就导致数据库扛不住了,数据库的连接池经常在跑任务的时候被打满。于是就需要想办法解决这个问题。 技术选型 关于这个问题,有挺多方案的。 首先就是可以选择分库分表,把原来的单独分成多个库,这样整体的连接数就多了,也就可以扛得住并发扫表了。但是这个方案比较重,分表后也会带来一系列问题。 于是考虑了另外一种方案,那就是基于"同步转异步"的思想,在借据生成的时候,就进行合并,而不是定时任务批量合并。 这样就可以把集中地流量分散到每一条借据生成的过程中,而且这个过程允许失败,一旦失败了,通过定时任务补偿即可。 但是这么做就会导致借据生成这部分逻辑很复杂,需要考虑到合并案件的事情,耦合性太深了。于是就基于Spring Event,把借据生成和案件合并进行解耦。 所以,整体方案就是基于Spring Event,实现同步转异步,解决定时任务扫表导致数据库连接池不够的问题。 在方案改造前,每次扫表需要处理的数据量有20万条,改造后,只需要1000左右的数据量需要扫表处理,大大提升系统的可用性。 你做了什么 在借据生成的方法中,增加一个事件发送: 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 protected BaseManageResponse genenrateLoan(LoanGenerateEvent loanGenerateEvent) { BaseManageResponse manageResponse = new BaseManageResponse(); try { //开启事务 return transactionTemplate.execute(transactionStatus -> { //核心逻辑执行 doGenerateLoan(loanGenerateEvent); //发送一个案件入催完成的事件 try { applicationContext.publishEvent(new CaseStartFinishEvent(loanGenerateEven)); } catch (Exception e) { LOG.warn("publishLoanGenerateEventEvent failed", e); } //结果返回 return manageResponse.successResponse(caseModel); }); } catch (Exception e) { LoanGenerateStream existStream = queryExistStream(request); if (existStream != null) { return manageResponse.duplicatedResponse(existStream); } throw e; } } 这里在applicationContext.publishEvent(new CaseStartFinishEvent(caseModel.getCaseItem()));中发送一个事件,并且用try-catch包上,一旦失败了,不影响主流程。 ...

March 22, 2026 · 2 min · santu

通过采用“一锁二判三更新”方式设计接口幂等,解决支付单重复支付的问题

背景 负责了一个支付相关的功能,因为我们的支付渠道有很多,有的时候用户在选择某一个支付渠道支付(如微信)之后一直处理中,然后用户又换了一个其他的支付渠道(如支付宝)来支付。但是后面出现了两个支付都成功了的问题。但是我们的系统上没有针对这种情况做特殊处理,就导致用户多付了钱。 技术选型 关于支付的并发问题,其实有几种方案,最简单的就是用户在某个支付渠道支付中的时候,不让他用其他的渠道再次支付,直到上一个渠道支付结果明确的成功或者失败。 这时候就需要给订单加锁,最开始我们就是这么做的。实现方式就是在每一个订单上,我们加了一个payStatus,payChannel和payStreamNo三个字段,当用户用某个渠道开始支付的时候: payStatus = PAYING payChannel = WECHAT payStreanNo = NULL 这时候,如果微信支付的结果还没有返回的话,那么用户想要用支付宝再次针对这笔订单支付的时候会失败,因为payStatus = PAYING并且payChannel != ALIPAY ,所以会提示支付中。 一直到微信返回结果之后,我们再根据支付结果尝试继续处理,支付成功: payStatus = PAID payChannel = WECHAT payStreanNo = 02312321321321423123 支付失败 payStatus = FAILED payChannel = NULL payStreanNo = NULL 但是,上面这个整体逻辑还存在一个问题,那就是当我们有并发请求,可能有两笔支付请求在检查payStatus的时候,发现都是INIT或者FAILED,那么这两个请求就都可以唤起支付渠道。 这个情况,在支付的时候比较少,因为支付都是用户自己操作的,问题不大,但是有一些场景可能会存在,比如说有那种花呗还款的场景,这时候就可能会有用户的主动还款,和系统的自动扣款。 那么这时候就可能会发生上述的并发情况,那么就需要想办法解决这个问题。 你做了什么 这个地方其实主要就是并发情况下的一个幂等问题,所以为了解决并发的问题,我这里引入了一个分布式锁,在开始进行支付操作时,尝试添加分布式锁,加锁成功,再去判断payStatus。如果加锁失败,说明当前有并发请求,那么就失败掉。 伪代码如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 //一锁:先加一个分布式锁 @DistributeLock(scene = "PAY", keyExpression = "#request.orderId", expire = 3000) public PayResponse apply(PayRequest request) { PayResponse response = new PayResponse(); //二判:判断请求是否执行成功过 OrderDTO orderDTO = orderService.queryOrder(request.getProduct(), request.getIdentifier()); if (orderDTO != null && orderDTO.getPayStatus() == PayStatus.PAYING) { response.setSuccess(false); response.setResponseCode("PAYING"); return response; } //三更新:执行更新的业务逻辑 return orderService.applyPay(request); } 学习资料 ✅如何解决接口幂等的问题? ...

March 22, 2026 · 1 min · santu

留言给博主