悲观锁总结和实践

本文详细介绍了数据库悲观锁的实现方式、使用场景及注意事项,包括如何在MySQL中使用悲观锁来避免并发冲突,以及悲观锁对MySQL锁级别的影响。同时,提供了实例演示了不同查询条件下的锁级别变化。

最近学习了一下数据库的悲观锁和乐观锁,根据自己的理解和网上参考资料总结如下:

 

悲观锁介绍(百科):

悲观锁,正如其名,它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。

 

使用场景举例:以MySQL InnoDB为例

商品goods表中有一个字段status,status为1代表商品未被下单,status为2代表商品已经被下单,那么我们对某个商品下单时必须确保该商品status为1。假设商品的id为1。

 

1如果不采用锁,那么操作方法如下:

//1.查询出商品信息

select status from t_goods where id=1;

//2.根据商品信息生成订单

insert into t_orders (id,goods_id) values (null,1);

//3.修改商品status为2

update t_goods set status=2;

 

上面这种场景在高并发访问的情况下很可能会出现问题。

前面已经提到,只有当goods status为1时才能对该商品下单,上面第一步操作中,查询出来的商品status为1。但是当我们执行第三步Update操作的时候,有可能出现其他人先一步对商品下单把goods status修改为2了,但是我们并不知道数据已经被修改了,这样就可能造成同一个商品被下单2次,使得数据不一致。所以说这种方式是不安全的。

 

2使用悲观锁来实现:

在上面的场景中,商品信息从查询出来到修改,中间有一个处理订单的过程,使用悲观锁的原理就是,当我们在查询出goods信息后就把当前的数据锁定,直到我们修改完毕后再解锁。那么在这个过程中,因为goods被锁定了,就不会出现有第三者来对其进行修改了。

 

注:要使用悲观锁,我们必须关闭mysql数据库的自动提交属性,因为MySQL默认使用autocommit模式,也就是说,当你执行一个更新操作后,MySQL会立刻将结果进行提交。

 

我们可以使用命令设置MySQL为非autocommit模式:

set autocommit=0;

 

设置完autocommit后,我们就可以执行我们的正常业务了。具体如下:

//0.开始事务

begin;/begin work;/start transaction; (三者选一就可以)

//1.查询出商品信息

select status from t_goods where id=1 for update;

//2.根据商品信息生成订单

insert into t_orders (id,goods_id) values (null,1);

//3.修改商品status为2

update t_goods set status=2;

//4.提交事务

commit;/commit work;

 

注:上面的begin/commit为事务的开始和结束,因为在前一步我们关闭了mysql的autocommit,所以需要手动控制事务的提交,在这里就不细表了。

 

上面的第一步我们执行了一次查询操作:select status from t_goods where id=1 for update;

与普通查询不一样的是,我们使用了select…for update的方式,这样就通过数据库实现了悲观锁。此时在t_goods表中,id为1的 那条数据就被我们锁定了,其它的事务必须等本次事务提交之后才能执行。这样我们可以保证当前的数据不会被其它事务修改。

 

注:需要注意的是,在事务中,只有SELECT ... FOR UPDATE 或LOCK IN SHARE MODE 同一笔数据时会等待其它事务结束后才执行,一般SELECT ... 则不受此影响。拿上面的实例来说,当我执行select status from t_goods where id=1 for update;后。我在另外的事务中如果再次执行select status from t_goods where id=1 for update;则第二个事务会一直等待第一个事务的提交,此时第二个查询处于阻塞的状态,但是如果我是在第二个事务中执行select status from t_goods where id=1;则能正常查询出数据,不会受第一个事务的影响。

 

补充:MySQL select…for update的Row Lock与Table Lock

上面我们提到,使用select…for update会把数据给锁住,不过我们需要注意一些锁的级别,MySQL InnoDB默认Row-Level Lock,所以只有「明确」地指定主键,MySQL 才会执行Row lock (只锁住被选取的数据) ,否则MySQL 将会执行Table Lock (将整个数据表单给锁住)。

 

举例说明:

数据库表t_goods,包括id,status,name三个字段,id为主键,数据库中记录如下;

Sql代码   收藏代码
  1. mysql> select * from t_goods;  
  2. +----+--------+------+  
  3. | id | status | name |  
  4. +----+--------+------+  
  5. |  1 |      1 | 道具 |  
  6. |  2 |      1 | 装备 |  
  7. +----+--------+------+  
  8. rows in set  
  9.   
  10. mysql>  

注:为了测试数据库锁,我使用两个console来模拟不同的事务操作,分别用console1、console2来表示。 

 

