Redis分布式锁实战:解决秒杀系统高并发下库存防超卖
简介这是一份基于Spring Boot与Redis的分布式锁抢单秒杀实战工程面向Java后端开发者尤其适合希望掌握高并发下单、库存防超卖与公平抢单逻辑的中级读者也可用于相关课程设计或毕业设计的参考。压缩包共13个文件整体仅59KB其中5个Java源文件覆盖了分布式锁获取、库存原子扣减、订单结果返回等核心实现2个properties文件分别用于配置Redis连接与业务参数并附有Maven构建文件、说明文档以及项目忽略规则导入IDE后能快速看清各模块职责并运行调试。目前已有4056人学习代码量精简而结构完整。项目基于真实秒杀场景整理演示了Redis分布式锁的关键写法、利用原子命令避免超卖、通过队列机制削峰填谷同时考虑了锁自动释放、请求幂等性、自定义返回状态等生产细节理解这套实现后可以方便地扩展为完整的秒杀订单服务或移植到其他需要并发控制的后端业务中。1. 用Redis分布式锁给秒杀系统“止痒”一个抢单接口的演进过程我去年接手过一个库存秒杀模块上线当天就被流量打穿Tomcat线程池堵满、库存扣成负数、用户拿着“支付成功”的订单来找客服。后来把核心逻辑从synchronized换成Redis分布式锁再配合队列削峰才把峰值下单的失败率从18%压到2%以内。这套方案没有引入重量级中间件纯粹基于SpringBoot Redis实现可以说是秒杀抢单场景里成本最低、见效最快的“后悔药”。这套资源适合两类人一类是刚接触高并发、想把“抢单接口”从单机锁升级成分布式锁的初级开发者另一类是手里已有秒杀项目、但被超卖、锁失效、重复下单折磨过的“老兵”。下面我会从目录结构讲起逐段拆核心代码最后给出二次开发的改造路线。2. 项目拆解从Controller到Redis的完整下单链路2.1 模块清单与目录结构整个工程是标准的SpringBoot项目解压后核心代码放在src/main/java下包名com.demo.seckill。如果你拿到源码建议先按这个结构建立全局认知src/main/java/com/demo/seckill ├── controller │ └── SeckillController.java # 入口抢单接口、库存查询接口 ├── service │ ├── SeckillService.java # 核心业务扣库存、创建订单 │ ├── SeckillServiceImpl.java │ └── OrderConsumer.java # 队列消费者串行处理订单 ├── util │ ├── RedisLockUtil.java # 分布式锁工具类 │ └── LuaScript.java # Lua脚本常量 ├── config │ └── RedisConfig.java # Redis序列化、连接池配置 └── entity ├── SeckillGoods.java # 商品库存实体 └── SeckillOrder.java # 订单实体这个资源不包含前端页面也没有复杂的微服务拆分所有代码就这几百行但你真正要读的不是代码量而是“Redis锁在哪个环节介入、怎么介入”。我一般拿到项目先看util包因为锁的实现决定了整个系统的上限。2.2 数据模型库存、订单、消息表怎么设计秒杀场景的数据表设计核心是“库存独立成表订单状态有限”。资源里用两张核心表seckill_goods存商品库存与秒杀时间窗口seckill_order存用户订单。关键字段说明字段所属表作用说明stockseckill_goods剩余库存扣减时必须在SQL条件里带stock 0start_time / end_timeseckill_goods秒杀时间窗控制接口是否可抢user_id goods_idseckill_order唯一索引防止同一用户重复下单statusseckill_order订单状态0待支付、1已支付、2已取消设计上有两个细节容易被忽略一是订单表必须加(user_id, goods_id)唯一索引这是数据库层面的“最后一道锁”二是库存表不用version乐观锁字段因为Redis锁已经承担了并发控制再叠一层乐观锁属于重复建设。这个取舍在后面的踩坑章节里会细说。2.3 接口设计URL、参数、返回值资源里有3个接口全部走Restful风格返回统一的Result对象POST /api/seckill/doSeckill 参数goodsId (商品ID)、userId (用户ID) 返回code200 表示抢单成功code500 表示库存不足或已抢过GET /api/seckill/stock 参数goodsId 返回剩余库存量用于前端倒计时展示POST /api/seckill/test 参数无 说明内置的多线程测试入口用CountDownLatch模拟1000并发抢购这里要解释一个常见误区接口参数里没有token、没有sign这不代表资源缺少安全设计而是秒杀系统通常把防刷、鉴权放在网关层比如Nginx限流、参数签名业务接口只关心“这个用户是不是真的能买”。如果你要在生产环境用第一步就是把userId从“前端传参”改成“登录态解析”否则任何人都能伪造用户ID刷单。3. 核心模块实现Lua脚本加锁、执行业务、释放锁3.1 分布式锁的正确姿势setnx expire 唯一value先明确一个观点秒杀抢单里的分布式锁不是在synchronized里包一个Redis操作而是“锁住整个判断库存扣减下单”的临界区。很多初版实现会写成这样错误示范// 错误写法只锁了扣库存这一行锁形同虚设 if (redisTemplate.opsForValue().setIfAbsent(lockKey, 1)) { int stock goodsMapper.getStock(goodsId); if (stock 0) { goodsMapper.deductStock(goodsId); } redisTemplate.delete(lockKey); }这段代码有三个问题锁没有过期时间拿到锁的线程一旦OOM或GC停顿锁永远不释放后面的请求全部阻塞加锁和执行业务不是原子的极端情况下一个线程刚set成功还没执行扣库存锁就被另一个线程覆盖释放锁时没有校验“锁是不是自己的”可能把自己刚获得的锁删掉。资源里正解是加锁用SET key value NX PX 30000value必须存一个唯一标识比如UUID释放锁时先比较再删除。Redis从2.6.12版本开始支持NX PX组合这是比setnxexpire两条命令更稳妥的做法省了一次网络往返也从根本上消除了“加锁成功但设置过期时间失败”的隐患。3.2 用Lua保证抢锁和扣库存的原子性只解决锁本身还不够。看调用链拿到锁之后要先查库存、再扣减、再生成订单这三步加起来要多次访问Redis和MySQL。如果拿到锁的线程在“查库存”和“扣减”之间挂了锁会因过期自动释放但库存也没扣——这还能接受。真正的问题是反向扣减成功、订单生成失败库存已经没了用户却买不到只能靠对账补偿。资源的思路是“能在一个原子操作里完成的绝不放多个命令”所以把“锁检查 库存扣减”合并成一个Lua脚本// LuaScript.java public class LuaScript { /** * 扣库存脚本 * KEYS[1] 商品库存的Redis key * ARGV[1] 扣减数量 * 返回1表示扣减成功0表示库存不足 */ public static final String DEDUCT_STOCK if redis.call(exists, KEYS[1]) 1 then local stock tonumber(redis.call(get, KEYS[1])); if stock tonumber(ARGV[1]) then redis.call(decrby, KEYS[1], ARGV[1]); return 1; end; return 0; end; return -1;; }这段Lua脚本有三个分支-1代表key不存在商品还没初始化0代表库存不足1代表扣减成功。为什么要把库存放在Redis而不是直接读MySQL因为秒杀场景是“读多写少”的极端特例1000个并发同时读MySQL的行锁开销远高于Redis的内存操作。调用处使用DefaultRedisScript执行整个过程被Redis的脚本机制保证原子性——Lua脚本在执行期间不会被其他Redis命令插入// SeckillServiceImpl.java 核心方法 public Result doSeckill(String goodsId, String userId) { // 1. 校验秒杀时间窗口 SeckillGoods goods goodsMapper.selectByGoodsId(goodsId); if (goods.getStartTime().after(new Date()) || goods.getEndTime().before(new Date())) { return Result.error(不在秒杀时间段内); } // 2. 分布式锁锁的粒度是“商品用户”防止同一用户重复抢 String lockKey seckill:lock: goodsId; String clientId UUID.randomUUID().toString(); Boolean locked redisTemplate.opsForValue() .setIfAbsent(lockKey, clientId, Duration.ofSeconds(30)); if (Boolean.FALSE.equals(locked)) { return Result.error(手慢了请重试); } try { // 3. 执行Lua脚本扣减Redis库存 DefaultRedisScriptLong script new DefaultRedisScript(); script.setScriptText(LuaScript.DEDUCT_STOCK); script.setResultType(Long.class); String stockKey seckill:stock: goodsId; Long result redisTemplate.execute(script, List.of(stockKey), String.valueOf(1)); // 4. 根据Lua结果决定是否落库 if (result null || result 0L) { return Result.error(库存不足); } if (result -1L) { // key不存在说明商品没有预热需要从DB加载 loadStockToRedis(goodsId); return Result.error(活动未开始请稍后); } // 5. 扣减MySQL库存SQL里带stock 0条件兜底 int rows goodsMapper.deductStock(goodsId); if (rows 0) { return Result.error(库存不足); } // 6. 生成订单状态为待支付 SeckillOrder order new SeckillOrder(); order.setGoodsId(goodsId); order.setUserId(userId); order.setStatus(0); orderMapper.insert(order); return Result.success(抢单成功, order.getId()); } finally { // 7. 释放锁必须用Lua比较value防止误删别人的锁 releaseLock(lockKey, clientId); } }这段代码的逻辑顺序值得细品Redis扣库存和MySQL扣库存不是“双写”而是“Redis先扣MySQL后扣”的异步对账模式。deductStock的SQL条件必须是UPDATE seckill_goods SET stock stock - 1 WHERE goods_id ? AND stock 0这样即使Redis锁因为极端情况失效数据库的行锁和条件更新也能兜住最后一道防线。参数说明lockKey用seckill:lock:前缀加goodsId粒度是商品级而非全局级意味着不同商品可以同时被秒杀不会互相阻塞expire(30秒)是锁的自动过期时间业务必须保证在30秒内完成DB操作否则锁会提前释放引发并发问题。clientId是每次请求生成的UUID释放锁时用它来确认“我是锁的主人”。3.3 RedisLockUtil完整代码与参数说明上面把操作内联在Service里但生产环境我习惯封装一个工具类方便其他地方复用也方便替换成Redisson后面改造章节会讲。资源的RedisLockUtil就是干这个的// RedisLockUtil.java Component public class RedisLockUtil { Autowired private StringRedisTemplate redisTemplate; private static final String LOCK_SUCCESS OK; private static final Long RELEASE_SUCCESS 1L; // 加锁SET key value NX PX expireTime public boolean tryLock(String key, String value, long expireTimeMs) { Boolean result redisTemplate.opsForValue() .setIfAbsent(key, value, Duration.ofMillis(expireTimeMs)); return Boolean.TRUE.equals(result); } // 释放锁Lua脚本保证“比较删除”原子性 public boolean releaseLock(String key, String value) { String script if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]); else return 0; end;; DefaultRedisScriptLong redisScript new DefaultRedisScript(script, Long.class); Long result redisTemplate.execute(redisScript, List.of(key), value); return RELEASE_SUCCESS.equals(result); } }这里有个血泪经验tryLock里用setIfAbsent一次完成“占用过期时间”设置千万不要分两步setnx再expire宁可多写一行代码也别省这次网络往返。之前在生产环境踩过某个线程setnx成功后Redis主节点发生了主从切换过期时间还没来得及同步给从节点结果从节点顶上后锁永远不超时接口全部阻塞。releaseLock必须用Lua脚本做“先get再del”原因很简单线程A的锁过期后线程B抢到了锁此时线程A才执行到del如果不比较value就会把B的锁删掉。很多同学知道要加过期时间却不知道释放锁也要校验这是分布式锁最常见的“锁误删”事故点。3.4 测试Postman并发模拟拿到资源后怎么快速验证锁生效资源里内置了/api/seckill/test接口它不依赖Postman直接用Java的线程池模拟并发// SeckillServiceImpl.java 并发测试方法 public void testConcurrency(String goodsId) throws InterruptedException { int threadCount 1000; CountDownLatch readyLatch new CountDownLatch(threadCount); CountDownLatch startLatch new CountDownLatch(1); AtomicInteger successCount new AtomicInteger(0); AtomicInteger failCount new AtomicInteger(0); for (int i 0; i threadCount; i) { final String userId u_ i; new Thread(() - { readyLatch.countDown(); try { startLatch.await(); Result r doSeckill(goodsId, userId); if (r.isSuccess()) { successCount.incrementAndGet(); } else { failCount.incrementAndGet(); } } catch (Exception e) { failCount.incrementAndGet(); } }).start(); } readyLatch.await(); startLatch.countDown(); Thread.sleep(5000); System.out.println(成功: successCount.get() , 失败: failCount.get()); }这段代码用两个CountDownLatch模拟“同时起跑”第一个等1000个线程全部就绪第二个统一放行。这样能最大限度地制造并发冲突验证Redis锁是否真能拦截重复请求。我在本机跑过库存50个1000并发最终成功数稳定在50左右失败数950左右没有一条超卖订单。注意测试接口是同步阻塞的真实场景里用户不可能等5秒看结果所以第4章的队列削峰才是把“能跑”变成“能上线”的关键。4. 队列削峰把同步下单改造成异步消费4.1 为什么要队列抢锁成功的请求也不能全量打库Redis锁解决了“同时只有一个线程扣库存”但它没解决“成功的请求全部落库”的冲击。比如商品库存100个1万人参与秒杀最终有100人抢锁成功。如果这100人同时调用orderMapper.insertMySQL的插入压力还是很大。资源给出的方案是“Redis锁只管筛选队列负责排队”。抢锁成功的请求不直接写库而是返回“抢单成功排队中”然后往Redis List里推一条消息后台消费者从List里lpop逐条处理。这样数据库的写压力被摊平了而且消费者是单线程天然串行化连MySQL行锁的竞争都省了。4.2 生产者下单请求入队SeckillServiceImpl里抢锁成功后把订单信息推入Redis List// 抢锁成功后的入队逻辑 public Result doSeckillByQueue(String goodsId, String userId) { // 复用前面的锁Lua扣减逻辑成功后才执行下面代码 String queueKey seckill:order:queue; SeckillOrderMessage msg new SeckillOrderMessage(); msg.setGoodsId(goodsId); msg.setUserId(userId); msg.setCreateTime(new Date()); // 序列化后推入Redis List右端 redisTemplate.opsForList().rightPush(queueKey, JSON.toJSONString(msg)); // 立即返回不等消费者处理结果 return Result.success(抢单成功正在排队出单); }入队后立即返回这是秒杀系统“响应快”的秘诀——用户看到的是“抢到了”系统后台慢慢处理。这里有个参数需要关注queueKey是固定字符串意味着所有商品共用一个队列。如果不同商品的秒杀时间重叠单一队列会让“手机秒杀”的订单拖慢“电脑秒杀”的订单。改造方法很简单把queueKey改成seckill:order:queue: goodsId每个商品一个队列消费者也按商品维度部署。4.3 消费者串行消费与库存二次校验消费者在项目启动后通过PostConstruct启动一个后台线程不断lpop队列数据。之所以不直接用Spring的KafkaListener或RabbitListener是因为这个资源只依赖Redis不想为了一个队列引入整套MQ。// OrderConsumer.java Component public class OrderConsumer { Autowired private StringRedisTemplate redisTemplate; Autowired private SeckillOrderMapper orderMapper; Autowired private SeckillGoodsMapper goodsMapper; private static final String QUEUE_KEY seckill:order:queue; PostConstruct public void startConsume() { new Thread(() - { while (true) { try { // 从左端弹出确保先到先出 String msgJson redisTemplate.opsForList().leftPop(QUEUE_KEY, Duration.ofSeconds(2)); if (msgJson null) { // 队列为空休眠1秒再轮询避免空转 Thread.sleep(1000); continue; } SeckillOrderMessage msg JSON.parseObject(msgJson, SeckillOrderMessage.class); // 二次校验数据库库存必须 0 SeckillGoods goods goodsMapper.selectByGoodsId(msg.getGoodsId()); if (goods.getStock() 0) { // 库存没了订单取消记录日志 System.out.println(队列消费库存不足订单已取消); continue; } // 防重复唯一索引兜底若插入失败说明用户已下单 try { SeckillOrder order new SeckillOrder(); order.setGoodsId(msg.getGoodsId()); order.setUserId(msg.getUserId()); order.setStatus(0); orderMapper.insert(order); } catch (DuplicateKeyException e) { System.out.println(队列消费重复下单已忽略); } } catch (Exception e) { // 单条消息失败不能阻塞整个队列记录后继续 System.err.println(队列消费异常: e.getMessage()); } } }).start(); } }队列消费者里有个很容易被忽视的设计leftPop(key, Duration.ofSeconds(2))是阻塞读取队列为空时会阻塞最多2秒而不是立刻返回null。这样做的好处是减少空轮询对Redis的CPU消耗。如果你不传阻塞时间直接leftPop()队列空闲时线程会疯狂打Redis数据库没挂Redis先挂。参数说明Duration.ofSeconds(2)是空队列时的等待超时根据实际流量调节流量大可以缩短到500ms流量小建议放长到5秒Thread.sleep(1000)是二次兜底防止极端情况下的忙等。4.4 数据一致性等消息还是查标记队列化之后有个常见疑问用户看到“抢单成功”但订单可能还没落到MySQL用户立刻去查“我的订单”查不到怎么办实际生产里我不会在秒杀接口里同步返回订单ID而是返回“抢单Id”或“排队编号”用户端拿到这个编号后轮询/api/seckill/order/status?queueIdxxx后台根据队列消费进度返回“处理中/成功/失败”。资源里简化了这个流程直接返回“正在排队出单”不做状态查询接口——因为本文的核心是Redis锁状态机部分请按你自己的系统补齐。这里也引出一个我坚持的习惯消息入队前订单号先生成并记录在Redis或数据库单独的表消费者拿到消息后“update”这个预先创建的订单状态而不是“insert”新订单。这样用户查询时订单一定存在只是状态是“处理中”体验上比“查不到”好得多。如果你在公司里做秒杀这条建议能帮你少接不少客服投诉。5. 避坑记录Redis锁从能跑到稳的五件事5.1 锁被误删A线程删了B线程的锁现象压测时出现超卖排查日志发现一个用户抢到了两个订单。原因线程A执行业务超过锁的过期时间30秒锁自动释放线程B拿到锁继续执行此时A执行完finally里的del直接把B的锁删了另一个线程C趁虚而入。解决释放锁前必须用Lua脚本比较value。血的教训是我一开始就是简单redisTemplate.delete(lockKey)直到压测翻车才改成先get再del。升级后的releaseLock已经把比较和删除写在一个原子操作里如果你拿到的资源版本没有这段代码务必自己加上。5.2 锁还没用上Redis就先崩了setnx和expire之间的空隙现象瞬间有几十个请求同时抢锁实际通过的并发明显高于预期。原因只调用了setnx返回1表示成功还没来得及调用expire设置过期时间当前线程就抛异常中断了。这把锁永远不会过期后续所有请求全部卡在setnx失败上。解决用SET key value NX PX 30000一条命令完成或者用setIfAbsent(key, value, Duration)。核心思路是“原子性”宁可多等一次网络往返把命令合并也不要赌“两条命令之间不会出事”。5.3 库存扣成负数锁对了SQL没兜底现象Redis锁正常运行但数据库库存最终为负数。原因扣减SQL写成了UPDATE seckill_goods SET stock stock - 1 WHERE goods_id ?。第一个线程读库存是1扣完变0第二个线程读库存也是1因为第一个线程还没提交又扣了一次变成-1。Redis锁只保证“拿锁的线程串行”但业务代码里先select后update的间隙依然存在。解决SQL必须带条件AND stock 0且不要先select再update直接在一条update里完成判断和扣减。这是我每次review新人代码必查的点。5.4 锁粒度太粗一个商品抢锁拖慢所有商品现象商品A秒杀刚开始商品B的请求大量超时但B根本还没到秒杀时间。原因锁key设计成了seckill:lock:all或者全局常量所有商品共用一把锁。商品A的抢购线程持有锁30秒商品B的请求在这30秒内全部阻塞。解决锁key必须包含业务维度。资源的写法是seckill:lock: goodsId商品之间互不干扰。同样的道理适用于Redis队列的key不要把所有商品的消息塞进同一个List。5.5 锁过期时间拍脑袋业务一慢锁就失效现象正常情况下锁是30秒但某次数据库慢查询超过40秒锁提前释放两个线程同时进入临界区。原因锁过期时间是硬编码的Duration.ofSeconds(30)没有考虑到业务最慢耗时。秒杀场景里扣库存、插入订单、回写缓存都是快操作但一旦依赖的MySQL慢查30秒很容易被突破。解决过期时间设置为“业务正常耗时的2到3倍”并且给中间件调用设置超时时间。更稳妥的做法是用Redisson的看门狗机制——锁快过期时自动续期这个在下一章展开。如果你的项目不能引Redisson至少得在释放锁时加个判断如果业务执行超过过期时间的一半主动延长锁的过期时间续期操作但这一步要加锁保护。6. 二次开发从单Redis锁到Redisson与多级缓存6.1 升级点这五个位置是你改造的起点现有实现升级方向解决什么问题RedisLockUtil手动锁Redisson的RLock自动续期、看门狗机制Lua脚本扣库存提前预热库存到Redis 异步对账减少DB压力、提升响应速度Redis List队列RocketMQ / RabbitMQ消息持久化、消费者负载均衡单机RedisRedis Cluster RedLock主从切换导致的锁丢失同步返回结果状态轮询 WebSocket推送用户体验升级资源里最核心的改造是把自研RedisLockUtil替换成Redisson。Redisson的分布式锁自带看门狗默认每10秒检查一次锁是否仍被持有是的话自动续期到30秒彻底解决第5.5节“锁过期业务没执行完”的问题。// 替换后的核心代码结构 Autowired private RedissonClient redissonClient; public Result doSeckillWithRedisson(String goodsId, String userId) { String lockKey seckill:lock: goodsId; RLock lock redissonClient.getLock(lockKey); try { // 尝试加锁最多等待1秒自动释放时间30秒 boolean locked lock.tryLock(1, 30, TimeUnit.SECONDS); if (!locked) { return Result.error(手慢了请重试); } // 业务逻辑与原来的第3、4、5步完全一致 // 注意这里不需要手动unlock看门狗会处理续期 } catch (InterruptedException e) { Thread.currentThread().interrupt(); return Result.error(系统繁忙); } finally { // 但释放锁还是要手动调防止业务执行完但锁未续期 if (lock.isHeldByCurrentThread()) { lock.unlock(); } } }tryLock(1, 30, TimeUnit.SECONDS)三个参数分别是获取锁的最大等待时间1秒、锁的自动释放时间30秒、时间单位。值得说明的是Redisson的看门狗只在“不传自动释放时间”或“自动释放时间”很大的情况下生效生产环境我建议显式传30秒配合isHeldByCurrentThread()双重校验。6.2 验证你的改造Jmeter压测与断言二次开发不能改完就上线我每次改锁都会跑一轮压测指标只有三条吞吐量不低于原来的80%、超卖数为0、同用户重复订单数为0。具体做法启动项目确保Redis和MySQL都在本机 用Jmeter创建1000个线程每个线程请求一次 /api/seckill/doSeckill 线程组里用CSV Data Set Config分配不同userId 压测完成后执行断言SQL SELECT COUNT(*) FROM seckill_order WHERE user_id IN (压测用的用户集合) GROUP BY user_id HAVING COUNT(*) 1 理论上返回0行如果把锁从RedisLockUtil换成Redisson压测指标不能只看“成功数”还要看P99响应时间。Redisson的看门狗机制会增加几次Redis续期命令P99可能会从原来的20ms变成30ms这是正常的只要不翻倍就可以接受。6.3 一个具体复用技巧把锁Key抽象成策略改造过程中我强烈建议把锁Key生成抽象成一个接口而不是散落在Service里。比如public interface LockKeyStrategy { // 不同业务可以实现不同前缀和粒度 String buildKey(String bizType, String id); }订单用seckill:lock:order: orderId库存用seckill:lock:stock: goodsId积分扣减用seckill:lock:points: userId。这套抽象能让你的锁粒度一眼看清排查问题时直接按key搜日志不用一个类一个类翻。从那以后我每次接新项目都强制自己先把锁的key设计文档写出来再动手写代码——锁key命名不乱分布式并发问题就少了一半。希望帮到你。本文还有配套的精品资源点击获取

