- 乐观锁
如一个金融系统,当某个操作员读取用户的数据,并在读出的用户数据的基础上进行修改时(如更改用户帐户余额),
如果采用悲观锁机制,也就意味着整个操作过程中(从操作员读出数据、开始修改直至提交修改结果的全过程,甚至还包括操作员中途去煮咖啡的时间),数据库记录始终处于加锁状态,可以想见,如果面对几百上千个并发,这样的情况将导致怎样的后果。
乐观锁机制在一定程度上解决了这个问题。
乐观锁,大多是基于数据版本 ( Version )记录机制实现。何谓数据版本?
即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。
读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。
此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,
如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。
对于上面修改用户帐户信息的例子而言,假设数据库中帐户信息表中有一个 version 字段,当前值为 1 ;而当前帐户余额字段( balance )为 $100 。
- 操作员 A 此时将其读出( version=1 ),并从其帐户余额中扣除 $50( $100-$50 )。
- 在操作员 A 操作的过程中,操作员B 也读入此用户信息( version=1 ),并从其帐户余额中扣除 $20 ( $100-$20 )。
- 操作员 A 完成了修改工作,将数据版本号加一( version=2 ),
连同帐户扣除后余额( balance=$50 ),提交至数据库更新,此时由于提交数据版本大于数据库记录当前版本,数据被更新,数据库记录 version 更新为 2 。 - 操作员 B 完成了操作,也将版本号加一( version=2 )试图向数据库提交数据( balance=$80 ),但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 2 ,数据库记录当前版本也为 2 ,不满足 “ 提交版本必须大于记录当前版本才能执行更新 “ 的乐观锁策略,因此,操作员 B 的提交被驳回。
这样,就避免了操作员 B 用基于 version=1 的旧数据修改的结果覆盖操作员A 的操作结果的可能。
转载自乐观锁
另外参考:一分钟教你知道乐观锁和悲观锁的区别
- CAS:compare and swap
CAS的含义是,我认为V的值应该为A,如果是,那么将V的值更新为B,否则不修改并告诉V的值实际为多少。(Java并发编程实战P263)
附上java.util.concurrent.atomic.AtomicLong中的源码
public final long getAndAccumulate(long x,
LongBinaryOperator accumulatorFunction) {
long prev, next;
do {
prev = get();
next = accumulatorFunction.applyAsLong(prev, x);
} while (!compareAndSet(prev, next));
return prev;
}
public final boolean compareAndSet(long expect, long update) {
return unsafe.compareAndSwapLong(this, valueOffset, expect, update);
}
还有两个链接CAS(Compare and Swap)理解,非阻塞同步算法与CAS(Compare and Swap)无锁算法
- MVCC:multiversion concurrency control
我们知道,mysql的innodb采用的是行锁,而且采用了多版本并发控制来提高读操作的性能。
什么是多版本并发控制呢 ?其实就是在每一行记录的后面增加两个隐藏列,记录创建版本号和删除版本号,
而每一个事务在启动的时候,都有一个唯一的递增的版本号。
1、在插入操作时 : 记录的创建版本号就是事务版本号。
比如我插入一条记录, 事务id 假设是1 ,那么记录如下:也就是说,创建版本号就是事务版本号。
id | name | create version | delete version |
---|---|---|---|
1 | test | 1 |
2、在更新操作的时候,采用的是先标记旧的那行记录为已删除,并且删除版本号是事务版本号,然后插入一行新的记录的方式。
比如,针对上面那行记录,事务Id为2 要把name字段更新
update table set name= 'new_value' where id=1;
id | name | create version | delete version |
---|---|---|---|
1 | test | 1 | 2 |
1 | new_value | 2 |
3、删除操作的时候,就把事务版本号作为删除版本号。比如
delete from table where id=1;
id | name | create version | delete version |
---|---|---|---|
1 | new_value | 2 | 3 |
4、查询操作:
从上面的描述可以看到,在查询时要符合以下两个条件的记录才能被事务查询出来:
- 删除版本号 大于 当前事务版本号,就是说删除操作是在当前事务启动之后做的。
- 创建版本号 小于或者等于 当前事务版本号 ,就是说记录创建是在事务中(等于的情况)或者事务启动之前。
这样就保证了各个事务互不影响。从这里也可以体会到一种提高系统性能的思路,就是:
通过版本号来减少锁的争用。
另外,只有read-committed和 repeatable-read 两种事务隔离级别才能使用MVCC
read-uncommited由于是读到未提交的,所以不存在版本的问题
而serializable 则会对所有读取的行加锁。
转载自mysql的mvcc(多版本并发控制)
,另外参考innodb 多版本并发控制原理详解