例1: (明确指定主键,并且有此数据,row lock)

console1:查询出结果,但是把该条数据锁定了

Sql代码   收藏代码
  1. mysql> select * from t_goods where id=1 for update;  
  2. +----+--------+------+  
  3. | id | status | name |  
  4. +----+--------+------+  
  5. |  1 |      1 | 道具 |  
  6. +----+--------+------+  
  7. 1 row in set  
  8.   
  9. mysql>  

console2:查询被阻塞

Sql代码   收藏代码
  1. mysql> select * from t_goods where id=1 for update;  

console2:如果console1长时间未提交,则会报错

Sql代码   收藏代码
  1. mysql> select * from t_goods where id=1 for update;  
  2. ERROR 1205 : Lock wait timeout exceeded; try restarting transaction  

 

例2: (明确指定主键,若查无此数据,无lock)

console1:查询结果为空

Sql代码   收藏代码
  1. mysql> select * from t_goods where id=3 for update;  
  2. Empty set  

console2:查询结果为空,查询无阻塞,说明console1没有对数据执行锁定

Sql代码   收藏代码
  1. mysql> select * from t_goods where id=3 for update;  
  2. Empty set  

 

例3: (无主键,table lock)

console1:查询name=道具 的数据,查询正常

Sql代码   收藏代码
  1. mysql> select * from t_goods where name='道具' for update;  
  2. +----+--------+------+  
  3. | id | status | name |  
  4. +----+--------+------+  
  5. |  1 |      1 | 道具 |  
  6. +----+--------+------+  
  7. 1 row in set  
  8.   
  9. mysql>  

console2:查询name=装备 的数据,查询阻塞,说明console1把表给锁住了

Sql代码   收藏代码
  1. mysql> select * from t_goods where name='装备' for update;  

console2:若console1长时间未提交,则查询返回为空

Sql代码   收藏代码
  1. mysql> select * from t_goods where name='装备' for update;  
  2. Query OK, -1 rows affected  

 

例4: (主键不明确,table lock)

console1:查询正常

Sql代码   收藏代码
  1. mysql> begin;  
  2. Query OK, 0 rows affected  
  3.   
  4. mysql> select * from t_goods where id>0 for update;  
  5. +----+--------+------+  
  6. | id | status | name |  
  7. +----+--------+------+  
  8. |  1 |      1 | 道具 |  
  9. |  2 |      1 | 装备 |  
  10. +----+--------+------+  
  11. rows in set  
  12.   
  13. mysql>  

console2:查询被阻塞,说明console1把表给锁住了

Sql代码   收藏代码
  1. mysql> select * from t_goods where id>1 for update;  

 

例5: (主键不明确,table lock)

console1:

Sql代码   收藏代码
  1. mysql> begin;  
  2. Query OK, 0 rows affected  
  3.   
  4. mysql> select * from t_goods where id<>1 for update;  
  5. +----+--------+------+  
  6. | id | status | name |  
  7. +----+--------+------+  
  8. |  2 |      1 | 装备 |  
  9. +----+--------+------+  
  10. 1 row in set  
  11.   
  12. mysql>  

console2:查询被阻塞,说明console1把表给锁住了

Sql代码   收藏代码
  1. mysql> select * from t_goods where id<>2 for update;  

console1:提交事务

Sql代码   收藏代码
  1. mysql> commit;  
  2. Query OK, 0 rows affected  

console2:console1事务提交后,console2查询结果正常

Sql代码   收藏代码
  1. mysql> select * from t_goods where id<>2 for update;  
  2. +----+--------+------+  
  3. | id | status | name |  
  4. +----+--------+------+  
  5. |  1 |      1 | 道具 |  
  6. +----+--------+------+  
  7. 1 row in set  
  8.   
  9. mysql>  

 

以上就是关于数据库主键对MySQL锁级别的影响实例,需要注意的是,除了主键外,使用索引也会影响数据库的锁定级别

 

举例:

我们修改t_goods表,给status字段创建一个索引

修改id为2的数据的status为2,此时表中数据为:

Sql代码   收藏代码
  1. mysql> select * from t_goods;  
  2. +----+--------+------+  
  3. | id | status | name |  
  4. +----+--------+------+  
  5. |  1 |      1 | 道具 |  
  6. |  2 |      2 | 装备 |  
  7. +----+--------+------+  
  8. rows in set  
  9.   
  10. mysql>  

 

例6: (明确指定索引,并且有此数据,row lock)

console1:

