按 Enter 键跳转到正文

Mysql中的锁

MYSQL中的锁

一、 MySQL锁概述

MySQL锁机制是数据库管理系统实现并发控制的核心技术,它通过在共享资源上实施访问限制,确保数据的一致性和完整性。作为PHP高级开发者,深入理解MySQL锁机制对于构建高性能、高并发的Web应用至关重要。

二、 MySQL锁分类体系

1. 按锁的粒度划分

1. 表级锁

特点

  • 开销小,加锁快
  • 锁定整个表
  • 并发度低 使用场景
1-- 显式加表锁
2LOCK TABLES users READ;  -- 共享锁
3LOCK TABLES users WRITE; -- 排他锁
4
5-- 操作完成后释放
6UNLOCK TABLES;

注意事项:

  • MyISAM引擎默认使用表锁
  • 在InnoDB上应谨慎使用,会严重影响并发

2. 行级锁

特点

  • 开销大,加锁慢
  • 只锁定需要的行
  • 并发度高 使用场景
1-- 共享锁(S锁)
2SELECT * FROM accounts WHERE id = 1 LOCK IN SHARE MODE;
3-- MySQL 8.0+推荐语法
4SELECT * FROM accounts WHERE id = 1 FOR SHARE;
5
6-- 排他锁(X锁)
7SELECT * FROM accounts WHERE id = 1 FOR UPDATE;

注意事项:

  • MyISAM引擎默认使用表锁
  • 在InnoDB上应谨慎使用,会严重影响并发

2. 按锁的性质划分

1. 共享锁(S锁)

  • 允许多个事务同时读取
  • 阻止其他事务获取排他锁

2. 排他锁(X锁)

  • 独占锁,阻止其他任何锁
  • 自动加于UPDATE/DELETE操作

三、InnoDB高级锁机制

1. 意向锁(Intention Locks)

作用: 快速判断表中是否有行被锁定,避免全表扫描

  • IS锁:意向共享锁
  • IX锁:意向排他锁

2. 记录锁(Record Locks)

锁定索引中的特定记录:

1-- 锁定id=5的记录
2SELECT * FROM products WHERE id = 5 FOR UPDATE;

3. 间隙锁(Gap Locks)

锁定索引记录间的间隙,防止幻读:

1-- 锁定id在10到20之间的间隙
2SELECT * FROM products WHERE id BETWEEN 10 AND 20 FOR UPDATE;

4. 临键锁(Next-Key Locks)

InnoDB默认行锁算法,组合了记录锁和间隙锁:

  • 锁定记录及之前的间隙
  • 解决幻读问题的关键

5. 插入意向锁(Insert Intention Locks)

特殊间隙锁,允许不同事务在相同间隙插入不同记录,提高并发插入性能。

四、 锁的应用实践(PHP)

1. 悲观锁实现

 1// PHP中使用悲观锁示例
 2$pdo->beginTransaction();
 3try {
 4    // 1. 锁定账户记录
 5    $stmt = $pdo->prepare('SELECT * FROM accounts WHERE user_id = ? FOR UPDATE');
 6    $stmt->execute([$userId]);
 7    $account = $stmt->fetch();
 8    
 9    // 2. 检查余额
10    if ($account['balance'] < $amount) {
11        throw new Exception('余额不足');
12    }
13    
14    // 3. 更新余额
15    $update = $pdo->prepare('UPDATE accounts SET balance = balance - ? WHERE user_id = ?');
16    $update->execute([$amount, $userId]);
17    
18    // 4. 记录交易
19    $insert = $pdo->prepare('INSERT INTO transactions (...) VALUES (...)');
20    $insert->execute([...]);
21    
22    $pdo->commit();
23} catch (Exception $e) {
24    $pdo->rollBack();
25    // 错误处理
26}

