乐观锁完全指南(Python 实现版):原理与实战
·
乐观锁完全指南(Python 实现版):原理与实战
文档目标:使用 Python 实现乐观锁的核心机制,基于 MySQL 和
sqlalchemy演示版本号控制的完整流程。
适用人群:Python 后端开发、数据工程师
技术栈:Python 3.8+, MySQL 8.0, SQLAlchemy, Flask
一、引言:并发更新问题
在电商系统中,多个用户同时下单可能导致库存超卖:
# 用户A:查询库存
stock_a = db.query("SELECT stock, version FROM products WHERE id=1")
# 返回: stock=10, version=0
# 用户B:也查询库存
stock_b = db.query("SELECT stock, version FROM products WHERE id=1")
# 返回: stock=10, version=0
# 用户A:扣减库存
db.execute("UPDATE products SET stock=9, version=1 WHERE id=1 AND version=0")
# 用户B:也扣减库存(但 version 已变为 1)
db.execute("UPDATE products SET stock=9, version=1 WHERE id=1 AND version=0") # ❌ 失败!
👉 乐观锁通过 version 字段确保只有第一个请求成功,避免超卖。
二、乐观锁实现原理
核心思想
- ✅ 读取时不加锁
- 🔁 更新时检查版本号是否匹配
- ❌ 不匹配则拒绝更新
SQL 更新语句
UPDATE products
SET stock = %s, version = version + 1
WHERE id = %s AND version = %s;
- 如果返回
rowcount == 1→ 成功 - 如果返回
0→ 版本冲突,需重试
三、完整实现(Python + SQLAlchemy)
1. 环境准备
安装依赖
pip install sqlalchemy flask pymysql retrying
创建数据库表
CREATE DATABASE optimistic_lock_demo CHARACTER SET utf8mb4;
USE optimistic_lock_demo;
CREATE TABLE product (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
stock INT NOT NULL DEFAULT 0,
price DECIMAL(10,2),
version INT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 插入测试数据
INSERT INTO product (name, stock, price) VALUES ('iPhone 15', 10, 5999.00);
2. 数据库连接配置
# database.py
from sqlalchemy import create_engine, Column, Integer, String, DECIMAL, TIMESTAMP, text
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
import os
# 数据库配置
DATABASE_URL = "mysql+pymysql://root:123456@localhost:3306/optimistic_lock_demo"
# 创建引擎
engine = create_engine(
DATABASE_URL,
pool_size=10,
max_overflow=20,
pool_pre_ping=True, # 连接前检测
echo=False # 开发环境可设为 True 查看 SQL
)
# 基类
Base = declarative_base()
# 创建会话
SessionLocal = sessionmaker(bind=engine)
3. ORM 模型定义
# models.py
from .database import Base
from sqlalchemy import Column, Integer, String, DECIMAL, TIMESTAMP, text
class Product(Base):
__tablename__ = 'product'
id = Column(Integer, primary_key=True, index=True)
name = Column(String(100), nullable=False)
stock = Column(Integer, nullable=False, default=0)
price = Column(DECIMAL(10, 2))
version = Column(Integer, nullable=False, default=0)
created_at = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'))
updated_at = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'))
def __repr__(self):
return f"<Product(id={self.id}, name='{self.name}', stock={self.stock}, version={self.version})>"
4. 服务层(带重试逻辑)
# services.py
from sqlalchemy.exc import SQLAlchemyError
from .models import Product
from .database import SessionLocal
from retrying import retry
import logging
# 配置日志
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class ProductService:
def __init__(self):
self.session = SessionLocal()
def get_product(self, product_id: int) -> Product:
"""查询商品"""
product = self.session.query(Product).filter(Product.id == product_id).first()
if not product:
raise ValueError(f"商品不存在: {product_id}")
return product
@retry(
stop_max_attempt_number=5, # 最大重试5次
wait_exponential_multiplier=100, # 指数退避:100ms, 200ms, 400ms...
retry_on_exception=lambda ex: isinstance(ex, ValueError) and "版本冲突" in str(ex)
)
def deduct_stock(self, product_id: int, count: int) -> bool:
"""
扣减库存(乐观锁 + 自动重试)
"""
try:
# 1. 查询当前商品信息(包含版本号)
product = self.get_product(product_id)
logger.info(f"查询商品: {product}")
# 2. 检查库存
if product.stock < count:
raise ValueError(f"库存不足: 当前{product.stock}, 请求{count}")
# 3. 计算新库存和版本号
new_stock = product.stock - count
old_version = product.version
new_version = old_version + 1
# 4. 执行乐观锁更新
result = self.session.query(Product)\
.filter(Product.id == product_id, Product.version == old_version)\
.update({
Product.stock: new_stock,
Product.version: new_version
})
# 5. 提交事务
self.session.commit()
if result == 1:
logger.info(f"✅ 扣减成功: 新库存={new_stock}, 新版本={new_version}")
return True
else:
self.session.rollback()
raise ValueError(f"版本冲突: 商品ID={product_id}, 期望版本={old_version}, 当前可能已更新")
except Exception as e:
self.session.rollback()
logger.error(f"扣减库存失败: {e}")
raise
finally:
self.session.close()
def close(self):
self.session.close()
✅ 关键点:
@retry装饰器自动处理重试filter(..., version=old_version)实现乐观锁update()返回影响行数,0表示版本不匹配
5. Flask API 接口
# app.py
from flask import Flask, request, jsonify
from .services import ProductService
app = Flask(__name__)
product_service = ProductService()
@app.route('/product/<int:product_id>', methods=['GET'])
def get_product(product_id):
try:
product = product_service.get_product(product_id)
return jsonify({
'id': product.id,
'name': product.name,
'stock': product.stock,
'price': float(product.price) if product.price else None,
'version': product.version
})
except Exception as e:
return jsonify({'error': str(e)}), 400
@app.route('/product/<int:product_id>/deduct', methods=['POST'])
def deduct_stock(product_id):
try:
data = request.get_json()
count = data.get('count', 1)
success = product_service.deduct_stock(product_id, count)
if success:
return jsonify({'message': '库存扣减成功'})
else:
return jsonify({'error': '库存扣减失败'}), 400
except ValueError as ve:
return jsonify({'error': f'业务错误: {ve}'}), 400
except Exception as e:
return jsonify({'error': f'系统错误: {e}'}), 500
@app.route('/health', methods=['GET'])
def health():
return jsonify({'status': 'OK'})
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=True)
6. 测试脚本(模拟并发)
# test_concurrent.py
import requests
import threading
import time
BASE_URL = "http://localhost:5000"
def deduct_stock(user_id, product_id, count):
url = f"{BASE_URL}/product/{product_id}/deduct"
payload = {"count": count}
start_time = time.time()
response = requests.post(url, json=payload)
end_time = time.time()
print(f"[用户{user_id}] 状态码: {response.status_code}, 响应: {response.json()}, 耗时: {end_time - start_time:.2f}s")
# 场景:两个用户同时扣减库存
if __name__ == "__main__":
product_id = 1
count = 1
# 创建两个线程模拟并发请求
t1 = threading.Thread(target=deduct_stock, args=(1, product_id, count))
t2 = threading.Thread(target=deduct_stock, args=(2, product_id, count))
t1.start()
t2.start()
t1.join()
t2.join()
# 查看最终结果
final = requests.get(f"{BASE_URL}/product/{product_id}")
print(f"\n最终商品状态: {final.json()}")
四、运行与测试
1. 启动服务
python app.py
2. 执行并发测试
python test_concurrent.py
3. 预期输出
[用户1] 状态码: 200, 响应: {'message': '库存扣减成功'}, 耗时: 0.12s
[用户2] 状态码: 400, 响应: {'error': '业务错误: 版本冲突: 商品ID=1...'}, 耗时: 0.11s
最终商品状态: {'id': 1, 'name': 'iPhone 15', 'stock': 9, 'price': 5999.0, 'version': 1}
👉 只有一个请求成功,库存正确从 10 → 9,避免了超卖。
五、核心优势总结
| 特性 | 说明 |
|---|---|
| 无阻塞读 | 查询性能高,适合读多写少场景 |
| 自动重试 | 结合 retrying 库简化开发 |
| 简单可靠 | 仅需一个 version 字段即可实现 |
| 易于调试 | 失败时明确提示“版本冲突” |
六、生产环境建议
- 重试策略优化:
- 使用指数退避(Exponential Backoff)
- 设置最大重试时间而非次数
- 监控告警:
- 记录乐观锁失败次数
- 高频失败可能预示热点数据问题
- 结合缓存:
- 使用 Redis 预减库存,减少数据库压力
- 分布式场景:
- 在微服务中可通过消息队列异步处理
七、结语
乐观锁是构建高性能、高并发系统的基石之一。
通过 Python + SQLAlchemy 的实现,你可以轻松在 Web 应用、订单系统、库存管理等场景中应用此模式,确保数据一致性的同时最大化系统吞吐量。
更多推荐

所有评论(0)