Sql代码   收藏代码
  1. mysql> select * from t_goods where status=1 for update;  
  2. +----+--------+------+  
  3. | id | status | name |  
  4. +----+--------+------+  
  5. |  1 |      1 | 道具 |  
  6. +----+--------+------+  
  7. 1 row in set  
  8.   
  9. mysql>  

console2:查询status=1的数据时阻塞,超时后返回为空,说明数据被console1锁定了

Sql代码   收藏代码
  1. mysql> select * from t_goods where status=1 for update;  
  2. Query OK, -1 rows affected  

console2:查询status=2的数据,能正常查询,说明console1只锁住了行,未锁表

Sql代码   收藏代码
  1. mysql> select * from t_goods where status=2 for update;  
  2. +----+--------+------+  
  3. | id | status | name |  
  4. +----+--------+------+  
  5. |  2 |      2 | 装备 |  
  6. +----+--------+------+  
  7. 1 row in set  
  8.   
  9. mysql>  

 

例7: (明确指定索引,若查无此数据,无lock)

console1:查询status=3的数据,返回空数据

Sql代码   收藏代码
  1. mysql> select * from t_goods where status=3 for update;  
  2. Empty set  

console2:查询status=3的数据,返回空数据

Sql代码   收藏代码
  1. mysql> select * from t_goods where status=3 for update;  
  2. Empty set  

 

 

以上就是关于我对数据库悲观锁的理解和总结,有不对的地方欢迎拍砖,下一次会带来数据库乐观锁的总结和实践

 

参考资料:

MySQL事务与锁定命令:http://www.docin.com/p-16805970.html

悲观锁:http://www.cnblogs.com/chenwenbiao/archive/2012/06/06/2537508.html 


===============

原文链接:http://chenzhou123520.iteye.com/blog/1860954