2. 乐观锁实现

 1// 使用版本号实现乐观锁
 2$pdo->beginTransaction();
 3try {
 4    // 1. 获取当前版本
 5    $stmt = $pdo->prepare('SELECT id, balance, version FROM accounts WHERE id = ?');
 6    $stmt->execute([$accountId]);
 7    $account = $stmt->fetch();
 8    
 9    // 2. 业务处理
10    $newBalance = $account['balance'] - $amount;
11    
12    // 3. 带版本检查的更新
13    $update = $pdo->prepare('UPDATE accounts SET balance = ?, version = version + 1 
14                            WHERE id = ? AND version = ?');
15    $affected = $update->execute([$newBalance, $accountId, $account['version']]);
16    
17    if ($affected === 0) {
18        throw new Exception('并发修改冲突');
19    }
20    
21    $pdo->commit();
22} catch (Exception $e) {
23    $pdo->rollBack();
24    // 错误处理
25}

五、锁的监控与优化

1. 锁等待监控

 1-- 查看当前锁等待情况
 2SHOW ENGINE INNODB STATUS;
 3
 4-- 查看锁等待统计
 5SHOW STATUS LIKE 'innodb_row_lock%';
 6
 7-- 查看正在执行的SQL和锁信息
 8SELECT * FROM information_schema.INNODB_TRX;
 9SELECT * FROM information_schema.INNODB_LOCKS;
10SELECT * FROM information_schema.INNODB_LOCK_WAITS;

2. 性能优化建议

  1. 索引优化:确保查询使用适当的索引,避免全表扫描导致表锁
  2. 事务设计:
    1. 尽量缩短事务长度
    2. 避免在事务中进行耗时操作(如网络请求)
  3. 隔离级别选择:根据业务需求选择合适的事务隔离级别
  4. 死锁预防:
    1. 按固定顺序访问多表
    2. 使用NOWAIT或SKIP LOCKED(MySQL 8.0+)
      1SELECT * FROM table FOR UPDATE NOWAIT;
      2SELECT * FROM table FOR UPDATE SKIP LOCKED;
      

六、常见问题解决方案

1. 死锁处理

当检测到死锁时,InnoDB会自动回滚代价较小的事务。开发者应:

  • 实现重试机制
  • 分析死锁日志优化业务逻辑

2. 锁等待超时

1-- 设置锁等待超时时间(秒)
2SET innodb_lock_wait_timeout = 50;

3. 高并发场景优化

  • 考虑使用Redis等缓存层减少数据库压力
  • 对于计数器等场景可使用原子操作
1UPDATE counters SET value = value + 1 WHERE id = 1;

七、Laravel ORM 中 MySQL 锁的应用

1.悲观锁在 Laravel 中的应用

1. 共享锁 (Shared Lock)

应用场景:读取数据时允许其他事务也读取,但阻止写入操作,适用于读多写少的场景。

1// 使用 sharedLock() 方法
2$products = Product::where('category_id', 5)
3            ->sharedLock()
4            ->get();
5
6// 等同于原生SQL: SELECT * FROM products WHERE category_id = 5 LOCK IN SHARE MODE

实际案例:生成报表时需要确保数据在读取过程中不被修改。

2. 排他锁 (Exclusive Lock)

应用场景:需要修改数据时使用,阻止其他事务读取或写入。

 1// 使用 lockForUpdate() 方法
 2$product = Product::where('stock', '>', 0)
 3            ->lockForUpdate()
 4            ->first();
 5
 6if ($product) {
 7    $product->decrement('stock');
 8    $product->save();
 9}
10
11// 等同于原生SQL: SELECT * FROM products WHERE stock > 0 LIMIT 1 FOR UPDATE

实际案例:库存扣减、账户余额变更等需要原子性操作的场景。

2. 事务中的锁使用

Laravel 提供了简洁的事务处理机制,结合锁使用可以确保数据一致性

 1DB::transaction(function () {
 2    $account = Account::where('user_id', 123)
 3                ->lockForUpdate()
 4                ->first();
 5    
 6    if ($account->balance >= 100) {
 7        $account->balance -= 100;
 8        $account->save();
 9        
10        Payment::create([
11            'account_id' => $account->id,
12            'amount' => 100
13        ]);
14    }
15});

最佳实践:

  1. 尽量缩短事务执行时间
  2. 锁应该尽早获取
  3. 按照固定顺序获取多个锁以避免死锁

3. 高级锁策略

1. 悲观锁的变体 (MySQL 8.0+)

应用场景:高并发环境下优化锁行为。

 1// 跳过被锁定的行
 2$products = Product::where('category_id', 5)
 3            ->skipLocked()
 4            ->get();
 5
 6// 等同于: SELECT * FROM products WHERE category_id = 5 SKIP LOCKED
 7
 8// 不等待立即返回
 9$product = Product::where('id', 10)
10            ->lockForUpdate()
11            ->noWait()
12            ->first();
13
14// 等同于: SELECT * FROM products WHERE id = 10 FOR UPDATE NOWAIT

实际案例:秒杀系统中处理高并发请求时。

2. 乐观锁实现

**应用场景:**读多写少且冲突较少的场景。 在模型中使用版本号:

1Schema::table('products', function (Blueprint $table) {
2    $table->integer('version')->default(0);
3});
 1$product = Product::find(1);
 2
 3// 模拟其他进程修改了数据
 4Product::where('id', 1)->update(['price' => 200, 'version' => DB::raw('version + 1')]);
 5
 6try {
 7    $product->price = 150;
 8    $product->save();
 9} catch (\Illuminate\Database\QueryException $e) {
10    // 捕获乐观锁冲突
11    if (str_contains($e->getMessage(), 'version conflict')) {
12        // 处理冲突
13    }
14}

自定义乐观锁实现:

 1public function save(array $options = [])
 2{
 3    if ($this->exists) {
 4        $affected = DB::table($this->getTable())
 5            ->where($this->getKeyName(), $this->getKey())
 6            ->where('version', $this->version)
 7            ->update([
 8                'price' => $this->price,
 9                'version' => DB::raw('version + 1'),
10                'updated_at' => $this->freshTimestamp(),
11            ]);
12        
13        if ($affected === 0) {
14            throw new OptimisticLockException('数据已被其他进程修改');
15        }
16        
17        return true;
18    }
19    
20    return parent::save($options);
21}

4. 锁的应用场景详解

1. 电商库存管理

 1DB::transaction(function () use ($productId, $quantity) {
 2    $product = Product::where('id', $productId)
 3                ->where('stock', '>=', $quantity)
 4                ->lockForUpdate()
 5                ->firstOrFail();
 6    
 7    $product->decrement('stock', $quantity);
 8    
 9    Order::create([
10        'product_id' => $productId,
11        'quantity' => $quantity
12    ]);
13});

2. 财务系统转账

 1try {
 2    DB::transaction(function () use ($from, $to, $amount) {
 3        $fromAccount = Account::where('id', $from)
 4                      ->lockForUpdate()
 5                      ->firstOrFail();
 6        
 7        $toAccount = Account::where('id', $to)
 8                    ->lockForUpdate()
 9                    ->firstOrFail();
10        
11        if ($fromAccount->balance < $amount) {
12            throw new InsufficientFundsException();
13        }
14        
15        $fromAccount->decrement('balance', $amount);
16        $toAccount->increment('balance', $amount);
17        
18        Transaction::create([
19            'from_account' => $from,
20            'to_account' => $to,
21            'amount' => $amount
22        ]);
23    });
24} catch (\Exception $e) {
25    Log::error('转账失败: '.$e->getMessage());
26    return back()->with('error', '操作失败,请重试');
27}

3. 分布式任务处理

 1$task = Task::where('status', 'pending')
 2        ->skipLocked()
 3        ->lockForUpdate()
 4        ->first();
 5
 6if ($task) {
 7    $task->update(['status' => 'processing']);
 8    // 处理任务...
 9    $task->update(['status' => 'completed']);
10}

5. 性能优化与陷阱规避

1. 常见性能问题

  1. 锁范围过大:
    1// 不好的做法 - 锁定了整个表
    2Product::lockForUpdate()->get();
    3
    4// 好的做法 - 只锁定需要的行
    5Product::where('id', 1)->lockForUpdate()->first();
    
  2. 长事务问题:
    1DB::transaction(function () {
    2    $data = Product::lockForUpdate()->first();
    3    // 这里执行了耗时操作(如API调用)
    4    // 应该把耗时操作移到事务外
    5});
    

2. 锁监控与诊断

 1// 记录慢查询和锁等待
 2DB::listen(function ($query) {
 3    if ($query->time > 100) { // 超过100ms的查询
 4        Log::channel('slow_queries')->info('Slow query', [
 5            'sql' => $query->sql,
 6            'bindings' => $query->bindings,
 7            'time' => $query->time
 8        ]);
 9    }
10});

3. 最佳实践总结

  1. 索引优化:确保锁操作使用索引,避免全表扫描
    1// 好的做法 - 使用索引列
    2Product::where('id', 1)->lockForUpdate()->first();
    3
    4// 不好的做法 - 无索引会导致表锁
    5Product::where('name', 'Laptop')->lockForUpdate()->first();
    
  2. 锁顺序:按照固定顺序获取锁避免死锁
    1// 正确的顺序
    2$accountA = Account::where('id', 1)->lockForUpdate()->first();
    3$accountB = Account::where('id', 2)->lockForUpdate()->first();
    4
    5// 错误的顺序(可能导致死锁)
    6// 事务1: 锁1然后锁2
    7// 事务2: 锁2然后锁1
    
  3. 隔离级别:根据业务需求设置合适的事务隔离级别
1config(['database.connections.mysql.isolation' => 'REPEATABLE READ']);

发表评论