Laravel秒杀实战:Redis预扣库存与队列异步落库防超卖

聊到秒杀,不少 Laravel 同学的第一个反应都是:这有什么好写的,不是一条 update 语句判断库存大于 0 的事情吗?说实话我最早也这么想,直到在某次活动里用“先查库存再扣减”的写法,直接见证了同一个商品库存从正数变成负数、后台订单多出好几条之后,才老老实实去研究 Laravel 秒杀系统的完整实现链路。

这篇文章不是教你写漂亮的控制器,而是把一个基础但完整的Laravel 秒杀 Demo从零搭到能扛压测:包括数据表怎么设计、为什么用 Redis 预扣库存而不用数据库行锁、订单为什么要丢到队列里异步落库、怎么防用户连点、怎么限流防刷,以及最后怎么用压测工具验证库存不超卖。读完你至少能复现一个“在高并发下库存数据依然正确”的秒杀接口,适合已经会 Laravel 基础增删改查、但没正经做过高并发场景的开发者。

1. 为什么一个库存判断写不好,秒杀就一定会超卖

秒杀系统最难的不是写代码,而是想清楚几万个人同时点按钮时会发生什么。先看最典型的“朴素写法”,再逐步拆解它为什么必挂。

1.1 最常见的“查库存再扣减”代码,错在哪

很多 Laravel 教程里会出现这样的代码:

$product = Product::find($id); if ($product->stock > 0) { $product->decrement('stock'); // 创建订单 }

看着挺合理:先判断有货,再扣库存,再下单。问题在于“判断”和“扣减”中间隔着一道网络请求和代码执行的间隙。

假设 Redis 或数据库里当前库存是 1,两个用户同时发起请求。A 请求和 B 请求几乎同一瞬间读到 stock = 1,两个都通过了 if 判断,然后各自执行 decrement。最终库存变成 -1,订单却生成了两条。这就是经典的“检查-更新”竞态,学过数据库的人都知道它叫 race condition,但写业务代码时还是会随手写出来。

这个事故的本质不是 decrement 不原子,而是“库存大于 0 的判断”和“库存扣减动作”被拆成了两步,中间留下了时间窗。高并发下这个时间窗会被无限放大。

1.2 用数据库硬扛高并发的两个方案,为什么都不够舒服

既然拆开不行,那就合并。先看看数据库层面能怎么救。

第一种方案是悲观锁,Laravel 里用lockForUpdate():

$product = Product::where('id', $id)->lockForUpdate()->first(); if ($product->stock > 0) { $product->decrement('stock'); // 创建订单 }

lockForUpdate()会执行select ... for update,把这一行锁住,直到事务提交才释放。并发请求会串行排队,库存不会超卖,但代价是性能上限很低。秒杀场景动辄每秒几千上万的请求量,全挤在一个行锁上排队,数据库连接很快被打满。

第二种方案是乐观锁,用“条件更新”把判断和扣减合成一条 SQL:

$affected = DB::table('products') ->where('id', $id) ->where('stock', '>', 0) ->decrement('stock'); if ($affected === 0) { return '已售罄'; }

UPDATE products SET stock = stock - 1 WHERE id = ? AND stock > 0这条 SQL 在数据库内部是原子的,它也解决超卖问题,性能比悲观锁好很多,很多教程会把这当作标准答案。

但真正的秒杀场景里,问题不止“扣库存”。数据库单表在极端 QPS 下依然会成为瓶颈,而且秒杀链路里往往还有用户校验、风控、订单生成、支付单创建等一堆动作,如果全部同步做完,接口响应时间会很长,用户的重复点击又反过来加剧数据库压力。所以稍微有点规模的项目,最终都走向同一个架构思路:Redis 扛流量,队列慢慢落库。

提示:如果你只是想快速验证一个不超卖的接口,乐观锁确实够用。但要做到“完整秒杀 Demo”,Redis 预扣 + 异步落库这套逻辑才是更接近生产环境的做法。

2. Demo 的数据模型设计:商品、秒杀商品、订单三张表怎么拆

数据表设计往往决定后面代码写起来顺不顺手。秒杀场景里商品普通信息和秒杀规则最好不要混在一张表里,我下面给出的是实践下来比较舒服的一套结构。

2.1 环境版本与依赖准备

这个 Demo 我用的环境如下,大家可以直接参考:

  • PHP 8.2+
  • Laravel 10 / 11 均可
  • Redis 6+,用于缓存、库存预扣、队列
  • predis/predis扩展包,如果你装了 phpredis 扩展也可以把Redis::connection()->client()换成 phpredis 客户端

安装依赖:

composer require predis/predis

.env关键配置:

QUEUE_CONNECTION=redis CACHE_STORE=redis REDIS_CLIENT=predis REDIS_HOST=127.0.0.1 REDIS_PORT=6379

队列驱动用 Redis 是因为后面要跑异步订单落库,如果本地没装 Redis,建议先用 Docker 拉一个,别偷懒。

2.2 数据表结构与迁移文件

秒杀 Demo 涉及三张核心表:商品表、秒杀商品表、订单表。商品表是基础数据,秒杀商品表记录某次活动的秒杀价和秒杀库存,订单表记录用户抢到的结果。

商品表迁移:

Schema::create('products', function (Blueprint $table) { $table->id(); $table->string('name'); $table->decimal('price', 10, 2); $table->unsignedInteger('stock')->default(0); $table->timestamps(); });

秒杀商品表:

Schema::create('seckill_products', function (Blueprint $table) { $table->id(); $table->foreignId('product_id')->constrained(); $table->decimal('seckill_price', 10, 2); $table->unsignedInteger('seckill_stock')->default(0); $table->unsignedInteger('limit_per_user')->default(1); $table->timestamp('start_time'); $table->timestamp('end_time'); $table->timestamps(); });

订单表:

Schema::create('orders', function (Blueprint $table) { $table->id(); $table->string('order_no', 64)->unique(); $table->foreignId('user_id')->constrained(); $table->foreignId('seckill_product_id')->constrained(); $table->decimal('amount', 10, 2); $table->unsignedTinyInteger('status')->default(0); $table->json('product_snapshot'); $table->timestamp('expired_at')->nullable(); $table->timestamps(); $table->unique(['user_id', 'seckill_product_id'], 'uk_user_seckill'); });

订单表里这个联合唯一索引很重要。它保证同一个用户对同一个秒杀商品只能生成一条订单记录,是后面幂等消费的兜底。

2.3 为什么单独拆一张秒杀商品表

有同学可能会问,为什么不在 products 表里直接加seckill_price、seckill_stock、start_time这几个字段?那样确实简单,但有几个问题:

  • 普通商品列表、购物车、订单详情都依赖 products 表,加一堆秒杀字段会让这张表的缓存体积变大,命中率下降。
  • 秒杀活动往往有多个批次,同一个商品这次秒杀结束,下次还要再开,把活动字段塞在商品表里只能记一组状态。
  • 秒杀库存和普通库存的扣减逻辑完全不同,拆开后可以分别预热缓存,互不干扰。

拆表之后,products 表只存“静态”的商品信息,seckill_products 表管活动维度数据,orders 表管结果。这个分层在后续写 Redis 缓存预热和库存扣减时非常顺手。

提示:下单时记得把商品名称、价格快照写入 orders 表的product_snapshot字段。秒杀活动一旦结束,商品表和秒杀表随时可能改价、改名,历史订单不能跟着变。很多新手在这里踩坑,订单详情页显示的是商品当前价格,而不是下单时价格。

3. 下单核心链路:Redis 预扣库存,订单交给队列

这一节是整个 Demo 的心脏。理解了这里的流程,秒杀系统的大半个骨架就清楚了。

3.1 一次秒杀的完整流程

我设计的接口调用流程如下,每个步骤的职责都很单一:

  1. 校验秒杀活动时间是否在 start_time 和 end_time 之间。
  2. 检查用户维度的幂等键,防止同一个用户连点或脚本刷接口。
  3. 用 Redis 原子扣减秒杀库存,扣减失败直接返回“已抢光”。
  4. 扣减成功后,把“创建订单”的任务丢进队列。
  5. 接口立刻返回“排队中”,前端稍后轮询订单状态。

控制器核心逻辑大致长这样:

public function start(Request $request, $id) { $userId = $request->user()->id; // 1. 活动时间与商品状态校验 $seckill = SeckillProduct::with('product')->where('id', $id)->firstOrFail(); $now = now(); abort_if($now < $seckill->start_time, 403, '活动未开始'); abort_if($now > $seckill->end_time, 403, '活动已结束'); // 2. 用户幂等校验,防止重复下单 $lockKey = "seckill:user:{$seckill->product_id}:{$userId}"; $locked = Redis::set($lockKey, 1, 'EX', 600, 'NX'); if (!$locked) { return response()->json(['message' => '请勿重复提交']); } // 3. Redis 原子预扣库存 $stockKey = "seckill:stock:{$seckill->product_id}"; $result = Redis::eval( <<<'LUA' local stock = tonumber(redis.call('GET', KEYS[1])) if stock == false or stock <= 0 then return 0 end redis.call('DECR', KEYS[1]) return 1 LUA, 1, $stockKey ); if (!$result) { return response()->json(['message' => '手慢了,已售罄']); } // 4. 丢进队列异步落库 dispatch(new CreateSeckillOrder($userId, $seckill->id)); // 5. 返回排队中 return response()->json(['message' => '排队中,请稍后查看订单']); }

注意这个流程里所有容易出错的、耗时的操作都在 Redis 和队列之间完成,数据库连接只在 worker 消费队列时短暂使用,这是秒杀接口能扛高并发的关键。

3.2 库存预扣的两种写法与原子性说明

刚才控制器里用的是 Lua 脚本。为了让没有写过 Redis Lua 的同学也能看懂,我把两种常见写法都列出来。

第一种是 DECR 后判断负数再回补:

$stock = Redis::decr("seckill:stock:{$productId}"); if ($stock < 0) { Redis::incr("seckill:stock:{$productId}"); return false; }

这种做法非常常见,思路是先把库存减 1,发现减成负数就加回来,等于“借了再还”。它能防止超卖,但有隐患:一旦 DECR 成功而接下来的业务逻辑挂了,或者网络超时导致 INCR 执行失败,Redis 里的库存就永久少了一件。而且从 DECR 到 INCR 之间有一段延迟,这期间库存是负数,前端看到的数据会异常。

第二种是 Lua 脚本原子完成“检查 + 扣减”:

local stock = tonumber(redis.call('GET', KEYS[1])) if stock == false or stock <= 0 then return 0 end redis.call('DECR', KEYS[1]) return 1

Redis 的 Lua 脚本在执行期间不会被其他命令插入,相当于整个检查-扣减流程是原子的。这样既没有负数窗口期,也不存在“忘了回补”的问题。这也是我在 Demo 里推荐的方式。

注意:执行 Lua 脚本前,务必先确保GET能取到值,否则库存秒杀完了再请求,脚本会返回 0 并安全退出。生产环境里库存预热就是把秒杀库存量提前 SET 进 Redis。

3.3 队列 Job:订单落库和数据库库存扣减

库存预扣成功之后,订单还没有创建。这时需要把任务交给队列 worker,让后台慢慢把订单写进 MySQL。

CreateSeckillOrder Job:

class CreateSeckillOrder implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public function __construct( public int $userId, public int $seckillId ) {} public function handle(): void { DB::beginTransaction(); try { $seckill = SeckillProduct::with('product')->lockForUpdate()->find($this->seckillId); // 幂等兜底:如果订单已存在,直接返回 $exists = Order::where('user_id', $this->userId) ->where('seckill_product_id', $this->seckillId) ->exists(); if ($exists) { DB::commit(); return; } Order::create([ 'order_no' => 'SK' . date('YmdHis') . random_int(1000, 9999), 'user_id' => $this->userId, 'seckill_product_id' => $seckill->id, 'amount' => $seckill->seckill_price, 'product_snapshot' => [ 'name' => $seckill->product->name, 'price' => $seckill->seckill_price, ], 'status' => 0, 'expired_at' => now()->addMinutes(15), ]); $seckill->decrement('seckill_stock'); DB::commit(); } catch (\Throwable $e) { DB::rollBack(); // 库存回补,避免 Redis 库存与真实可售库存不一致 Redis::incr("seckill:stock:{$seckill->product_id}"); throw $e; } } }

这里有几个地方值得解释一下。

Job 里又对秒杀商品做了lockForUpdate(),是为了防止多个 worker 同时处理同一个秒杀商品时,数据库里的seckill_stock扣减也出现竞态。虽然 Redis 层已经挡掉了大部分流量,但并发消费队列时 worker 之间依然可能同时读到相同库存,所以数据库侧的兜底锁不能省。

exists判断加联合唯一索引,是保证“同一个用户同一场秒杀只生成一条订单”的最后一道防线。队列消费失败会自动重试,如果重试时订单已经创建,直接返回即可,不能再次生成订单。

3.4 失败补偿:库存回补的时机与实现

库存回补是很多人容易漏掉的一环。Redis 库存扣了,但订单最终没建成,那库存就凭空消失了。我遇到过不少次,活动结束一核对 Redis 和 MySQL,两边数字对不上。

需要回补的场景主要有三种:

  • 队列消费失败,Job 抛出异常后系统自动重试仍失败,订单没创建成功。
  • 用户 15 分钟内未支付,订单超时取消。
  • 用户主动取消订单。

第一种场景就在 Job 的 catch 块里处理,回滚后立刻Redis::incr。这里要注意别重复回补,所以需要给 Job 加上幂等标记。我的做法是把订单创建动作和回补动作做成配套,只有订单确实没写入系统时才回补,靠exists判断兜底。

超时未支付的回补,我用一个定时任务扫描expired_at已过的未支付订单:

$expiredOrders = Order::where('status', 0) ->where('expired_at', '<', now()) ->get(); foreach ($expiredOrders as $order) { DB::transaction(function () use ($order) { $order->update(['status' => 2]); $seckill = SeckillProduct::where('id', $order->seckill_product_id)->lockForUpdate()->first(); $seckill->increment('seckill_stock'); Redis::incr("seckill:stock:{$seckill->product_id}"); }); }

这里 Redis 库存和数据库库存同时回补,两边保持一致。如果用户是主动取消订单,走的也是同一套逻辑,把两个地方的数字都加回去。

3.5 客户端怎么知道抢没抢到

接口只返回了一个“排队中”,用户肯定不愿意干等。前端需要轮询订单查询接口:

public function order(Request $request, $seckillId) { $order = Order::where('user_id', $request->user()->id) ->where('seckill_product_id', $seckillId) ->first(); if (!$order) { return response()->json(['status' => 'pending', 'message' => '抢购处理中']); } return response()->json([ 'status' => $order->status === 0 ? 'created' : 'paid', 'order_no' => $order->order_no, ]); }

用户看到“排队中”后,前端每隔 1 到 2 秒查一次订单接口。如果 Redis 库存抢光了,Order表里不会有记录,查询接口会一直返回 pending。这里无妨,因为用户最终通过活动页“已售罄”状态感知结果。更精细的做法是保存一个“排队结果”缓存,订单创建成功或失败都往里写值,查询接口优先读缓存。

4. 防超卖与防刷的实战组合拳

库存不超卖只是底线,秒杀系统还要挡住连点、脚本、恶意流量。这一节讲我实际用到的几层防护。

4.1 用户维度幂等键:让连点不再产生并发请求

用户手速再快,一秒钟也就能点几次;但脚本可以瞬间发几十个请求。所以接口第一步要拦“同一个用户对同一个商品的重复请求”。

我用的方案是 RedisSET NX EX:

$key = "seckill:user:{$seckillId}:{$userId}"; $locked = Redis::set($key, 1, 'EX', 600, 'NX'); if (!$locked) { return response()->json(['message' => '请勿重复提交']); }

NX参数表示只有 key 不存在时才设置成功,EX 600表示这个幂等键 10 分钟后自动过期。它的作用有两个:一是 10 分钟内同一个用户无法重复提交,二是即使 Redis 预扣库存之前用户就退出了,10 分钟后还能再次尝试。

这个过期时间要根据业务调整。如果秒杀活动持续 2 小时,用户 10 分钟不能重复下单问题不大。如果你希望用户“付款失败后立刻能重新抢”,可以把过期时间缩到 60 秒,甚至监听订单支付失败时主动删除幂等键。

4.2 Laravel throttle 中间件与自定义 Redis 限流

Laravel 自带throttle中间件,配置很方便:

Route::post('/seckill/{id}', [SeckillController::class, 'start']) ->middleware('throttle:10,1');

意思是每个 IP 每分钟最多 10 次请求。它的好处是开箱即用,坏处是粒度太粗。秒杀活动里同一个出口 IP 可能承载成千上万个真实用户,用 IP 限流容易误伤,而且 Laravel 默认的 throttle 是写在应用层缓存里的,本身就扛不住太大压力。

更贴近生产环境的做法是自己在 Redis 里做计数器限流,按用户维度限制频率:

$key = "seckill:rate:{$userId}:{$seckillId}"; $count = Redis::incr($key); if ($count === 1) { Redis::expire($key, 5); } if ($count > 5) { return response()->json(['message' => '操作过于频繁'], 429); }

这个方案允许每个用户每 5 秒最多发起 5 次请求,粒度更合理,而且 Redis 的 INCR 是原子的,不需要额外加锁。真实风控系统还会接设备指纹、行为轨迹、人机验证,Demo 做到用户维度限流已经比裸接口安全很多了。

4.3 活动时间与库存预热,避免缓存穿透把数据库打穿

秒杀开始前,会有大量请求提前打到活动接口。如果每次请求都查 MySQL,数据库必挂。所以我通常会写一个预热脚本,在活动开始前把秒杀库存提前加载进 Redis:

php artisan seckill:preheat --seckillId=1

脚本逻辑:

class PreheatSeckillStock { public function handle($seckillId) { $seckill = SeckillProduct::with('product')->find($seckillId); Redis::set("seckill:stock:{$seckill->product_id}", $seckill->seckill_stock); Redis::set("seckill:start:{$seckill->product_id}", $seckill->start_time->timestamp); Redis::set("seckill:end:{$seckill->product_id}", $seckill->end_time->timestamp); } }

接口侧先判断 Redis 里的活动时间,再查数据库:

$start = Redis::get("seckill:start:{$seckill->product_id}"); $end = Redis::get("seckill:end:{$seckill->product_id}"); $now = time(); if ($now < $start || $now > $end) { return response()->json(['message' => '不在活动时间内']); }

预热之后,秒杀接口完全不依赖 MySQL 时间,也不会因为活动开始的一瞬间高并发读库把数据库打瘫。这个细节在老项目里很容易被忽略,我见过不止一次活动开始后 MySQL 连接数瞬间打满,最后查出来是活动时间每次实时查表。

4.4 压测中最容易暴露的 Laravel 瓶颈

高并发下 Laravel 本身的坑比业务逻辑更隐蔽。下面这几个是我每次压测都必然要检查的点:

  • 配置缓存:没执行php artisan config:cache时,每个请求都会重新读取 .env 并解析配置文件,压测一上来 CPU 直接飙高。
  • 路由缓存:php artisan route:cache也能减少路由解析开销,Demo 阶段可以顺手打上。
  • 数据库连接数:Laravel 没有内置连接池,每个请求都会新建 PDO 连接。压测时 MySQL 的max_connections很容易被打满。
  • 队列 worker 数量:订单落库依赖 queue worker,默认只开一个 worker 时,消费速度根本跟不上 Redis 预扣速度。本地压测至少开 5 个 worker,生产环境建议用专业队列管理工具。

队列开启方式:

php artisan queue:work redis --tries=3 --backoff=3 --sleep=1 --timeout=60 & php artisan queue:work redis --tries=3 --backoff=3 --sleep=1 --timeout=60 & php artisan queue:work redis --tries=3 --backoff=3 --sleep=1 --timeout=60 &

注意--tries=3表示失败最多重试 3 次,--backoff=3表示重试间隔 3 秒。这样即使订单创建偶发失败,也不会无限重试把日志刷爆。

5. 压测验证与从 Demo 到生产还差什么

写了这么多,到底对不对,最终要看数据。我习惯用 Apache ab 做简单压测,它虽然不是最专业的工具,但足够验证库存一致性问题。

5.1 用 ab 压出的真实数据长什么样

把请求参数写入文件 post_data.json:

{ "product_id": 1 }

执行压测命令:

ab -n 10000 -c 200 -p post_data.json -T application/json \ -H "Authorization: Bearer 测试用户的token" \ http://localhost:8000/api/seckill/1

这个命令表示总共发 10000 个请求,并发 200。压测结束后重点看两处:

一是请求的失败率。正常配置下,大部分请求会返回“排队中”或“已售罄”,两者都属于正常业务响应,non-2xx responses里如果出现 500 才是真的有问题。

二是核对订单表和 Redis 库存。活动结束后,查一下订单表总订单数,对比 Redis 里剩余库存加已扣库存是否等于最初预热库存。我压测时订单一共创建了 X 条,Redis 剩余库存 Y,X 加 Y 恰好等于预热库存,说明超卖被挡住了。

压测结果里最直观的是 Requests per second。在本地开发机上,不加任何防护的秒杀接口可能 RPS 只有几百,加了 Redis 预扣和队列异步后,接口响应很快,RPS 能大幅提升。

5.2 我踩过的几个坑,按严重程度排序

第一个坑是忘了预热直接压测。压测一开始请求全打到 MySQL,数据库连接数瞬间飙满,导致所有请求都超时。后来我才反应过来,活动开始前必须把秒杀商品、库存、活动时间全部预热到 Redis,接口只从 Redis 读,才能扛住流量。

第二个坑是队列 worker 数量不够。库存预扣很快,但 worker 只有 1 个,订单表插入速度完全跟不上,结果出现“Redis 库存已经扣完,但订单表只有一半记录”的假象。这不是超卖,是消费积压。把 worker 开到 5 个以上就恢复正常了。这里也引出一个注意点:Redis 预扣库存只是“占坑”,真正提交订单还要靠队列及时消化。

第三个坑是订单重复生成。第一次实现时没加联合唯一索引,队列重试导致同一用户同一个商品生成了多条订单。后来加了uk_user_seckill唯一索引,并在 Job 里做 exists 判断,才彻底解决。

第四个坑是 Redis 里的幂等键过早过期。最早我把过期时间设成 30 秒,结果用户从点击按钮到支付完成需要一分钟,支付成功后想再确认订单详情,却被幂等键挡住,提示“请勿重复提交”。后来把过期时间调整到 15 分钟,和订单的expired_at对齐,体验就好了。

5.3 从 Demo 到生产还需要补什么东西

Demo 能跑通、能扛住本地压测,但离生产还有一段距离。我把主要差距列一下,方便后续扩展:

补充项说明
库存分片把同一商品库存拆成多个 Redis key,每个 key 服务一部分流量,降低单 key 热点压力
多队列隔离秒杀订单队列和普通业务队列分开,普通队列积压不影响秒杀落库
消息确认与死信队列消费失败后进入死信队列,人工补偿库存和订单
监控告警对 Redis 内存、队列长度、订单积压数量做监控,活动异常时能第一时间发现
降级方案Redis 不可用时切换数据库乐观锁方案,保证活动不死但性能下降

库存分片这块,简单做法是加一个后缀索引,比如seckill:stock:1:0到seckill:stock:1:9十个分片,用户请求按 hash 取模落到某个分片。每个分片独立扣减,总量就是所有分片之和,能显著降低单点热点。但要注意订单创建后的库存回补也要按分片回补。生产级别的复杂度主要体现在这些边界细节上。

最后分享一个我个人的体会:秒杀 Demo 看起来代码量不大,真正难的是理解每个环节为什么这么设计。为什么用 Redis 预扣库存,不是为了炫技,而是为了把数据库从高并发流量中解放出来;为什么订单要异步落库,是为了让用户请求快速结束,避免长时间占用连接;为什么每个环节都要做幂等,是因为分布式系统里“重试是常态”。

如果让我重新做一个 Laravel 秒杀 Demo,我会先把数据表设计画清楚,再写一条最简链路:预热库存、Lua 扣减、队列落库、库存回补。链路跑通之后,再把限流、幂等、防刷一层层加上去。按照这个顺序,排查问题会轻松很多,不至于一开始就被一堆概念绕昏头。

相关新闻

EasyLive实战:基于ffmpeg的流汇聚与转发全解析

EasyLive实战:基于ffmpeg的流汇聚与转发全解析

简介&#xff1a;EasyLive是一款基于ffmpeg实现的流汇聚与转发工具&#xff0c;支持RTSP、RTMP及本地视频文件输入&#xff0c;输出RTSP、RTMP并可直接录制到本地&#xff0c;适合音视频开发者、流媒体运维人员以及相关项目集成场景使用。压缩包共74个文件&#xff0c;主要由可…

2026/10/20 21:53:24 阅读更多 →
Spring Boot + MyBatis + JavaFX 联调工程模板:从架构到打包一次讲清

Spring Boot + MyBatis + JavaFX 联调工程模板:从架构到打包一次讲清

简介&#xff1a;这个项目是一个将SpringBoot、MyBatis与JavaFX整合在一起的档案扫描管理应用&#xff0c;面向需要搭建桌面端服务端完整业务系统的Java开发者&#xff0c;也适合正在学习主流框架整合的初学者。资源包内共包含130个文件&#xff0c;以xml配置文件、java源码和c…

2026/10/20 21:53:24 阅读更多 →
批量抠图导出全流程实战:方案选型、参数设置与失败处理

批量抠图导出全流程实战:方案选型、参数设置与失败处理

开头我先交代一个场景&#xff1a;上个月帮人整理一批电商商品图&#xff0c;一共两千多张&#xff0c;每张都要把原来的生活场景背景换成纯白底。如果按老办法一张张用钢笔工具勾路径&#xff0c;或者用魔棒一点点擦边缘&#xff0c;按一张十五分钟算&#xff0c;这是五百个小…

2026/10/20 21:53:24 阅读更多 →

最新新闻

开源日报热门项目实操:终端AI助手、剪贴板同步与自托管监控

开源日报热门项目实操:终端AI助手、剪贴板同步与自托管监控

1. 先说说这一期的热门全景怎么观察我做开源项目日报已经有一段时间了。每天早上第一件事&#xff0c;不是打开各种资讯站&#xff0c;而是直接去代码托管平台的榜单上看一圈&#xff0c;再结合社区讨论、release动态、issue反馈&#xff0c;综合判断这一天到底哪些项目值得被写…

2026/10/21 0:52:38 阅读更多 →
有限元面试53题:从刚度矩阵到工程实战的复习指南

有限元面试53题:从刚度矩阵到工程实战的复习指南

一提到“有限元面试”&#xff0c;很多人第一反应就是背公式、刷题。但等真正坐到面试桌对面&#xff0c;面试官问的往往不是“你有多少年经验”&#xff0c;而是“你算过的东西&#xff0c;你真的懂吗”。这份复习指南的核心&#xff0c;就是围绕53道有限元&#xff08;FEA&am…

2026/10/21 0:52:38 阅读更多 →
游戏支付平台源码:轻量级支付中台搭建与避坑指南

游戏支付平台源码:轻量级支付中台搭建与避坑指南

简介&#xff1a;这是一套面向游戏运营与支付系统开发者的完整技术方案源码包&#xff0c;涵盖游戏支付平台、充值平台、第三方支付对接及游戏网关支付接口四大核心模块&#xff0c;适用于中小型游戏公司快速搭建合规、可扩展的支付中台。资源共2000个文件&#xff0c;主体为Ja…

2026/10/21 0:52:38 阅读更多 →
8类AI文献综述工具全解析:从检索到评述的流水线指南

8类AI文献综述工具全解析:从检索到评述的流水线指南

写文献综述最让人崩溃的不是“写”&#xff0c;而是“综述”这两个字背后的整套流程——找文献、筛文献、读文献、分类、归纳、梳理脉络、找研究空白、建框架、动笔写。我当年写第一篇综述熬了三个通宵&#xff0c;最后被导师一句“只有罗列&#xff0c;没有评述”打回重来&…

2026/10/21 0:51:37 阅读更多 →
Redisson缓存报错missing @class?序列化配置与排查修复全指南

Redisson缓存报错missing @class?序列化配置与排查修复全指南

如果你在项目里用 Redisson 做分布式缓存&#xff0c;get()读取数据时突然抛出missing type id property class&#xff0c;说明你写入缓存的数据格式和读取时使用的解析规则已经对不上了。这个报错我前阵子刚踩过&#xff0c;第一反应以为是get()方法用错了&#xff0c;后来才…

2026/10/21 0:51:37 阅读更多 →
MySQL事务底层原理:undo/redo日志、MVCC与锁机制全解析

MySQL事务底层原理:undo/redo日志、MVCC与锁机制全解析

1. 事务到底在解决什么问题很多刚接触数据库的朋友会觉得事务这东西有点虚——平时写个增删改查&#xff0c;加不加事务好像都能跑&#xff0c;业务也没出过什么大乱子。但只要你经历过一次线上对账不平、库存扣成负数、或者转账转了一半系统崩了&#xff0c;你就会明白事务不是…

2026/10/21 0:51:37 阅读更多 →

日新闻

统一内存+量化:锐龙AI Max PRO 400如何本地跑320B大模型

统一内存+量化:锐龙AI Max PRO 400如何本地跑320B大模型

这段时间硬件圈和AI圈都在传一个事&#xff1a;AMD锐龙AI Max PRO 400系列把最高192GB统一内存搬进了PC形态的产品里&#xff0c;按宣传口径&#xff0c;本地跑320B参数的大家伙不再是纸面概念。我第一反应倒不是“内存真大”&#xff0c;而是这话背后的含金量——它把过去只能…

2026/10/21 0:00:11 阅读更多 →
AI助手深度使用体验:对话质量、长文本处理与指令遵循如何留住用户

AI助手深度使用体验:对话质量、长文本处理与指令遵循如何留住用户

1. 从“封号重开”这个行为说起&#xff1a;用户到底在追求什么“封号重开”这四个字&#xff0c;放在任何产品语境里都带着一股决绝的味道。一个账号从零开始积累使用记录、调教偏好、沉淀工作流&#xff0c;背后是大量的时间和精力成本。正常情况下&#xff0c;没有人愿意主动…

2026/10/21 0:00:11 阅读更多 →
ROS中YOLOv3工程落地:通信同步、坐标变换与性能调优

ROS中YOLOv3工程落地:通信同步、坐标变换与性能调优

1. 为什么在ROS里硬刚YOLOv3不是“炫技”&#xff0c;而是工程落地的必经之路我第一次在某高校机器人实验室看到那台搭载Jetson TX2的移动底盘原地打转、反复识别错垃圾桶时&#xff0c;就意识到&#xff1a;单纯跑通darknet_ros demo和真正让YOLOv3在ROS里“干活”&#xff0c…

2026/10/21 0:00:11 阅读更多 →

周新闻

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

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

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

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

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

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

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

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

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

2026/10/20 2:50:50 阅读更多 →

月新闻

我发现了一个新思路:用 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/20 10:02:08 阅读更多 →
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/20 9:50:17 阅读更多 →
黑夜航拍船只数据集训练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/20 9:50:45 阅读更多 →