<think>我们正在讨论乐观锁悲观锁的概念及区别,特别是在Java中的实现原理。根据引用内容,我们可以整理如下: 1. **悲观锁**: - 概念:总是假设最坏情况,每次访问共享资源时都会上锁,确保同一时间只有一个线程能访问该资源。 - 实现:在Java中,`synchronized`关键字`ReentrantLock`是典型的悲观锁实现。 - 适用场景:写多读少,竞争激烈的场景,可以避免频繁的失败重试。 - 缺点:加锁会带来额外的性能开销,并且可能导致线程阻塞。 2. **乐观锁**: - 概念:假设冲突很少发生,因此在访问共享资源时不上锁,而是在更新数据时检查在此期间是否有其他线程修改了数据(通常通过版本号或CAS操作)。 - 实现:在Java中,原子类(如`AtomicInteger`)使用CAS(Compare and Swap)操作实现乐观锁数据库中使用版本号字段。 - 适用场景:读多写少,竞争较少的场景,可以避免加锁的开销。 - 缺点:如果冲突频繁,重试操作可能反而降低性能。 **区别对比**: | 特性 | 悲观锁 | 乐观锁 | |--------------|---------------------------|---------------------------| | 加锁时机 | 操作前先加锁 | 操作时不上锁,提交时检查 | | 实现方式 | `synchronized`, `ReentrantLock` | CAS, 版本号 | | 适用场景 | 写操作多,竞争激烈 | 读操作多,竞争较少 | | 性能开销 | 固定,可能阻塞线程 | 无冲突时开销小,冲突时需重试 | | 数据一致性 | 强一致性 | 最终一致性 | **实现原理**: 1. **悲观锁的实现原理**: - 在Java中,`synchronized`关键字通过监视器锁(Monitor)实现,每个对象都有一个监视器锁,当线程进入同步代码块时,会自动获取锁,退出时释放锁。在JVM内部,它通过进入退出管程(Monitor)来实现,底层使用操作系统的互斥量(Mutex)来实现线程阻塞。 - `ReentrantLock`则是基于AQS(AbstractQueuedSynchronizer)框架实现,通过CAS操作队列来管理线程的阻塞唤醒。 2. **乐观锁的实现原理**: - CAS(Compare and Swap)操作:CAS是一种无锁算法,包含三个操作数——内存位置(V)、预期原值(A)新值(B)。当且仅当V的值等于A时,CAS才会将V的值更新为B,否则什么都不做。整个操作是原子性的,由硬件(如CPU的cmpxchg指令)保证。 - 版本号机制:在数据库中,可以为数据增加一个版本号字段。每次更新时,先读取当前版本号,更新时检查版本号是否改变,如果改变则重试。 **Java代码示例**: - 悲观锁示例(使用`synchronized`): ```java public class Counter { private int count; public synchronized void increment() { count++; } } ``` - 乐观锁示例(使用`AtomicInteger`): ```java import java.util.concurrent.atomic.AtomicInteger; public class Counter { private AtomicInteger count = new AtomicInteger(0); public void increment() { count.incrementAndGet(); } } ``` **适用场景选择**: - 根据引用[5]:读多写少用乐观锁(配合Atomic类或StampedLock),写多读少用悲观锁(配合ReentrantLock或synchronized)。 - 高并发场景:考虑分段锁(如ConcurrentHashMap)或LongAdder(减少CAS空转)。 **总结**: 乐观锁悲观锁是两种不同的并发控制策略,选择哪种取决于具体的应用场景。乐观锁在无冲突时性能更高,而悲观锁在冲突频繁时更稳定。 §§相关问题§§ 1. 什么是CAS操作?它存在哪些问题? 2. 在分布式系统中如何实现乐观锁悲观锁? 3. Java中除了synchronizedReentrantLock,还有哪些锁机制? 4. 乐观锁在哪些情况下会失效?如何解决? 5. 如何根据系统负载选择合适的锁策略?</think>### Java中乐观锁悲观锁的区别及实现原理 #### 一、核心概念对比 | 特性 | 悲观锁 | 乐观锁 | |--------------|-----------------------------------|-----------------------------------| | **基本思想** | 假设冲突必然发生,先加锁再操作数据 | 假设冲突很少发生,操作后检查冲突 | | **加锁时机** | 操作前显式加锁 | 操作时不加锁,提交时验证版本 | | **性能影响** | 高(线程阻塞) | 低(无阻塞,冲突时重试) | | **适用场景** | 写多读少,竞争激烈 | 读多写少,冲突率低 | | **实现方式** | `synchronized`, `ReentrantLock` | CAS, 版本号机制 | #### 二、实现原理详解 1. **悲观锁实现原理** - 基于**阻塞机制**:线程访问共享资源前必须先获得锁 - Java实现: ```java // synchronized实现 public synchronized void updateData() { // 临界区操作 } // ReentrantLock实现 private Lock lock = new ReentrantLock(); public void updateData() { lock.lock(); // 显式加锁 try { // 临界区操作 } finally { lock.unlock(); // 必须释放锁 } } ``` - 底层机制:通过操作系统的互斥量(Mutex)实现线程阻塞,JVM使用**监视器锁(Monitor)** 管理`synchronized`[^3] 2. **乐观锁实现原理** - 基于**版本控制**或**CAS(Compare and Swap)** - Java实现: ```java // CAS实现(AtomicInteger示例) private AtomicInteger value = new AtomicInteger(0); public void safeIncrement() { int oldValue; do { oldValue = value.get(); // 获取当前值 } while (!value.compareAndSet(oldValue, oldValue + 1)); // CAS更新 } // 版本号实现(数据库场景) UPDATE table SET data=new_data, version=version+1 WHERE id=record_id AND version=current_version; ``` - CAS底层:依赖CPU的**原子指令**(如x86的`cmpxchg`),无需线程阻塞[^2] #### 三、关键区别分析 1. **并发控制方式** - 悲观锁:**预防冲突**(先加锁后操作) - 乐观锁:**检测冲突**(先操作后验证) 2. **数据一致性** - 悲观锁:强一致性(串行操作) - 乐观锁:最终一致性(可能需重试) 3. **性能对比** ```mermaid graph LR A[线程竞争程度] -->|低| B[乐观锁更高效] A -->|高| C[悲观锁更稳定] ``` 当冲突率<20%时,乐观锁性能优势明显;冲突率>40%时悲观锁更优[^5] #### 四、典型应用场景 1. **悲观锁适用场景** - 银行转账操作(高竞争写) - 库存扣减(防止超卖) - 数据库事务中的`SELECT FOR UPDATE` 2. **乐观锁适用场景** - 商品浏览计数(读多写少) - 缓存更新(低冲突率) - 分布式系统状态同步 #### 五、选型建议 1. 优先考虑乐观锁: - 读操作远多于写操作 - 冲突概率低于20% - 需要高吞吐量的场景 2. 选择悲观锁: - 写操作频繁且竞争激烈 - 业务逻辑复杂需要强一致性 - 无法容忍重试带来的延迟[^5] > **实践提示**:在Java中,`LongAdder`通过分段CAS机制解决了高并发下乐观锁的频繁重试问题,是`AtomicLong`的优化替代方案[^2]。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值