相关新闻

深入浅出BACnet模拟器:楼宇自控联调与设备仿真完全指南

深入浅出BACnet模拟器:楼宇自控联调与设备仿真完全指南

简介:这是一份BACnet楼宇自动控制网络通信协议的模拟器及相关源码整合包,面向从事建筑自动化系统集成、设备调试与协议学习的工程师和技术人员。压缩包内共含1650个文件,以Python脚本为主(1072个py),并包含…

2026/10/19 15:38:18 阅读更多 →
基于Spring Boot+Vue的一卡通消费系统:统一刷卡扫码刷脸流水,实现幂等扣款

基于Spring Boot+Vue的一卡通消费系统:统一刷卡扫码刷脸流水,实现幂等扣款

简介:一套基于SpringBoot与Vue前后端分离架构的一卡通消费系统源码包,覆盖人脸、刷码、实体卡三种消费方式,面向需要完成毕业设计或课程设计的计算机专业学生,也适合初学者学习主流前后端分离项目落地。资源共748个文件&#xff0…

2026/10/19 22:55:47 阅读更多 →
Vibe Coding氛围编程系列|2026全网最热Claude Skills工具箱:GitHub最受欢迎的7大开源AI技能库

Vibe Coding氛围编程系列|2026全网最热Claude Skills工具箱:GitHub最受欢迎的7大开源AI技能库

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 8:27:15 阅读更多 →

