目录


问题背景

在高并发场景下,多个事务同时操作同一条数据记录时,可能会出现数据不一致的问题。典型的场景包括:

  • 库存扣减
  • 账户余额更新
  • 计数器累加
  • 订单状态变更

并发更新问题的原因

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 > 0

2. 使用 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:
                raise

3. 避免长事务

# ✅ 推荐:短事务
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并发更新问题的解决没有银弹,需要根据具体场景选择合适的方案:

  1. 简单场景:优先使用原子操作,性能最好
  2. 低冲突场景:使用乐观锁,兼顾性能和一致性
  3. 高冲突场景:使用悲观锁,保证数据正确性
  4. 分布式系统:使用分布式锁或消息队列
  5. 超高并发:组合方案(Redis + 消息队列 + 数据库)

关键原则:

  • 尽量缩短事务和锁的持有时间
  • 合理设计重试机制和降级策略
  • 做好监控和告警
  • 根据业务特点选择合适方案
  • 充分测试并发场景

记住:过早优化是万恶之源,先从简单的方案开始,根据实际监控数据再逐步优化。


PatWu16
82 声望6 粉丝

仰望星空,脚踏实地