【数据库】乐观锁和悲观锁

【数据库】乐观锁和悲观锁

当程序中可能出现并发的情况时,就需要通过一定的手段来保证在并发情况下数据的准确性,通过这种手段保证了当前用户和其他用户一起操作时,所得到的结果和他单独操作时的结果是一样的。这种手段就叫做并发控制。并发控制的目的是保证一个用户的工作不会对另一个用户的工作产生不合理的影响。

没有做好并发控制,就可能导致脏读、幻读和不可重复读等问题。

常说的并发控制,一般都和数据库管理系统(DBMS)有关。在DBMS中的并发控制的任务,是确保在多个事务同时存取数据库中同一数据时,不破坏事务的隔离性和统一性以及数据库的统一性。

实现并发控制的主要手段大致可以分为 乐观并发控制 悲观并发控制 两种

1 . 乐观锁(Optimistic Locking) :

乐观锁假设数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则返回给用户错误的信息,让用户决定如何去做.

在对数据库进行处理的时候,乐观锁并不会使用数据库提供的锁机制。一般的实现乐观锁的方式就是记录数据版本

乐观并发控制相信事务之间的数据竞争(data race)的概率是比较小的,因此尽可能直接做下去,直到提交的时候才去锁定,所以不会产生任何锁和死锁

乐观锁的实现方式:

冲突检测 和 数据更新 : 典型的就是CAS(Compare and Swap)

当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试

ABA 问题: 可以通过单独设置 自增 version 字段控制

线程A读取了数据库的值 a = 1, 
线程B读取了数据库的值 a = 1, 线程B执行了++操作,将 a = 2 写入数据库,
线程C读取了a = 2,并做了 -- 操作,将结果 a = 1 写入数据库, 
线程A修改了 a++ 的值,在写入数据库的时候查询 数据库的值依然是自己当时读取的值,就将修改的值 a = 2 写入数据库,但是这样就导致 数据不一致的问题.

如果线程是顺序读取和处理的: 
A 应该读取 a = 1, 写入 a = 2
B 应该读取 a = 2, 写入 a = 1
C 应该读取 a = 1, 写入 a = 2

解决办法: 自增 version  或者 时间戳
可以通过单独设置 自增 version 字段控制,在查询的时候,查询版本号,在更新的时候,比较版本号,如果版本号一致,就修改并将版本号自增,如果版本号不一致,就认为该字段被修改过了,就失败.

在高并发场景下:

// 修改商品库存 
update item set quantity = quantity - 1 where id = 1 and quantity - 1 > 0 
将读写操作写在一起,保证原子操作,就不用加锁了

扩展:

自旋锁:
在竞争锁的时候,如果锁被占用,可能会切换到 内核态,这时需要保持当前线程的状态信息,然后等有机会了再恢复线程,这个操作在 内核态 和 用户态之前切换,非常的消耗资源,而且很时候,尝试一次获取锁失败,第二次可能就成功了,没有必要切换状态,所以线程在竞争锁的时候使用了自旋锁,在尝试获取锁失败的时候,过一段时间再次尝试,不断的尝试,但自旋锁也是有有效期的,在一段时间之后,就必须释放锁,切换到内核态,等待下一次调用。在 JDK 1.6 之后,自选时间根据 上一次在同一个锁上的自旋时间确定,基本认为一个线程上下文切换的时间是一个最佳的时间点。
什么场景适合? 
线程任务执行时间短的时候,持有锁的时间短的时候,这样锁会被不断的释放 持有。
如果线程持有时间较长,那么自旋锁的作用就被浪费了,还消耗资源。

2 . 悲观锁:PCC (Pessimistic Concurrency Control)

悲观并发控制实际上是“先取锁再访问”的保守策略,为数据处理的安全提供了保证.

具有强烈的独占和排他特性, 在整个数据处理过程中,将数据处于锁定状态, 悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性.

悲观锁 :共享锁 排它锁

共享锁【Shared lock】又称为读锁,简称S锁。顾名思义,共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。
排它锁【Exclusive lock】又称为写锁,简称X锁。顾名思义,排他锁就是不能与其他锁并存,如果一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,但是获取排他锁的事务是可以对数据行读取和修改

实现方式:悲观锁的实现,往往依靠数据库提供的锁机制

1. 在对记录进行修改前,先尝试为该记录加上排他锁(exclusive locking)
2. 如果加锁失败,说明该记录正在被修改,那么当前查询可能要等待或者抛出异常。具体响应方式由开发者根据实际需要决定
3. 如果成功加锁,那么就可以对记录做修改,事务完成后就会解锁了
4. 期间如果有其他对该记录做修改或加排他锁的操作,都会等待解锁或直接抛出异常
MySQL的Innodb引擎中,使用悲观锁,需要手动关闭autocommit模式,然后在select 的时候 添加 for update(排它锁),如果where 的内容是索引,那么会添加行级锁,如果where的内容不是索引,会锁表。

问题:

在效率方面,处理加锁的机制会让数据库产生额外的开销,还有增加产生死锁的机会。另外还会降低并行性

synchronzied 修饰的锁:

除了数据库层面的,JVM的悲观锁 通过 synchronized修饰.

Java SE 1.6 对 synchronized 进行了各种优化之后,为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁

synchronized 通过对象头(Mark Word 标记字段实现),Mark Word 保存了锁标志位 和 Monitor 对象的起始地址,monitor中还有两个队列 EntryList 和 WaitList,主要用来存放进入及等待获取锁的线程。

要想获取 synchronzied 的锁,先通过 偏向锁判断,是否是同一个线程,不是同一线程的话,然后通过 CAS(修改对象的Mark Word 的owner 为线程的 Lock Record记录)持有锁,失败之后,自选 10次,尝试获取锁,如果还是失败,就挂起,等待被唤醒。

MySQL中的锁:

锁: S锁、X锁(只有加到索引上,才是 锁行,否则锁表)

select ... in share mode;  # 添加 S锁(共享锁)
select ... for update;     # 添加 X锁(排它锁)
添加 S锁,允许其他事务对同一条记录添加 S锁,
添加 X锁,需要确定该记录没有被加锁,添加 X锁之后,该记录不能再被加锁,
添加 意向锁(Intention Locks),分为意向共享锁(IS) 和 意向独占锁(IX),

参考文章:
https://www.jianshu.com/p/d2ac26ca6525

0 0 vote
Article Rating
Subscribe
提醒
guest
4 评论
Inline Feedbacks
View all comments
谭维才
谭维才
1 月 之前

我想看这个文档,快点出一篇呀,嘿嘿