最新新闻

校招面试算法与JVM核心考点全解析:从刷题到调优的实战指南

校招面试算法与JVM核心考点全解析:从刷题到调优的实战指南

1. 校招面试官到底想在算法题里看到什么很多人准备校招的时候,第一反应是去刷题网站按标签刷个三五百道,觉得数量上去了自然就稳了。我带过几届校招的技术面,也跟不少做校招的朋友聊过,实际情况是:面试官出算法题&…

2026/10/19 22:50:12 阅读更多 →
【信息科学与工程学】【数据中心】 第十一篇 人工智能智算中心整体设计101 智算中心解决方案 第一部分1.1 智算中心ICT基础设施的定义与边界

【信息科学与工程学】【数据中心】 第十一篇 人工智能智算中心整体设计101 智算中心解决方案 第一部分1.1 智算中心ICT基础设施的定义与边界

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/19 22:46:04 阅读更多 →
open-code-review:CLI+LLM+Git驱动的开源代码审查新范式

open-code-review:CLI+LLM+Git驱动的开源代码审查新范式

1. 什么是 open-code-review:一个被严重低估的开发者协作新范式open-code-review 不是某个具体软件的名字,也不是某家公司的私有产品,而是一类正在快速成型的技术实践范式——它指代的是基于开源理念、命令行界面(CLI)…

