库存预占与释放——防止超卖的核心机制

引言:超卖的代价 超卖:卖出的数量超过实际库存,导致无法发货。 超卖的后果: 客户投诉、差评 平台处罚(Amazon可能封店) 紧急采购成本高 品牌信誉受损 防止超卖的核心:库存预占机制。 一、库存模型设计 1.1 库存类型 ┌─────────────────────────────────────────────────────┐ │ 库存类型 │ ├─────────────────────────────────────────────────────┤ │ 实物库存 = WMS系统中的实际库存数量 │ │ │ │ 可售库存 = 实物库存 - 预占库存 - 锁定库存 │ │ │ │ 预占库存 = 订单已预占但未发货的库存 │ │ │ │ 锁定库存 = 因其他原因锁定的库存(盘点、质量问题等) │ │ │ │ 在途库存 = 采购已下单但未入库的库存 │ └─────────────────────────────────────────────────────┘ 1.2 库存计算公式 可售库存 = 实物库存 - 预占库存 - 锁定库存 其中: - 实物库存:从WMS同步 - 预占库存:OMS计算(订单预占) - 锁定库存:手动锁定或系统锁定 1.3 数据模型 -- SKU库存汇总表(OMS) CREATE TABLE t_sku_inventory ( id BIGINT PRIMARY KEY AUTO_INCREMENT, sku_id VARCHAR(32) NOT NULL COMMENT 'SKU编码', warehouse_id VARCHAR(32) NOT NULL COMMENT '仓库ID', physical_qty INT NOT NULL DEFAULT 0 COMMENT '实物库存(从WMS同步)', reserved_qty INT NOT NULL DEFAULT 0 COMMENT '预占库存', locked_qty INT NOT NULL DEFAULT 0 COMMENT '锁定库存', available_qty INT NOT NULL DEFAULT 0 COMMENT '可售库存(计算字段)', in_transit_qty INT NOT NULL DEFAULT 0 COMMENT '在途库存', version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本', updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_sku_warehouse (sku_id, warehouse_id), KEY idx_sku_id (sku_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='SKU库存汇总表'; -- 库存预占明细表 CREATE TABLE t_inventory_reservation ( id BIGINT PRIMARY KEY AUTO_INCREMENT, reservation_id VARCHAR(32) NOT NULL COMMENT '预占ID', order_id VARCHAR(32) NOT NULL COMMENT '订单号', sku_id VARCHAR(32) NOT NULL COMMENT 'SKU编码', warehouse_id VARCHAR(32) NOT NULL COMMENT '仓库ID', reserved_qty INT NOT NULL COMMENT '预占数量', status VARCHAR(16) NOT NULL DEFAULT 'RESERVED' COMMENT '状态:RESERVED/RELEASED/DEDUCTED', reserved_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '预占时间', released_at DATETIME COMMENT '释放时间', UNIQUE KEY uk_reservation_id (reservation_id), KEY idx_order_id (order_id), KEY idx_sku_warehouse (sku_id, warehouse_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='库存预占明细表'; 二、预占流程设计 2.1 预占时机 下单 ──> 支付 ──> 审核 ──> 拆单 ──> 下发WMS ──> 发货 │ │ └──────── 预占库存 ─────────────────>│ │ 扣减库存 两种预占策略: ...

2026-01-29 · maneng

库存预占与释放机制:高并发下的库存一致性保障

引言 想象这样一个场景:用户在购物车中看到商品显示"有货",提交订单时却提示"库存不足"。或者更糟糕的情况:订单已支付,仓库却发现没有库存可发货。这些问题的根源都指向一个核心能力:库存预占。 库存预占是OMS系统的生命线。它要解决的核心问题是:在下单到发货的整个周期内,如何保证已承诺给用户的商品库存不被其他订单抢走?本文将从第一性原理出发,系统性地探讨库存预占与释放机制的设计与实现。 库存预占的本质 什么是库存预占? 库存预占(Inventory Reservation)是指在订单创建时,为订单中的商品临时锁定库存,防止被其他订单占用。 库存状态转换: 可用库存 → 预占库存 → 已出库 ↓ 释放回可用(订单取消) 为什么需要库存预占? 场景1:下单到支付的时间窗口 用户操作流程: 1. 加入购物车(不预占) 2. 提交订单(预占库存)← 关键点 3. 支付(15分钟内) 4. 发货 问题:如果不预占,步骤2到步骤3之间,库存可能被其他用户购买 场景2:支付到发货的时间窗口 订单处理流程: 1. 用户支付成功 2. 订单进入待发货队列 3. 仓库拣货打包(可能需要几小时) 4. 发货 问题:如果不预占,步骤1到步骤4之间,库存可能被超卖 场景3:高并发秒杀 秒杀场景: - 库存:100件 - 请求:10000个并发请求 - 目标:保证只有前100个请求成功,且不超卖 核心挑战:如何在毫秒级时间内准确预占库存? 库存预占的核心目标 防止超卖:已售数量 ≤ 实际库存 保证时效:预占操作要快(毫秒级) 支持释放:订单取消/超时后自动释放 高并发:支持万级QPS的库存预占 数据一致性:分布式环境下保证强一致性 库存预占流程设计 标准预占流程 class InventoryReservationService: """库存预占服务""" def reserve(self, order): """ 预占库存的标准流程 1. 校验库存可用性 2. 锁定库存 3. 扣减可用库存 4. 增加预占库存 5. 记录预占日志 """ try: # 1. 校验库存 self._validate_inventory(order) # 2. 获取分布式锁 lock = self._acquire_lock(order.items) # 3. 执行预占 reservations = [] for item in order.items: reservation = self._reserve_single_item( sku_id=item.sku_id, warehouse_id=item.warehouse_id, quantity=item.quantity, order_id=order.order_id ) reservations.append(reservation) # 4. 释放锁 self._release_lock(lock) return ReservationResult( success=True, reservations=reservations ) except InsufficientStockException as e: return ReservationResult( success=False, error=str(e) ) def _reserve_single_item(self, sku_id, warehouse_id, quantity, order_id): """预占单个SKU的库存""" # 原子操作:扣减可用库存,增加预占库存 with self.db.transaction(): # 1. 查询当前库存(加行锁) inventory = self.db.query( """ SELECT available, reserved FROM inventory WHERE sku_id = %s AND warehouse_id = %s FOR UPDATE """, (sku_id, warehouse_id) ) # 2. 检查库存充足性 if inventory.available < quantity: raise InsufficientStockException( f"SKU {sku_id} 库存不足" ) # 3. 更新库存 self.db.execute( """ UPDATE inventory SET available = available - %s, reserved = reserved + %s WHERE sku_id = %s AND warehouse_id = %s """, (quantity, quantity, sku_id, warehouse_id) ) # 4. 记录预占记录 reservation = InventoryReservation( reservation_id=generate_id(), order_id=order_id, sku_id=sku_id, warehouse_id=warehouse_id, quantity=quantity, status='RESERVED', expire_at=datetime.now() + timedelta(minutes=30) ) self.db.insert('inventory_reservations', reservation) return reservation 数据库设计 -- 库存表 CREATE TABLE inventory ( id BIGINT PRIMARY KEY AUTO_INCREMENT, sku_id VARCHAR(64) NOT NULL, warehouse_id VARCHAR(64) NOT NULL, available INT NOT NULL DEFAULT 0, -- 可用库存 reserved INT NOT NULL DEFAULT 0, -- 预占库存 sold INT NOT NULL DEFAULT 0, -- 已售库存 total INT NOT NULL DEFAULT 0, -- 总库存 version INT NOT NULL DEFAULT 0, -- 乐观锁版本号 created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, UNIQUE KEY uk_sku_warehouse (sku_id, warehouse_id), INDEX idx_sku (sku_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 库存预占记录表 CREATE TABLE inventory_reservations ( id BIGINT PRIMARY KEY AUTO_INCREMENT, reservation_id VARCHAR(64) NOT NULL UNIQUE, order_id VARCHAR(64) NOT NULL, sku_id VARCHAR(64) NOT NULL, warehouse_id VARCHAR(64) NOT NULL, quantity INT NOT NULL, status VARCHAR(32) NOT NULL, -- RESERVED, CONSUMED, RELEASED expire_at DATETIME NOT NULL, -- 预占过期时间 created_at DATETIME NOT NULL, released_at DATETIME, INDEX idx_order (order_id), INDEX idx_expire (expire_at, status) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 库存变更日志表 CREATE TABLE inventory_change_logs ( id BIGINT PRIMARY KEY AUTO_INCREMENT, log_id VARCHAR(64) NOT NULL UNIQUE, sku_id VARCHAR(64) NOT NULL, warehouse_id VARCHAR(64) NOT NULL, change_type VARCHAR(32) NOT NULL, -- RESERVE, RELEASE, CONSUME quantity INT NOT NULL, before_available INT NOT NULL, after_available INT NOT NULL, order_id VARCHAR(64), created_at DATETIME NOT NULL, INDEX idx_sku (sku_id), INDEX idx_order (order_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 库存预占失败处理 失败场景分类 class ReservationFailureHandler: """预占失败处理器""" def handle_failure(self, order, failure_reason): """处理预占失败""" if isinstance(failure_reason, InsufficientStockException): return self._handle_insufficient_stock(order) elif isinstance(failure_reason, DistributedLockException): return self._handle_lock_timeout(order) elif isinstance(failure_reason, DatabaseException): return self._handle_db_error(order) else: return self._handle_unknown_error(order) def _handle_insufficient_stock(self, order): """处理库存不足""" # 1. 尝试部分预占 partial_result = self._try_partial_reservation(order) if partial_result.has_available_items(): return self._create_partial_order( order, partial_result.available_items ) # 2. 尝试跨仓调拨 transfer_result = self._try_warehouse_transfer(order) if transfer_result.success: return self._retry_reservation(order) # 3. 推荐替代商品 alternatives = self._find_alternatives(order) return ReservationResult( success=False, error="库存不足", alternatives=alternatives ) def _handle_lock_timeout(self, order): """处理锁超时""" # 重试策略:指数退避 for retry in range(3): time.sleep(0.1 * (2 ** retry)) # 100ms, 200ms, 400ms try: return self.reservation_service.reserve(order) except DistributedLockException: continue # 降级策略:异步处理 return self._async_reservation(order) 部分预占策略 class PartialReservationStrategy: """部分预占策略""" def try_partial_reservation(self, order): """尝试部分预占""" available_items = [] unavailable_items = [] for item in order.items: try: # 尝试预占单个商品 reservation = self.reservation_service.reserve_single_item( item ) available_items.append(item) except InsufficientStockException: unavailable_items.append(item) return PartialReservationResult( available_items=available_items, unavailable_items=unavailable_items, fulfillment_rate=len(available_items) / len(order.items) ) def should_accept_partial(self, result, order): """判断是否接受部分预占""" # 策略1:订单金额超过阈值,接受部分预占 if order.total_amount > 500 and result.fulfillment_rate > 0.8: return True # 策略2:重要商品都有货,接受部分预占 critical_items = [i for i in order.items if i.is_critical] if all(i in result.available_items for i in critical_items): return True # 策略3:用户设置允许部分发货 if order.allow_partial_shipment: return True return False 库存释放触发条件 自动释放场景 class InventoryReleaseService: """库存释放服务""" def __init__(self): # 定义释放规则 self.release_rules = [ # 规则1:订单取消 ReleaseRule( name="order_cancelled", condition=lambda order: order.status == 'CANCELLED', action=self._release_all ), # 规则2:支付超时 ReleaseRule( name="payment_timeout", condition=lambda order: ( order.status == 'PENDING_PAYMENT' and datetime.now() > order.payment_deadline ), action=self._release_all ), # 规则3:部分退货 ReleaseRule( name="partial_refund", condition=lambda order: order.has_partial_refund(), action=self._release_partial ), # 规则4:预占过期 ReleaseRule( name="reservation_expired", condition=lambda reservation: ( datetime.now() > reservation.expire_at ), action=self._release_expired ) ] def release_for_order(self, order): """为订单释放库存""" # 1. 查询订单的所有预占记录 reservations = self.reservation_repo.find_by_order(order.order_id) # 2. 执行释放 released_count = 0 for reservation in reservations: if reservation.status != 'RESERVED': continue try: self._release_reservation(reservation) released_count += 1 except Exception as e: self.logger.error( f"释放预占失败: {reservation.reservation_id}", exc_info=e ) return ReleaseResult( success=True, released_count=released_count ) def _release_reservation(self, reservation): """释放单个预占""" with self.db.transaction(): # 1. 更新库存(原子操作) affected_rows = self.db.execute( """ UPDATE inventory SET available = available + %s, reserved = reserved - %s WHERE sku_id = %s AND warehouse_id = %s AND reserved >= %s """, ( reservation.quantity, reservation.quantity, reservation.sku_id, reservation.warehouse_id, reservation.quantity ) ) if affected_rows == 0: raise InventoryMismatchException( f"预占库存不足: {reservation.reservation_id}" ) # 2. 更新预占记录状态 self.db.execute( """ UPDATE inventory_reservations SET status = 'RELEASED', released_at = NOW() WHERE reservation_id = %s """, (reservation.reservation_id,) ) # 3. 记录变更日志 self._log_inventory_change( sku_id=reservation.sku_id, warehouse_id=reservation.warehouse_id, change_type='RELEASE', quantity=reservation.quantity, order_id=reservation.order_id ) 定时释放任务 class ScheduledReleaseJob: """定时释放任务""" def run(self): """ 每分钟执行一次,释放过期预占 批量处理策略: 1. 查询过期预占(分批查询) 2. 批量释放库存 3. 批量更新预占状态 """ batch_size = 1000 offset = 0 while True: # 1. 查询过期预占 expired_reservations = self.reservation_repo.find_expired( limit=batch_size, offset=offset ) if not expired_reservations: break # 2. 按仓库和SKU分组 grouped = defaultdict(list) for res in expired_reservations: key = (res.sku_id, res.warehouse_id) grouped[key].append(res) # 3. 批量释放 for (sku_id, warehouse_id), reservations in grouped.items(): total_quantity = sum(r.quantity for r in reservations) # 批量更新库存 self.db.execute( """ UPDATE inventory SET available = available + %s, reserved = reserved - %s WHERE sku_id = %s AND warehouse_id = %s """, (total_quantity, total_quantity, sku_id, warehouse_id) ) # 批量更新预占状态 reservation_ids = [r.reservation_id for r in reservations] self.db.execute( """ UPDATE inventory_reservations SET status = 'RELEASED', released_at = NOW() WHERE reservation_id IN %s """, (tuple(reservation_ids),) ) offset += batch_size # 限流保护 time.sleep(0.1) 库存预占的并发控制 数据库层面的并发控制 方案1:悲观锁(SELECT FOR UPDATE) ...

2025-11-22 · maneng

MySQL事务与锁:并发控制的艺术

引言 “并发是计算机科学中最难的问题之一,因为它涉及时间、顺序和不确定性。” —— Leslie Lamport 在前两篇文章中,我们了解了MySQL如何通过索引实现快速查询,如何通过WAL日志保证数据持久化。但还有一个核心问题没有解决: 如何在高并发场景下保证数据一致性? 想象这样的场景: 双11零点,100万用户同时抢购一件库存只有10个的商品 每个用户都执行: 1. 读取库存 → 10 2. 判断库存足够 → 是 3. 扣减库存 → 库存 - 1 4. 创建订单 结果:卖出了100万件,但库存只扣了10个 💥 这就是并发控制的核心难题:如何让多个并发事务互不干扰,同时保证数据一致性? 今天,我们从第一性原理出发,深度剖析MySQL的并发控制机制: 无控制 → 锁机制 → MVCC → 隔离级别 → 死锁处理 混乱 串行化 读写分离 灵活平衡 自动恢复 ❌ ⚠️ ✅ ✅ ✅ 我们还将手写MVCC核心逻辑,彻底理解MySQL如何实现读写不阻塞。 一、问题的起点:并发导致的数据混乱 让我们从一个最经典的并发问题开始:电商库存扣减。 1.1 场景:秒杀商品超卖问题 需求: 商品:iPhone 16 Pro Max(库存10件) 活动:双11零点秒杀,原价9999元,秒杀价1元 预期:10个用户抢到,其余用户提示"已抢完" 无并发控制的实现: /** * 秒杀服务(无并发控制) */ @Service public class SeckillService { @Autowired private ProductMapper productMapper; @Autowired private OrderMapper orderMapper; /** * 秒杀下单(存在并发问题) */ public boolean seckill(Long productId, Long userId) { // 1. 读取库存 Product product = productMapper.selectById(productId); int stock = product.getStock(); // 2. 判断库存是否足够 if (stock <= 0) { return false; // 库存不足 } // 3. 扣减库存 product.setStock(stock - 1); productMapper.updateById(product); // 4. 创建订单 Order order = new Order(); order.setUserId(userId); order.setProductId(productId); order.setAmount(1.00); // 秒杀价1元 orderMapper.insert(order); return true; } } 并发测试: ...

2025-11-03 · maneng

MySQL事务与锁:并发控制的艺术

引言 “并发是计算机科学中最难的问题之一,因为它涉及时间、顺序和不确定性。” —— Leslie Lamport 在前两篇文章中,我们了解了MySQL如何通过索引实现快速查询,如何通过WAL日志保证数据持久化。但还有一个核心问题没有解决: 如何在高并发场景下保证数据一致性? 想象这样的场景: 双11零点,100万用户同时抢购一件库存只有10个的商品 每个用户都执行: 1. 读取库存 → 10 2. 判断库存足够 → 是 3. 扣减库存 → 库存 - 1 4. 创建订单 结果:卖出了100万件,但库存只扣了10个 💥 这就是并发控制的核心难题:如何让多个并发事务互不干扰,同时保证数据一致性? 今天,我们从第一性原理出发,深度剖析MySQL的并发控制机制: 无控制 → 锁机制 → MVCC → 隔离级别 → 死锁处理 混乱 串行化 读写分离 灵活平衡 自动恢复 ❌ ⚠️ ✅ ✅ ✅ 我们还将手写MVCC核心逻辑,彻底理解MySQL如何实现读写不阻塞。 一、问题的起点:并发导致的数据混乱 让我们从一个最经典的并发问题开始:电商库存扣减。 1.1 场景:秒杀商品超卖问题 需求: 商品:iPhone 16 Pro Max(库存10件) 活动:双11零点秒杀,原价9999元,秒杀价1元 预期:10个用户抢到,其余用户提示"已抢完" 无并发控制的实现: /** * 秒杀服务(无并发控制) */ @Service public class SeckillService { @Autowired private ProductMapper productMapper; @Autowired private OrderMapper orderMapper; /** * 秒杀下单(存在并发问题) */ public boolean seckill(Long productId, Long userId) { // 1. 读取库存 Product product = productMapper.selectById(productId); int stock = product.getStock(); // 2. 判断库存是否足够 if (stock <= 0) { return false; // 库存不足 } // 3. 扣减库存 product.setStock(stock - 1); productMapper.updateById(product); // 4. 创建订单 Order order = new Order(); order.setUserId(userId); order.setProductId(productId); order.setAmount(1.00); // 秒杀价1元 orderMapper.insert(order); return true; } } 并发测试: ...

2025-11-03 · maneng

事务实战:转账案例与并发控制

实战案例概览 案例 核心问题 解决方案 难点 转账业务 数据一致性、死锁 固定加锁顺序、悲观锁 多账户并发转账 秒杀抢购 超卖、高并发 乐观锁 + 限流 10000人抢100件商品 订单支付 重复支付、幂等性 悲观锁 + 唯一约束 防止重复扣款 红包发放 余额不足、公平性 悲观锁 + 事务隔离 1个红包被多人抢 积分扣减 负数积分 乐观锁 + 余额检查 并发扣减积分 案例1:转账业务 需求 用户A向用户B转账100元,要求: 余额不能为负数 转账过程中不能被打断 防止死锁 方案1:基础实现(有死锁风险) -- ❌ 可能死锁 -- 事务A:A向B转100 START TRANSACTION; UPDATE account SET balance = balance - 100 WHERE user_id = 'A'; -- 锁A UPDATE account SET balance = balance + 100 WHERE user_id = 'B'; -- 等待锁B COMMIT; -- 事务B:B向A转50(并发执行) START TRANSACTION; UPDATE account SET balance = balance - 50 WHERE user_id = 'B'; -- 锁B UPDATE account SET balance = balance + 50 WHERE user_id = 'A'; -- 等待锁A(死锁!) COMMIT; 方案2:固定加锁顺序(推荐) -- ✅ 避免死锁:按user_id升序加锁 -- 转账函数(伪代码) FUNCTION transfer(from_user, to_user, amount): -- 1. 固定加锁顺序(按user_id升序) first_user = MIN(from_user, to_user) second_user = MAX(from_user, to_user) START TRANSACTION; -- 2. 按顺序锁定账户 SELECT balance FROM account WHERE user_id = first_user FOR UPDATE; SELECT balance FROM account WHERE user_id = second_user FOR UPDATE; -- 3. 检查余额 IF from_user.balance < amount THEN ROLLBACK; RETURN "余额不足"; END IF; -- 4. 扣款和到账 UPDATE account SET balance = balance - amount WHERE user_id = from_user; UPDATE account SET balance = balance + amount WHERE user_id = to_user; COMMIT; RETURN "转账成功"; END FUNCTION; 方案3:Java实现(完整代码) @Service public class TransferService { @Autowired private AccountMapper accountMapper; @Transactional(rollbackFor = Exception.class) public void transfer(String fromUser, String toUser, BigDecimal amount) { // 1. 固定加锁顺序(避免死锁) String firstUser = fromUser.compareTo(toUser) < 0 ? fromUser : toUser; String secondUser = fromUser.compareTo(toUser) < 0 ? toUser : fromUser; // 2. 按顺序锁定账户(悲观锁) Account first = accountMapper.selectForUpdate(firstUser); Account second = accountMapper.selectForUpdate(secondUser); // 3. 检查余额 Account fromAccount = fromUser.equals(firstUser) ? first : second; if (fromAccount.getBalance().compareTo(amount) < 0) { throw new BusinessException("余额不足"); } // 4. 扣款和到账 accountMapper.updateBalance(fromUser, amount.negate()); // 扣款 accountMapper.updateBalance(toUser, amount); // 到账 // 5. 记录流水(可选) recordTransferLog(fromUser, toUser, amount); } } <!-- MyBatis Mapper --> <select id="selectForUpdate" resultType="Account"> SELECT * FROM account WHERE user_id = #{userId} FOR UPDATE </select> <update id="updateBalance"> UPDATE account SET balance = balance + #{amount} WHERE user_id = #{userId} </update> 案例2:秒杀抢购 需求 10000个用户抢购100件商品,要求: ...

2025-01-14 · maneng

乐观锁与悲观锁:应用场景对比

乐观锁 vs 悲观锁 核心思想 类型 核心思想 锁机制 冲突处理 适用场景 悲观锁 先加锁,再操作(悲观:总会冲突) 数据库锁(X锁、S锁) 阻塞等待 冲突频繁 乐观锁 先操作,提交时检查(乐观:很少冲突) 版本号、时间戳 重试或放弃 冲突少 1. 悲观锁(Pessimistic Lock) 定义 假设冲突一定会发生,每次读取数据前先加锁,其他事务无法修改数据。 实现方式 方式1:排他锁(FOR UPDATE) -- 加排他锁 START TRANSACTION; SELECT * FROM account WHERE id = 1 FOR UPDATE; -- 加X锁,其他事务阻塞 -- 修改数据 UPDATE account SET balance = 900 WHERE id = 1; COMMIT; -- 释放锁 方式2:共享锁(LOCK IN SHARE MODE) -- 加共享锁 START TRANSACTION; SELECT * FROM account WHERE id = 1 LOCK IN SHARE MODE; -- 加S锁,其他事务可读但不可写 -- 读取后再更新 UPDATE account SET balance = 900 WHERE id = 1; COMMIT; 应用场景 场景1:库存扣减(防止超卖) -- 秒杀场景:10000个用户抢100件商品 START TRANSACTION; -- 1. 加锁查询库存 SELECT stock FROM product WHERE id = 1001 FOR UPDATE; -- 悲观锁 -- stock = 100 -- 2. 检查库存 IF stock >= 1 THEN -- 3. 扣减库存 UPDATE product SET stock = stock - 1 WHERE id = 1001; -- 4. 创建订单 INSERT INTO orders (user_id, product_id) VALUES (123, 1001); COMMIT; ELSE ROLLBACK; -- 库存不足 END IF; 场景2:转账业务 ...

2025-01-14 · maneng

MySQL锁机制:全局锁、表锁、行锁

MySQL锁分类 按锁粒度分类 全局锁(Global Lock) └─ FTWRL(Flush Tables With Read Lock) 表锁(Table Lock) ├─ 表级锁 ├─ 元数据锁(MDL Lock) └─ 意向锁(Intention Lock) 行锁(Row Lock) ├─ 记录锁(Record Lock) ├─ 间隙锁(Gap Lock) └─ Next-Key Lock(Record + Gap) 按锁模式分类 锁模式 英文名 兼容性 应用场景 共享锁(S锁) Shared Lock 读读兼容,读写互斥 SELECT … LOCK IN SHARE MODE 排他锁(X锁) Exclusive Lock 完全互斥 UPDATE、DELETE、SELECT … FOR UPDATE 1. 全局锁(Global Lock) 定义 锁住整个数据库实例,只读不可写。 命令 -- 加全局读锁 FLUSH TABLES WITH READ LOCK; -- 简称FTWRL -- 此时其他会话: SELECT * FROM account WHERE id = 1; -- ✅ 可以读 UPDATE account SET balance = 900 WHERE id = 1; -- ❌ 阻塞 INSERT INTO account VALUES (2, 'B', 500); -- ❌ 阻塞 -- 释放锁 UNLOCK TABLES; 应用场景 全库逻辑备份(保证数据一致性): ...

2025-01-14 · maneng

MVCC多版本并发控制:原理与实现

什么是MVCC? MVCC(Multi-Version Concurrency Control,多版本并发控制) 是InnoDB实现高并发的核心机制。 核心思想: 每行数据有多个版本 读操作读取快照版本(不加锁) 写操作创建新版本(加锁) 读写不冲突,提高并发性能 适用隔离级别: ✅ READ COMMITTED ✅ REPEATABLE READ ❌ READ UNCOMMITTED(无需MVCC) ❌ SERIALIZABLE(完全加锁) MVCC的实现机制 1. 隐藏字段 InnoDB为每行数据添加三个隐藏字段: 字段名 长度 说明 DB_TRX_ID 6字节 最后修改该行的事务ID DB_ROLL_PTR 7字节 回滚指针,指向undo log DB_ROW_ID 6字节 隐藏主键(无主键时自动生成) -- 实际存储的行数据(用户不可见) ┌────┬──────┬─────────┬────────────┬─────────────┬────────────┐ │ id │ name │ balance │ DB_TRX_ID │ DB_ROLL_PTR │ DB_ROW_ID │ ├────┼──────┼─────────┼────────────┼─────────────┼────────────┤ │ 1 │ A │ 1000 │ 100 │ 0x7FA8... │ 1 │ └────┴──────┴─────────┴────────────┴─────────────┴────────────┘ 2. undo log版本链 每次修改数据,旧版本保存在undo log,形成版本链。 -- 初始数据 INSERT INTO account (id, name, balance) VALUES (1, 'A', 1000); -- DB_TRX_ID = 100 -- 事务101:修改余额 UPDATE account SET balance = 900 WHERE id = 1; -- DB_TRX_ID = 101,旧版本保存到undo log -- 事务102:再次修改 UPDATE account SET balance = 800 WHERE id = 1; -- DB_TRX_ID = 102,旧版本保存到undo log 版本链结构: ...

2025-01-14 · maneng

事务的隔离级别:Read Uncommitted、Read Committed、Repeatable Read、Serializable

为什么需要隔离级别? 并发事务可能产生三大问题: 脏读(Dirty Read):读到未提交的数据 不可重复读(Non-Repeatable Read):同一查询两次结果不同 幻读(Phantom Read):范围查询两次结果不同 隔离级别就是用来控制在多大程度上解决这些问题。 四种隔离级别 级别对比表 隔离级别 脏读 不可重复读 幻读 性能 应用场景 READ UNCOMMITTED(读未提交) ❌ 会 ❌ 会 ❌ 会 ⭐⭐⭐⭐ 几乎不用 READ COMMITTED(读已提交) ✅ 避免 ❌ 会 ❌ 会 ⭐⭐⭐ Oracle/PostgreSQL默认 REPEATABLE READ(可重复读) ✅ 避免 ✅ 避免 ⚠️ 部分避免 ⭐⭐ MySQL默认(推荐) SERIALIZABLE(串行化) ✅ 避免 ✅ 避免 ✅ 避免 ⭐ 严格一致性要求 1. READ UNCOMMITTED(读未提交) 特点:事务可以读取其他事务未提交的数据(脏读)。 演示:脏读问题 -- 设置隔离级别 SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -- 事务A START TRANSACTION; SELECT balance FROM account WHERE user_id = 'A'; -- 读到1000 -- 事务B(并发执行) START TRANSACTION; UPDATE account SET balance = 500 WHERE user_id = 'A'; -- 未提交 -- 此时事务A再次查询 -- 事务A SELECT balance FROM account WHERE user_id = 'A'; -- 读到500(脏读!) COMMIT; -- 事务B ROLLBACK; -- 回滚,余额恢复到1000 问题:事务A读到了事务B未提交的数据(500),但事务B最终回滚了,导致数据不一致。 ...

2025-01-14 · maneng

如约数科科技工作室

浙ICP备2025203501号

👀 本站总访问量 ...| 👤 访客数 ...| 📅 今日访问 ...