[{"data":1,"prerenderedAt":972},["ShallowReactive",2],{"\u002Farticles\u002Fredis":3},{"id":4,"title":5,"body":6,"category":960,"cover":961,"date":962,"description":963,"extension":964,"meta":965,"navigation":966,"path":967,"seo":968,"sitemap":969,"stem":970,"__hash__":971},"articles\u002Farticles\u002F库存扣减超时，别急着上Redis.md","老系统库存扣减超时，不一定需要马上引入 Redis",{"type":7,"value":8,"toc":923},"minimark",[9,13,18,21,32,40,43,59,65,68,72,77,83,88,99,104,118,127,129,133,136,140,148,152,169,177,179,183,186,191,206,210,221,225,239,247,249,253,256,259,262,265,362,365,368,371,373,377,381,392,396,399,403,406,414,418,421,423,427,433,436,450,452,456,460,463,466,477,481,484,498,501,505,511,513,527,529,533,536,539,545,551,553,557,560,566,569,572,583,585,589,595,598,604,606,610,614,620,625,642,648,650,654,659,663,677,682,684,688,693,697,711,719,721,725,728,731,737,740,754,757,759,763,765,854,856,860,863,871,874,891,894,896,900,906,909,920],[10,11,12],"p",{},"背景：\n朋友公司订单系统，架构古老，\n技术栈繁多，C\u002FS、B\u002FS混合，主库Oracle，经常遇到部分商品售卖过程中，出现卡顿的情况。经常观察和定位后，发现是库存扣减出现的偶发性超时。人力有限也不想大改系统架构，就一起讨论了一下后续可落地的一些小方案和建议。",[14,15,17],"h2",{"id":16},"一问题本质不是锁机制有问题而是热点行被反复争抢","一、问题本质：不是锁机制有问题，而是热点行被反复争抢",[10,19,20],{},"很多老系统的库存扣减逻辑大致是这样的：",[22,23,28],"pre",{"className":24,"code":26,"language":27},[25],"language-text","update t_stock\nset stock = stock - #{num}\nwhere sku_id = #{skuId}\n  and stock >= #{num};\n","text",[29,30,26],"code",{"__ignoreMap":31},"",[10,33,34,35,39],{},"这条 SQL 本身是原子的，Oracle 行锁也没有问题。\n问题在于：",[36,37,38],"strong",{},"同一个 SKU 的所有请求都在更新同一行记录","。",[10,41,42],{},"当某个商品在短时间内出现多单同时扣减时：",[44,45,46,50,53,56],"ul",{},[47,48,49],"li",{},"第一个请求拿到行锁；",[47,51,52],{},"后续请求必须等待前一个事务提交或回滚；",[47,54,55],{},"等待队列越来越长；",[47,57,58],{},"接口超时、事务堆积、部分订单失败。",[10,60,61,62,39],{},"这就是典型的",[36,63,64],{},"热点行锁竞争",[66,67],"hr",{},[14,69,71],{"id":70},"二为什么常见方案不一定适合存量系统","二、为什么常见方案不一定适合存量系统",[73,74,76],"h3",{"id":75},"_1-乐观锁-version","1. 乐观锁 Version",[22,78,81],{"className":79,"code":80,"language":27},[25],"update t_stock\nset stock = stock - #{num}, version = version + 1\nwhere sku_id = #{skuId}\n  and version = #{oldVersion}\n  and stock >= #{num};\n",[29,82,80],{"__ignoreMap":31},[10,84,85],{},[36,86,87],{},"优点：",[44,89,90,93,96],{},[47,91,92],{},"改造成本低；",[47,94,95],{},"不需要改表结构太多；",[47,97,98],{},"非热点商品可用。",[10,100,101],{},[36,102,103],{},"缺点：",[44,105,106,109,112,115],{},[47,107,108],{},"热点 SKU 会出现大量版本冲突；",[47,110,111],{},"大量请求反复重试；",[47,113,114],{},"CPU 压力上升；",[47,116,117],{},"不一定能明显改善超时问题。",[10,119,120,123,124,39],{},[36,121,122],{},"结论：","\n适合并发不高的普通商品，",[36,125,126],{},"不建议作为热点库存问题的主方案",[66,128],{},[73,130,132],{"id":131},"_2-应用层队列串行化","2. 应用层队列串行化",[10,134,135],{},"把同一个 SKU 的扣减请求排队，串行进入数据库。",[10,137,138],{},[36,139,87],{},[44,141,142,145],{},[47,143,144],{},"数据库侧逻辑改动较小；",[47,146,147],{},"可以避免并发行锁。",[10,149,150],{},[36,151,103],{},[44,153,154,157,160,163,166],{},[47,155,156],{},"多技术栈系统改造复杂；",[47,158,159],{},"C\u002FS、B\u002FS、Java、C#、PB 都可能要适配；",[47,161,162],{},"响应时间变长；",[47,164,165],{},"队列成为新瓶颈；",[47,167,168],{},"多实例部署还需要 SKU 路由。",[10,170,171,173,174,39],{},[36,172,122],{},"\n适合作为辅助削峰手段，",[36,175,176],{},"不太适合作为存量系统第一改造方案",[66,178],{},[73,180,182],{"id":181},"_3-redis-预扣-mq-异步落库","3. Redis 预扣 + MQ 异步落库",[10,184,185],{},"这是大促、秒杀场景常见方案。",[10,187,188],{},[36,189,190],{},"思路：",[192,193,194,197,200,203],"ol",{},[47,195,196],{},"Redis 先做库存预扣；",[47,198,199],{},"扣成功后发送 MQ；",[47,201,202],{},"消费端异步更新 Oracle；",[47,204,205],{},"订单、支付、取消、回补、对账链路重新设计。",[10,207,208],{},[36,209,87],{},[44,211,212,215,218],{},[47,213,214],{},"可以承受更高瞬时流量；",[47,216,217],{},"Redis 承担入口洪峰；",[47,219,220],{},"适合秒杀、大促场景。",[10,222,223],{},[36,224,103],{},[44,226,227,230,233,236],{},[47,228,229],{},"引入 Redis、MQ、对账、补偿、回补等多个组件；",[47,231,232],{},"一致性从强一致变成最终一致；",[47,234,235],{},"老系统风险高；",[47,237,238],{},"如果 Oracle 还是单行库存，消费端依然可能出现行锁等待。",[10,240,241,243,244,39],{},[36,242,122],{},"\n对存量混合技术栈系统来说，",[36,245,246],{},"风险较高，不建议作为第一步",[66,248],{},[14,250,252],{"id":251},"三推荐主方案oracle-库存分桶","三、推荐主方案：Oracle 库存分桶",[73,254,255],{"id":255},"核心思路",[10,257,258],{},"把一个逻辑 SKU 的库存，拆成多个物理桶。",[10,260,261],{},"例如：",[10,263,264],{},"表格",[266,267,268,284],"table",{},[269,270,271],"thead",{},[272,273,274,278,281],"tr",{},[275,276,277],"th",{},"sku_id",[275,279,280],{},"bucket_no",[275,282,283],{},"stock",[285,286,287,299,308,317,326,335,344,353],"tbody",{},[272,288,289,293,296],{},[290,291,292],"td",{},"1001",[290,294,295],{},"1",[290,297,298],{},"125",[272,300,301,303,306],{},[290,302,292],{},[290,304,305],{},"2",[290,307,298],{},[272,309,310,312,315],{},[290,311,292],{},[290,313,314],{},"3",[290,316,298],{},[272,318,319,321,324],{},[290,320,292],{},[290,322,323],{},"4",[290,325,298],{},[272,327,328,330,333],{},[290,329,292],{},[290,331,332],{},"5",[290,334,298],{},[272,336,337,339,342],{},[290,338,292],{},[290,340,341],{},"6",[290,343,298],{},[272,345,346,348,351],{},[290,347,292],{},[290,349,350],{},"7",[290,352,298],{},[272,354,355,357,360],{},[290,356,292],{},[290,358,359],{},"8",[290,361,298],{},[10,363,364],{},"总库存仍然是 1000，但同一个 SKU 不再只有一行。",[10,366,367],{},"扣减时，数据库随机选择一个有足够库存的桶进行更新。",[10,369,370],{},"这样，并发请求会分散到不同行，而不是全部挤在同一行。",[66,372],{},[14,374,376],{"id":375},"四分桶方案为什么适合存量系统","四、分桶方案为什么适合存量系统",[73,378,380],{"id":379},"_1-风险相对可控","1. 风险相对可控",[10,382,383,384,387,388,391],{},"主要改造集中在",[36,385,386],{},"库存表结构","和",[36,389,390],{},"库存扣减 \u002F 归还逻辑","，不一定要一开始就引入 Redis 和 MQ。",[73,393,395],{"id":394},"_2-仍然保留数据库事务边界","2. 仍然保留数据库事务边界",[10,397,398],{},"订单和库存可以尽量保持原有事务模型，不像 Redis + MQ 那样需要彻底重构链路。",[73,400,402],{"id":401},"_3-可以按-sku-灰度","3. 可以按 SKU 灰度",[10,404,405],{},"不是所有商品都必须分桶。",[44,407,408,411],{},[47,409,410],{},"热点 SKU：分 4 桶、8 桶或 16 桶；",[47,412,413],{},"普通 SKU：分 1 桶，等价于原来的单行模式。",[73,415,417],{"id":416},"_4-对多技术栈系统更友好","4. 对多技术栈系统更友好",[10,419,420],{},"库存分桶本质上是数据库侧的结构调整，只要各端最终调用同一套库存服务或存储过程，就可以逐步收敛。",[66,422],{},[14,424,426],{"id":425},"五表结构建议","五、表结构建议",[22,428,431],{"className":429,"code":430,"language":27},[25],"create table t_sku_stock (\n  id number primary key,\n  sku_id number not null,\n  bucket_no number not null,\n  stock number not null default 0,\n  create_time date default sysdate,\n  update_time date default sysdate,\n  constraint uk_sku_bucket unique (sku_id, bucket_no)\n);\n",[29,432,430],{"__ignoreMap":31},[10,434,435],{},"说明：",[44,437,438,444,447],{},[47,439,440,443],{},[29,441,442],{},"sku_id + bucket_no"," 是逻辑唯一键；",[47,445,446],{},"每个 SKU 预先创建 1~N 个桶；",[47,448,449],{},"冷 SKU 可以只保留 1 个桶。",[66,451],{},[14,453,455],{"id":454},"六桶是怎么分配的","六、桶是怎么分配的？",[73,457,459],{"id":458},"_1-桶不是哈希出来的","1. 桶不是哈希出来的",[10,461,462],{},"不要根据订单号、用户号做哈希取模来固定分配桶。",[10,464,465],{},"这种方式容易导致：",[44,467,468,471,474],{},[47,469,470],{},"某个桶先被扣光；",[47,472,473],{},"其他桶还有库存；",[47,475,476],{},"业务提前返回库存不足。",[73,478,480],{"id":479},"_2-桶是预先生成的","2. 桶是预先生成的",[10,482,483],{},"每个 SKU 在上架或初始化时，预先创建固定编号的桶：",[44,485,486,489,492,495],{},[47,487,488],{},"1 桶",[47,490,491],{},"4 桶",[47,493,494],{},"8 桶",[47,496,497],{},"16 桶",[10,499,500],{},"具体数量取决于商品热点程度。",[73,502,504],{"id":503},"_3-扣减时动态选桶","3. 扣减时动态选桶",[22,506,509],{"className":507,"code":508,"language":27},[25],"update t_sku_stock\nset stock = stock - #{num},\n    update_time = sysdate\nwhere sku_id = #{skuId}\n  and bucket_no = (\n        select bucket_no\n        from t_sku_stock\n        where sku_id = #{skuId}\n          and stock >= #{num}\n        order by dbms_random.value\n        fetch first 1 row only\n      )\n  and stock >= #{num};\n",[29,510,508],{"__ignoreMap":31},[10,512,435],{},[44,514,515,518,521,524],{},[47,516,517],{},"子查询随机找一个有足够库存的桶；",[47,519,520],{},"外层 update 只锁这一行；",[47,522,523],{},"返回行数大于 0，表示扣减成功；",[47,525,526],{},"返回 0，业务层可以有限重试。",[66,528],{},[14,530,532],{"id":531},"七归还库存怎么处理","七、归还库存怎么处理？",[10,534,535],{},"订单取消、退货时，不需要严格归还到原来扣减的那个桶。",[10,537,538],{},"可以随机选择一个桶归还：",[22,540,543],{"className":541,"code":542,"language":27},[25],"update t_sku_stock\nset stock = stock + #{num},\n    update_time = sysdate\nwhere sku_id = #{skuId}\n  and bucket_no = (\n        select bucket_no\n        from t_sku_stock\n        where sku_id = #{skuId}\n        order by dbms_random.value\n        fetch first 1 row only\n      );\n",[29,544,542],{"__ignoreMap":31},[10,546,547,550],{},[36,548,549],{},"注意：","\n归还逻辑必须保证最终总库存正确，避免漏还、重复还。",[66,552],{},[14,554,556],{"id":555},"八总库存怎么查询","八、总库存怎么查询？",[10,558,559],{},"真实总库存可以通过汇总得到：",[22,561,564],{"className":562,"code":563,"language":27},[25],"select sum(stock)\nfrom t_sku_stock\nwhere sku_id = #{skuId};\n",[29,565,563],{"__ignoreMap":31},[10,567,568],{},"但不建议把这个 SQL 放在下单主链路中高频执行。",[10,570,571],{},"更合适的方式是：",[44,573,574,577,580],{},[47,575,576],{},"下单链路只关心扣减是否成功；",[47,578,579],{},"前台展示库存读取缓存；",[47,581,582],{},"后台对账、管理页面再使用 sum 查询。",[66,584],{},[14,586,588],{"id":587},"九存储过程初始化示例","九、存储过程初始化示例",[22,590,593],{"className":591,"code":592,"language":27},[25],"create or replace procedure proc_init_sku_bucket(\n  p_sku_id      in  number,\n  p_total_stock in  number,\n  p_bucket_cnt  in  number,\n  p_ret_code    out number,\n  p_ret_msg     out varchar2\n)\nis\n  v_avg number;\n  v_mod number;\n  v_stock number;\n  v_exists number;\nbegin\n  p_ret_code := 0;\n  p_ret_msg  := 'success';\n\n  if p_bucket_cnt \u003C 1 then\n    p_ret_code := -1;\n    p_ret_msg  := 'bucket count must >= 1';\n    return;\n  end if;\n\n  if p_total_stock \u003C 0 then\n    p_ret_code := -1;\n    p_ret_msg  := 'stock can not \u003C 0';\n    return;\n  end if;\n\n  select count(1)\n  into v_exists\n  from t_sku_stock\n  where sku_id = p_sku_id;\n\n  if v_exists > 0 then\n    p_ret_code := -1;\n    p_ret_msg  := 'sku bucket already exists';\n    return;\n  end if;\n\n  v_avg := trunc(p_total_stock \u002F p_bucket_cnt);\n  v_mod := mod(p_total_stock, p_bucket_cnt);\n\n  for i in 1 .. p_bucket_cnt loop\n    if i \u003C= v_mod then\n      v_stock := v_avg + 1;\n    else\n      v_stock := v_avg;\n    end if;\n\n    insert into t_sku_stock(sku_id, bucket_no, stock, create_time, update_time)\n    values(p_sku_id, i, v_stock, sysdate, sysdate);\n  end loop;\n\n  commit;\n\nexception\n  when others then\n    rollback;\n    p_ret_code := -2;\n    p_ret_msg  := 'error: ' || sqlerrm;\nend proc_init_sku_bucket;\n\u002F\n",[29,594,592],{"__ignoreMap":31},[10,596,597],{},"调用方式：",[22,599,602],{"className":600,"code":601,"language":27},[25],"declare\n  v_code number;\n  v_msg  varchar2(200);\nbegin\n  proc_init_sku_bucket(\n    p_sku_id      => 1001,\n    p_total_stock => 1000,\n    p_bucket_cnt  => 8,\n    p_ret_code    => v_code,\n    p_ret_msg     => v_msg\n  );\n  dbms_output.put_line('code:' || v_code || ', msg:' || v_msg);\nend;\n\u002F\n",[29,603,601],{"__ignoreMap":31},[66,605],{},[14,607,609],{"id":608},"十存量系统可执行改造步骤","十、存量系统可执行改造步骤",[73,611,613],{"id":612},"阶段一止血先改热点-sku","阶段一：止血，先改热点 SKU",[10,615,616,619],{},[36,617,618],{},"目标："," 快速解决热点商品扣减超时问题。",[10,621,622],{},[36,623,624],{},"动作：",[192,626,627,630,633,636,639],{},[47,628,629],{},"识别最近发生超时、等待明显的 SKU；",[47,631,632],{},"新建分桶库存表或增加分桶字段；",[47,634,635],{},"对热点 SKU 初始化 4 桶或 8 桶；",[47,637,638],{},"把这些 SKU 的扣减逻辑切到分桶模式；",[47,640,641],{},"观察行锁等待和超时情况。",[10,643,644,647],{},[36,645,646],{},"建议：","\n不要一开始就全量改造所有商品。",[66,649],{},[73,651,653],{"id":652},"阶段二收敛库存服务","阶段二：收敛库存服务",[10,655,656,658],{},[36,657,618],{}," 避免 C\u002FS、B\u002FS、Java、C#、PB 各自维护一套不一致的扣减逻辑。",[10,660,661],{},[36,662,624],{},[192,664,665,668,671,674],{},[47,666,667],{},"抽象统一的库存扣减接口或存储过程；",[47,669,670],{},"各端通过统一入口操作库存；",[47,672,673],{},"区分 “扣减”“归还”“查询”“对账” 四类操作；",[47,675,676],{},"记录操作流水，便于问题追踪。",[10,678,679,681],{},[36,680,646],{},"\n这一步比单纯引入 Redis 更重要。\n存量系统很多问题不是技术单一问题，而是入口太多、逻辑分散。",[66,683],{},[73,685,687],{"id":686},"阶段三补充缓存与对账","阶段三：补充缓存与对账",[10,689,690,692],{},[36,691,618],{}," 降低数据库压力，同时保证库存数据可信。",[10,694,695],{},[36,696,624],{},[192,698,699,702,705,708],{},[47,700,701],{},"前台库存展示使用缓存；",[47,703,704],{},"定期以 Oracle 数据为准进行对账；",[47,706,707],{},"对异常差额进行人工或自动修复；",[47,709,710],{},"监控行锁等待、扣减耗时、失败率。",[10,712,713,715,716,39],{},[36,714,646],{},"\n缓存只承担展示和削峰，",[36,717,718],{},"不建议一开始就承担强一致库存扣减主逻辑",[66,720],{},[14,722,724],{"id":723},"十一是否需要上-redis-分桶","十一、是否需要上 Redis 分桶？",[10,726,727],{},"如果业务确实有明显的瞬时洪峰，例如大促、秒杀，可以考虑在数据库分桶之上，再增加 Redis 分桶。",[10,729,730],{},"推荐做法：",[22,732,735],{"className":733,"code":734,"language":27},[25],"请求 → Redis 分桶预扣 → MQ → Oracle 分桶更新\n",[29,736,734],{"__ignoreMap":31},[10,738,739],{},"其中：",[44,741,742,745,748,751],{},[47,743,744],{},"Redis 分桶：挡住入口洪峰；",[47,746,747],{},"Oracle 分桶：避免数据库行锁；",[47,749,750],{},"MQ 消息中携带 Redis 选中的 bucket_no；",[47,752,753],{},"Oracle 直接更新对应桶。",[10,755,756],{},"但这一步改造明显更大，适合作为第二阶段优化，而不是存量系统的首选起点。",[66,758],{},[14,760,762],{"id":761},"十二风险评估","十二、风险评估",[10,764,264],{},[266,766,767,786],{},[269,768,769],{},[272,770,771,774,777,780,783],{},[275,772,773],{},"方案",[275,775,776],{},"改造成本",[275,778,779],{},"一致性",[275,781,782],{},"风险",[275,784,785],{},"适合场景",[285,787,788,805,821,838],{},[272,789,790,793,796,799,802],{},[290,791,792],{},"乐观锁 Version",[290,794,795],{},"低",[290,797,798],{},"强一致",[290,800,801],{},"热点商品冲突明显",[290,803,804],{},"普通商品",[272,806,807,810,813,815,818],{},[290,808,809],{},"应用队列",[290,811,812],{},"中高",[290,814,798],{},[290,816,817],{},"多技术栈适配复杂",[290,819,820],{},"流量可控系统",[272,822,823,826,829,832,835],{},[290,824,825],{},"Redis + MQ",[290,827,828],{},"高",[290,830,831],{},"最终一致",[290,833,834],{},"链路复杂，一致性问题多",[290,836,837],{},"大促、秒杀",[272,839,840,843,846,848,851],{},[290,841,842],{},"Oracle 分桶",[290,844,845],{},"中",[290,847,798],{},[290,849,850],{},"结构调整，灰度得当风险可控",[290,852,853],{},"存量系统首选",[66,855],{},[14,857,859],{"id":858},"十三落地建议","十三、落地建议",[10,861,862],{},"对于古老、多技术栈混合、主库为 Oracle 的存量系统，我的建议是：",[864,865,866],"blockquote",{},[10,867,868],{},[36,869,870],{},"不要一上来就追求最先进架构，先从数据库分桶开始，把热点行锁问题控制住，再考虑是否引入 Redis。",[10,872,873],{},"更稳妥的改造顺序是：",[192,875,876,879,882,885,888],{},[47,877,878],{},"统一库存入口；",[47,880,881],{},"热点 SKU 分桶；",[47,883,884],{},"增加库存流水；",[47,886,887],{},"补充缓存与对账；",[47,889,890],{},"确有必要时，再叠加 Redis 分桶与 MQ。",[10,892,893],{},"这样既能解决当前超时问题，也不至于把老系统一次性拖入复杂的异步架构。",[66,895],{},[14,897,899],{"id":898},"十四小结","十四、小结",[10,901,902,903,39],{},"库存高并发扣减超时，表面上是接口慢，本质上常常是",[36,904,905],{},"同一个 SKU 热点行被大量事务争抢",[10,907,908],{},"对于存量 Oracle 系统，最值得先尝试的方案不是立刻引入 Redis 和 MQ，而是：",[44,910,911,914,917],{},[47,912,913],{},"先把单行库存拆成多个桶；",[47,915,916],{},"让并发请求分散到不同行；",[47,918,919],{},"再通过统一库存服务、缓存和对账逐步优化。",[10,921,922],{},"这是我目前认为比较适合这类系统的改造思路，不一定最炫，但通常更稳。",{"title":31,"searchDepth":924,"depth":924,"links":925},2,[926,927,933,936,942,943,948,949,950,951,956,957,958,959],{"id":16,"depth":924,"text":17},{"id":70,"depth":924,"text":71,"children":928},[929,931,932],{"id":75,"depth":930,"text":76},3,{"id":131,"depth":930,"text":132},{"id":181,"depth":930,"text":182},{"id":251,"depth":924,"text":252,"children":934},[935],{"id":255,"depth":930,"text":255},{"id":375,"depth":924,"text":376,"children":937},[938,939,940,941],{"id":379,"depth":930,"text":380},{"id":394,"depth":930,"text":395},{"id":401,"depth":930,"text":402},{"id":416,"depth":930,"text":417},{"id":425,"depth":924,"text":426},{"id":454,"depth":924,"text":455,"children":944},[945,946,947],{"id":458,"depth":930,"text":459},{"id":479,"depth":930,"text":480},{"id":503,"depth":930,"text":504},{"id":531,"depth":924,"text":532},{"id":555,"depth":924,"text":556},{"id":587,"depth":924,"text":588},{"id":608,"depth":924,"text":609,"children":952},[953,954,955],{"id":612,"depth":930,"text":613},{"id":652,"depth":930,"text":653},{"id":686,"depth":930,"text":687},{"id":723,"depth":924,"text":724},{"id":761,"depth":924,"text":762},{"id":858,"depth":924,"text":859},{"id":898,"depth":924,"text":899},"article","\u002Fimg\u002Fstock-no-redis.webp","2026-07-20","适用场景：多技术栈混合系统、部分热点商品在短时间内出现大量扣减请求，导致行锁等待、扣减超时。 设计原则：先止血、再优化；先数据库内解决问题，再谨慎引入缓存与消息队列。","md",{},true,"\u002Farticles\u002Fredis",{"title":5,"description":963},{"loc":967},"articles\u002F库存扣减超时，别急着上Redis","gHOu9LxtOKKXUHyEzw24BaOSlVo7X0HKZQ6N9J8KMD4",1793263560360]