2026/10/19 22:40:54 阅读更多 →
二轮平衡车全套源码:卡尔曼滤波与PID三环控制实战

二轮平衡车全套源码:卡尔曼滤波与PID三环控制实战

简介:这是一套面向电子爱好者与嵌入式初学者的二轮平衡车DIY完整资料包,涵盖从硬件搭建到软件调试的全链路内容。资源定位为STM32平台平衡车的开源学习套件,包含互补滤波、卡尔曼滤波、DMP三种姿态解算方案的源码,三套工程均编译通…

2026/10/19 22:39:52 阅读更多 →
加扰与解扰:原理、区别及在高速链路中的应用

加扰与解扰:原理、区别及在高速链路中的应用

简介:本资源面向数字通信、FPGA开发与信号处理方向的学习者,围绕加扰与解扰这一核心环节,提供基于VHDL的完整工程实现与验证流程。加扰通过可逆数学变换打散连续相同符号,降低信道自相关与互相关影响,广泛应用于数字电…

2026/10/19 22:35:37 阅读更多 →
数据范围决定算法选型:从O(n log n)到O(n)

数据范围决定算法选型:从O(n log n)到O(n)

我去年处理过一个挺有意思的活儿:某内部工具需要在上亿条日志里按时间窗口做聚合统计,数据的时间戳全部落在最近三个月内。一开始我按教科书思路上了红黑树、跳表这类“高级”结构,结果在真实数据上跑得还不如后来换上的一个普通数组快。原因…

