目录
问题背景
在高并发场景下,多个事务同时操作同一条数据记录时,可能会出现数据不一致的问题。典型的场景包括:
- 库存扣减
- 账户余额更新
- 计数器累加
- 订单状态变更
并发更新问题的原因
1. 丢失更新(Lost Update)
当两个事务同时读取同一行数据,然后基于读取的值进行更新时,后提交的事务会覆盖先提交事务的修改。
时间线:
T1: 读取 balance = 100
T2: 读取 balance = 100
T1: 更新 balance = 100 - 10 = 90 (提交)
T2: 更新 balance = 100 - 20 = 80 (提交) ← T1的更新被覆盖2. 脏读、不可重复读、幻读
这些是事务隔离级别相关的问题,在较低的隔离级别下可能出现。
3. 竞态条件(Race Condition)
多个线程/进程竞争同一资源,执行顺序不确定导致结果不一致。
常见并发问题场景
场景1:库存扣减
# 问题代码示例
def decrease_stock(product_id, quantity):
# 第一步:查询当前库存
product = db.query("SELECT stock FROM products WHERE id = %s", product_id)
# 第二步:业务逻辑判断
if product.stock >= quantity:
new_stock = product.stock - quantity
# 第三步:更新库存
db.execute("UPDATE products SET stock = %s WHERE id = %s", new_stock, product_id)问题:在高并发下,多个请求可能同时通过库存检查,导致超卖。
场景2:账户转账
# 问题代码示例
def transfer(from_account, to_account, amount):
# 查询余额
from_acc = db.query("SELECT balance FROM accounts WHERE id = %s", from_account)
if from_acc.balance >= amount:
# 扣款
db.execute("UPDATE accounts SET balance = balance - %s WHERE id = %s", amount, from_account)
# 收款
db.execute("UPDATE accounts SET balance = balance + %s WHERE id = %s", amount, to_account)问题:如果中间步骤失败,可能导致资金不一致;并发操作可能导致余额计算错误。
解决方案详解
方案一:悲观锁(Pessimistic Locking)
原理
在读取数据时就加锁,阻止其他事务修改,直到当前事务完成。
实现方式
1. SELECT FOR UPDATE
START TRANSACTION;
-- 加锁读取
SELECT stock FROM products WHERE id = 1 FOR UPDATE;
-- 业务逻辑处理
-- ...
-- 更新数据
UPDATE products SET stock = stock - 10 WHERE id = 1;
COMMIT;Python示例:
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
def decrease_stock_pessimistic(product_id, quantity):
engine = create_engine(DATABASE_URL)
Session = sessionmaker(bind=engine)
session = Session()
try:
# 使用 for_update() 加锁
product = session.query(Product).filter(
Product.id == product_id
).with_for_update().first()
if product and product.stock >= quantity:
product.stock -= quantity
session.commit()
return True
else:
session.rollback()
return False
except Exception as e:
session.rollback()
raise e
finally:
session.close()优点
- ✅ 数据一致性强
- ✅ 实现简单直观
- ✅ 适合冲突频繁的场景
缺点
- ❌ 并发性能较差
- ❌ 可能导致死锁
- ❌ 长时间持有锁影响系统吞吐量
适用场景
- 数据冲突概率高的场景
- 对数据一致性要求极高的场景
- 短事务操作
方案二:乐观锁(Optimistic Locking)
原理
假设冲突很少发生,在更新时检查数据是否被修改过。
实现方式
1. 版本号机制
-- 表结构增加 version 字段
ALTER TABLE products ADD COLUMN version INT DEFAULT 0;
-- 更新时检查版本号
UPDATE products
SET stock = stock - 10, version = version + 1
WHERE id = 1 AND version = 5; -- 5是读取时的版本号Python示例:
def decrease_stock_optimistic(product_id, quantity, max_retries=3):
"""
使用乐观锁扣减库存,支持重试
"""
for attempt in range(max_retries):
try:
# 第一次查询:获取当前数据和版本号
product = db.query(
"SELECT stock, version FROM products WHERE id = %s",
product_id
)
if not product or product.stock < quantity:
return False
# 业务逻辑处理
new_stock = product.stock - quantity
new_version = product.version + 1
# 更新时检查版本号
rows_affected = db.execute(
"""UPDATE products
SET stock = %s, version = %s
WHERE id = %s AND version = %s""",
new_stock, new_version, product_id, product.version
)
if rows_affected > 0:
return True # 更新成功
else:
# 版本号不匹配,说明数据被修改过,重试
continue
except Exception as e:
if attempt == max_retries - 1:
raise e
continue
return False # 超过最大重试次数2. 时间戳机制
-- 使用 updated_at 字段代替 version
UPDATE products
SET stock = stock - 10, updated_at = NOW()
WHERE id = 1 AND updated_at = '2024-01-01 12:00:00';优点
- ✅ 并发性能好
- ✅ 不会长时间持有锁
- ✅ 适合读多写少的场景
缺点
- ❌ 需要应用层处理冲突和重试
- ❌ 高冲突场景下重试开销大
- ❌ 实现相对复杂
适用场景
- 数据冲突概率低的场景
- 读多写少的场景
- 可以接受重试的场景
方案三:原子操作(Atomic Operations)
原理
利用数据库的原子性,在单条SQL语句中完成读取和更新。
实现方式
1. 直接原子更新
-- 库存扣减(不会超卖)
UPDATE products
SET stock = stock - 10
WHERE id = 1 AND stock >= 10;
-- 检查影响行数
-- rows_affected = 1: 扣减成功
-- rows_affected = 0: 库存不足Python示例:
def decrease_stock_atomic(product_id, quantity):
"""
使用原子操作扣减库存
"""
result = db.execute(
"""UPDATE products
SET stock = stock - %s
WHERE id = %s AND stock >= %s""",
quantity, product_id, quantity
)
return result.rows_affected > 02. 使用 CASE WHEN 处理复杂逻辑
UPDATE products
SET stock = CASE
WHEN stock >= 10 THEN stock - 10
ELSE stock
END,
updated_at = NOW()
WHERE id = 1;3. 计数器累加
-- 点赞数累加(线程安全)
UPDATE articles
SET like_count = like_count + 1
WHERE id = 1;优点
- ✅ 性能最好
- ✅ 实现最简单
- ✅ 天然避免并发问题
- ✅ 无需事务或锁
缺点
- ❌ 只适合简单的增减操作
- ❌ 复杂业务逻辑难以实现
- ❌ 无法获取更新前的值进行复杂判断
适用场景
- 简单的数值增减操作
- 计数器、统计类场景
- 对性能要求极高的场景
方案四:分布式锁(Distributed Lock)
原理
在应用层使用外部存储(如Redis)实现分布式锁,控制并发访问。
实现方式
1. Redis分布式锁
import redis
import time
import uuid
class RedisLock:
def __init__(self, redis_client, lock_name, expire_time=10):
self.redis = redis_client
self.lock_name = f"lock:{lock_name}"
self.expire_time = expire_time
self.lock_value = str(uuid.uuid4())
def acquire(self, timeout=5):
"""
尝试获取锁
"""
start_time = time.time()
while True:
# SET NX EX: 只在键不存在时设置,并设置过期时间
if self.redis.set(
self.lock_name,
self.lock_value,
nx=True,
ex=self.expire_time
):
return True
# 超时检查
if time.time() - start_time > timeout:
return False
# 短暂等待后重试
time.sleep(0.01)
def release(self):
"""
释放锁(使用Lua脚本保证原子性)
"""
lua_script = """
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
"""
self.redis.eval(lua_script, 1, self.lock_name, self.lock_value)
# 使用示例
def decrease_stock_with_distributed_lock(product_id, quantity):
redis_client = redis.Redis(host='localhost', port=6379, db=0)
lock = RedisLock(redis_client, f"product_{product_id}")
if not lock.acquire(timeout=3):
raise Exception("获取锁失败,请稍后重试")
try:
# 执行业务逻辑
product = db.query("SELECT stock FROM products WHERE id = %s", product_id)
if product.stock >= quantity:
db.execute(
"UPDATE products SET stock = stock - %s WHERE id = %s",
quantity, product_id
)
return True
else:
return False
finally:
lock.release()2. Redlock算法(更可靠的分布式锁)
from redlock import Redlock
def decrease_stock_with_redlock(product_id, quantity):
dlm = Redlock([
{"host": "redis1.example.com", "port": 6379, "db": 0},
{"host": "redis2.example.com", "port": 6379, "db": 0},
{"host": "redis3.example.com", "port": 6379, "db": 0},
])
lock_key = f"product_{product_id}"
# 尝试获取锁,超时时间10秒
my_lock = dlm.lock(lock_key, 10000)
if not my_lock:
raise Exception("获取分布式锁失败")
try:
# 执行业务逻辑
# ...
pass
finally:
dlm.unlock(my_lock)优点
- ✅ 适用于分布式系统
- ✅ 可以跨服务、跨实例加锁
- ✅ 灵活性高
缺点
- ❌ 依赖外部组件(Redis等)
- ❌ 实现复杂
- ❌ 需要考虑锁失效、网络分区等问题
- ❌ 性能开销较大
适用场景
- 分布式系统环境
- 需要跨多个服务协调的场景
- 微服务架构
方案五:消息队列串行化(Message Queue Serialization)
原理
将并发请求放入消息队列,按顺序消费处理,实现串行化。
实现方式
import pika
import json
class StockUpdateQueue:
def __init__(self):
self.connection = pika.BlockingConnection(
pika.ConnectionParameters('localhost')
)
self.channel = self.connection.channel()
self.channel.queue_declare(queue='stock_updates', durable=True)
def publish_update(self, product_id, quantity):
"""
发布库存更新请求到队列
"""
message = {
'product_id': product_id,
'quantity': quantity,
'timestamp': time.time()
}
self.channel.basic_publish(
exchange='',
routing_key='stock_updates',
body=json.dumps(message),
properties=pika.BasicProperties(
delivery_mode=2, # 持久化
)
)
def consume_updates(self):
"""
消费库存更新请求(串行处理)
"""
def callback(ch, method, properties, body):
message = json.loads(body)
try:
# 串行处理库存更新
product = db.query(
"SELECT stock FROM products WHERE id = %s",
message['product_id']
)
if product.stock >= message['quantity']:
db.execute(
"UPDATE products SET stock = stock - %s WHERE id = %s",
message['quantity'],
message['product_id']
)
print(f"库存扣减成功: {message}")
ch.basic_ack(delivery_tag=method.delivery_tag)
except Exception as e:
print(f"处理失败: {e}")
ch.basic_nack(delivery_tag=method.delivery_tag, requeue=True)
self.channel.basic_qos(prefetch_count=1)
self.channel.basic_consume(
queue='stock_updates',
on_message_callback=callback
)
print("开始消费库存更新队列...")
self.channel.start_consuming()优点
- ✅ 完全避免并发冲突
- ✅ 可以削峰填谷
- ✅ 提高系统可用性
缺点
- ❌ 实时性较差
- ❌ 架构复杂度高
- ❌ 需要额外的基础设施
适用场景
- 对实时性要求不高的场景
- 高并发写入场景
- 需要异步处理的场景
方案六:数据库事务隔离级别调整
原理
通过调整事务隔离级别来控制并发行为。
MySQL隔离级别
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 并发性能 |
|---|---|---|---|---|
| READ UNCOMMITTED | ✓ | ✓ | ✓ | 最高 |
| READ COMMITTED | ✗ | ✓ | ✓ | 高 |
| REPEATABLE READ (默认) | ✗ | ✗ | ✓ | 中 |
| SERIALIZABLE | ✗ | ✗ | ✗ | 最低 |
实现方式
# 设置事务隔离级别
from sqlalchemy import create_engine, event
engine = create_engine(DATABASE_URL)
@event.listens_for(engine, "connect")
def set_isolation_level(dbapi_conn, connection_record):
cursor = dbapi_conn.cursor()
# 设置为串行化隔离级别(最严格)
cursor.execute("SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE")
cursor.close()
# 或者在会话级别设置
def update_with_serializable():
Session = sessionmaker(bind=engine)
session = Session()
try:
session.execute("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE")
# 执行业务逻辑
product = session.query(Product).filter(Product.id == 1).first()
if product.stock >= 10:
product.stock -= 10
session.commit()
except Exception as e:
session.rollback()
raise e
finally:
session.close()优点
- ✅ 数据库层面保证一致性
- ✅ 不需要修改业务代码
缺点
- ❌ SERIALIZABLE性能很差
- ❌ 可能导致大量事务回滚
- ❌ 不适合高并发场景
适用场景
- 低并发场景
- 对数据一致性要求极高的场景
- 作为其他方案的补充
方案对比与选择
综合对比表
| 方案 | 并发性能 | 实现复杂度 | 数据一致性 | 适用场景 |
|---|---|---|---|---|
| 悲观锁 | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | 高冲突、短事务 |
| 乐观锁 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | 低冲突、读多写少 |
| 原子操作 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | 简单增减操作 |
| 分布式锁 | ⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐ | 分布式系统 |
| 消息队列 | ⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐⭐ | 异步、高并发 |
| 隔离级别 | ⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | 低并发场景 |
选择指南
是否需要分布式支持?
├─ 是 → 使用分布式锁或消息队列
└─ 否 ↓
冲突概率高吗?
├─ 是 → 使用悲观锁
└─ 否 ↓
操作简单吗(只是增减)?
├─ 是 → 使用原子操作
└─ 否 ↓
可以接受重试吗?
├─ 是 → 使用乐观锁
└─ 否 → 使用悲观锁或提高隔离级别最佳实践建议
1. 优先使用原子操作
# ✅ 推荐:原子操作
UPDATE products SET stock = stock - 1 WHERE id = 1 AND stock > 0;
# ❌ 不推荐:先查后改
SELECT stock FROM products WHERE id = 1;
UPDATE products SET stock = 99 WHERE id = 1;2. 合理设置超时和重试
def update_with_retry(max_retries=3, base_delay=0.1):
for attempt in range(max_retries):
try:
# 执行更新操作
return do_update()
except OptimisticLockException:
if attempt < max_retries - 1:
# 指数退避
delay = base_delay * (2 ** attempt)
time.sleep(delay)
else:
raise3. 避免长事务
# ✅ 推荐:短事务
def short_transaction():
with session.begin():
product = session.query(Product).with_for_update().get(1)
product.stock -= 1
# 锁立即释放
# ❌ 不推荐:长事务
def long_transaction():
with session.begin():
product = session.query(Product).with_for_update().get(1)
product.stock -= 1
# 执行耗时的HTTP请求、文件IO等
call_external_api() # 锁长时间不释放4. 监控和告警
import logging
from prometheus_client import Counter, Histogram
# 定义指标
LOCK_WAIT_TIME = Histogram('lock_wait_seconds', 'Lock wait time')
CONFLICT_COUNT = Counter('optimistic_lock_conflicts', 'Optimistic lock conflicts')
def monitored_optimistic_update():
start_time = time.time()
try:
# 执行乐观锁更新
success = optimistic_update()
if not success:
CONFLICT_COUNT.inc()
finally:
LOCK_WAIT_TIME.observe(time.time() - start_time)5. 死锁预防
-- ✅ 推荐:固定顺序访问资源
-- 事务1和事务2都按相同顺序访问
UPDATE products WHERE id = 1;
UPDATE products WHERE id = 2;
-- ❌ 不推荐:不同顺序可能导致死锁
-- 事务1: UPDATE products WHERE id = 1; UPDATE products WHERE id = 2;
-- 事务2: UPDATE products WHERE id = 2; UPDATE products WHERE id = 1;6. 添加合适的索引
-- 确保WHERE条件的字段有索引,避免锁全表
CREATE INDEX idx_product_id ON products(id);
-- 对于范围查询,考虑覆盖索引
CREATE INDEX idx_stock_id ON products(stock, id);实际案例分析
案例:电商秒杀系统
需求:100个商品,10000用户同时抢购
方案设计:
# 方案组合:原子操作 + Redis预扣减 + 消息队列异步处理
class FlashSaleService:
def __init__(self):
self.redis = redis.Redis()
self.db = get_database_session()
def seckill(self, product_id, user_id):
"""
秒杀流程
"""
# 1. Redis预扣减(高性能)
stock_key = f"seckill:stock:{product_id}"
if self.redis.decr(stock_key) < 0:
# 库存不足,恢复计数
self.redis.incr(stock_key)
return {"success": False, "message": "库存不足"}
# 2. 防止重复购买
user_key = f"seckill:user:{product_id}:{user_id}"
if not self.redis.setnx(user_key, 1):
self.redis.incr(stock_key) # 恢复库存
return {"success": False, "message": "请勿重复购买"}
# 3. 设置过期时间
self.redis.expire(user_key, 3600)
# 4. 异步创建订单(消息队列)
order_queue.publish({
'product_id': product_id,
'user_id': user_id,
'timestamp': time.time()
})
return {"success": True, "message": "排队中,请稍候"}
def process_order_queue(self):
"""
消费队列,创建订单(串行处理)
"""
while True:
message = order_queue.consume()
try:
# 数据库原子操作最终确认
result = self.db.execute(
"""UPDATE products
SET stock = stock - 1
WHERE id = %s AND stock > 0""",
message['product_id']
)
if result.rows_affected > 0:
# 创建订单
self.create_order(message)
else:
# 库存不足,回滚Redis
self.redis.incr(f"seckill:stock:{message['product_id']}")
except Exception as e:
logger.error(f"订单处理失败: {e}")
# 异常处理...优势:
- Redis抗住高并发读写的压力
- 数据库只处理最终一致的订单创建
- 消息队列削峰填谷,保护数据库
- 用户体验好,快速响应
总结
MySQL并发更新问题的解决没有银弹,需要根据具体场景选择合适的方案:
- 简单场景:优先使用原子操作,性能最好
- 低冲突场景:使用乐观锁,兼顾性能和一致性
- 高冲突场景:使用悲观锁,保证数据正确性
- 分布式系统:使用分布式锁或消息队列
- 超高并发:组合方案(Redis + 消息队列 + 数据库)
关键原则:
- 尽量缩短事务和锁的持有时间
- 合理设计重试机制和降级策略
- 做好监控和告警
- 根据业务特点选择合适方案
- 充分测试并发场景
记住:过早优化是万恶之源,先从简单的方案开始,根据实际监控数据再逐步优化。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用。你还可以使用@来通知其他用户。