乐观锁(Optimistic Locking)
乐观锁是一种假设数据库操作不会发生冲突的锁定机制。在执行数据更新操作时,它并不会立刻加锁,而是先允许所有事务继续执行,并在提交时检查数据是否发生了变化。如果数据在读取后被其他事务修改了,那么当前事务就会被阻止,并给出一个冲突的提示。
乐观锁的使用场景:
- 在并发冲突较少的环境中使用,适合读多写少的场景。
- 不需要过多地使用数据库锁,减少了资源消耗,提高了系统的效率。
工作原理:
- 每一行数据都有一个版本号(Version)或者时间戳(Timestamp),它在每次更新时会被递增或更新。
- 当一个事务准备提交时,它会检查该行数据的版本号是否发生了变化。如果版本号没有变化,则提交更新;如果版本号变化了,说明有其他事务修改了该数据,当前事务则会被回滚或抛出冲突异常。
乐观锁的实现:
- 通过版本号: 每条数据在数据库中有一个版本号字段,每次更新时都会检查版本号是否一致,若不一致则抛出异常。
SELECT * FROM account WHERE id = 1 AND version = 1;
UPDATE account SET balance = balance - 100, version = version + 1 WHERE id = 1 AND version = 1;
- 通过时间戳: 每条数据也可以通过时间戳来标识数据的修改时间,更新时验证修改时间是否一致。
优点:
- 对性能影响小,资源占用少。
- 可以并发处理,降低锁竞争。
缺点:
- 在高并发情况下,冲突概率大时,回滚和重试的成本可能增加。
悲观锁(Pessimistic Locking)
悲观锁是一种假设数据库操作会发生冲突的锁定机制。在执行数据更新操作时,事务会先加锁,其他事务在锁释放之前不能访问被锁定的数据,从而保证数据一致性。悲观锁通常是在读取数据时就加锁,直到事务结束才释放锁。
悲观锁的使用场景:
- 在并发冲突较多的环境中使用,适合写多读少的场景。
- 数据一致性要求高的情况下,适合使用悲观锁来避免数据被并发修改。
工作原理:
- 事务在访问数据时会给数据加锁,直到事务完成操作并提交,锁才会释放。其他事务在获取不到锁时会被阻塞,直到锁被释放。
- 悲观锁在数据库中通常通过
SELECT FOR UPDATE
语句来实现。
悲观锁的实现:
- 行级锁(SELECT FOR UPDATE): 在查询数据时加上
FOR UPDATE
关键字,保证当前数据行在事务提交之前不能被其他事务修改。
SELECT * FROM account WHERE id = 1 FOR UPDATE;
UPDATE account SET balance = balance - 100 WHERE id = 1;
- 数据库管理系统(DBMS)自动加锁: 例如,MySQL的InnoDB存储引擎在事务进行时会自动加锁,直到事务提交或回滚。
优点:
- 数据的冲突较少,避免了多次提交检查的开销。
- 数据一致性得到了严格保证。
缺点:
- 性能开销较大,可能导致死锁(特别是在多个事务同时进行时),并发性能差。
- 需要合理的锁粒度和锁管理,否则可能造成资源浪费。
总结对比:
特点 | 乐观锁 | 悲观锁 |
---|---|---|
锁的类型 | 读时不加锁,更新时检查版本号或时间戳 | 读取时就加锁,直到事务结束才释放锁 |
使用场景 | 并发冲突少,读多写少的场景 | 并发冲突多,写多读少的数据场景 |
实现方式 | 通过版本号、时间戳等方式实现 | 通过行级锁(SELECT FOR UPDATE)等实现 |
性能开销 | 性能较高,因为不加锁,只有提交时检查 | 性能较低,可能导致阻塞和死锁 |
优点 | 并发性能高,减少锁的竞争 | 确保数据一致性,避免了冲突 |
缺点 | 高并发情况下,回滚和重试的成本较高 | 性能差,可能导致死锁和资源占用 |
在实际开发中,选择乐观锁还是悲观锁取决于具体的场景:
- 对于冲突不频繁的场景(例如:库存、余额等),乐观锁是一个不错的选择;
- 对于高并发的写入操作,且需要确保数据一致性的场景,悲观锁可能更适合。