2026/10/19 22:32:32 阅读更多 →

日新闻

频率跟踪型PWM电源设计:臭氧发生器谐振点漂移与软开关实现

频率跟踪型PWM电源设计:臭氧发生器谐振点漂移与软开关实现

简介:本资源为电源技术方向的学术研究文档,聚焦介质阻挡放电型臭氧发生器电源的频率跟踪型PWM控制策略,面向电力电子、高压电源及臭氧应用领域的科研人员与工程技术人员。针对负载呈容性、等效电容随外加电压升高而增大、谐振频率随之漂移的难…

2026/10/19 0:00:47 阅读更多 →
Vibe Coding 遇上 TaoToken:把开发者思维圣殿的灵感流接入统一 Key 通道

Vibe Coding 遇上 TaoToken:把开发者思维圣殿的灵感流接入统一 Key 通道

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/19 0:00:47 阅读更多 →
HundredCV-Chat话题建模实战教程:利用4,871个内置话题标签构建主题分析系统

HundredCV-Chat话题建模实战教程:利用4,871个内置话题标签构建主题分析系统

【免费下载链接】HundredCV-Chat 项目地址: https://ai.gitcode.com/hf_mirrors/Jax-dan/HundredCV-Chat 点击查看 免费下载 HundredCV-Chat 是一个由 100 位青年简历数据开发而来的中文多轮对话数据集,共包含 24,750 组日常闲聊对话和 4,871 个话题标签…

2026/10/19 0:00:47 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/19 10:02:04 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/19 17:32:41 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/19 1:30:52 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/19 2:54:50 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/19 9:10:22 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 12:16:16 阅读更多 →