乐观锁

乐观并发控制(又名”乐观锁”,Optimistic Concurrency Control,缩写”OCC”)。它假设多用户并发的事务在处理时不会彼此互相影响,各事务能够在不产生锁的情况下处理各自影响的那部分数据。在提交数据更新之前,每个事务会先检查在该事务读取数据后,有没有其他事务又修改了该数据。如果其他事务有更新的话,正在提交的事务会进行回滚。简而言之,乐观锁总是认为不会产生并发问题,每次去取数据的时候总认为不会有其他线程对数据进行修改,因此不会上锁,但是在更新时会判断其他线程在这之前有没有对数据进行修改,一般会使用版本号机制或CAS操作实现。

CAS操作方式:即compare and swap 或者 compare and set,涉及到三个操作数,数据所在的内存值,预期值,新值。当需要更新时,判断当前内存值与之前取到的值是否相等,若相等,则用新值更新,若失败则重试,一般情况下是一个自旋操作,即不断的重试。

乐观锁大多数情况下用于数据争用不大、冲突较少的环境中,这是因为偶尔回滚事务的成本会低于读取数据时锁定数据的成本这样做可以获得比其他并发控制方法更高的吞吐量。相对于悲观锁,在对数据库进行处理的时候,乐观锁并不会使用数据库提供的锁机制。一般的实现乐观锁的方式就是记录数据版本。

数据版本为数据增加的一个版本标识。当读取数据时,将版本标识的值一同读出,数据每更新一次,同时对版本标识进行更新。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的版本标识进行比对,如果数据库表当前版本号与第一次取出来的版本标识值相等,则予以更新,否则认为是过期数据。实现数据版本有两种方式,第一种是使用版本号,第二种是使用时间戳。

使用版本号实现乐观锁

版本号方式:一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。

核心SQL语句:
update table set x=x+1, version=version+1 where id=#{id} and version=#{version};

例子:

(1)查询出商品信息
select (status,status,version) from t_goods where id=#{id};
(2)根据商品信息生成订单
(3)修改商品status为2
update t_goods
set status=2,version=version+1
where id=#{id} and version=#{version};

乐观并发控制是一种乐观阳光的并发控制方法,它认为事务之间的数据竞争的概率比较小,因此尽可能直接做下去,直到提交的时候才去锁定,所以不会产生任何锁和死锁。

悲观锁

悲观并发控制(又名”悲观锁”,Pessimistic Concurrency Control,缩写”PCC”)。它可以阻止一个事务以影响其他用户的方式来修改数据。如果一个事务执行的操作读某行数据应用了锁,那只有当这个事务把锁释放,其他事务才能够执行与该锁冲突的操作。 悲观并发控制主要用于数据争用激烈的环境,以及发生并发冲突时使用锁保护数据的成本要低于回滚事务的成本的环境中。 悲观锁总是假设最坏的情况,每次取数据时都认为其他线程会修改,所以都会加锁(读锁、写锁、行锁等),当其他线程想要访问数据时,都需要阻塞挂起。可以依靠数据库实现,如行锁、读锁和写锁等,都是在操作之前加锁。要使用悲观锁,我们必须关闭mysql数据库的自动提交属性,因为MySQL默认使用autocommit模式,也就是说,当你执行一个更新操作后,MySQL会立刻将结果进行提交set autocommit=0;

例子:

(1)开始事务
begin;/begin work;/start transaction; (三者选一)
(2)查询出商品信息
select status from t_goods where id=1 for update;
(3)根据商品信息生成订单
insert into t_orders (id,goods_id) values (null,1);
(4)修改商品status为2
update t_goods set status=2;
(5)提交事务
commit;/commit work;

上面使用select status from t_goods where id=1 for update;会把数据给锁住,不过我们需要注意一些锁的级别,MySQL InnoDB默认行级锁行级锁都是基于索引的,如果一条SQL语句用不到索引是不会使用行级锁的,会使用表级锁把整张表锁住,这点需要注意。

悲观并发控制实际上是一种过于保守策略,为数据处理的安全提供了保证。但是在效率方面,处理加锁的机制会让数据库产生额外的开销,还有增加产生死锁的概率;另外,在只读型事务处理中由于不会产生冲突,也没必要使用锁,这样做只能增加系统负载;还有会降低了并行性,一个事务如果锁定了某行数据,其他事务就必须等待该事务处理完才可以处理那行数

总结:读取频繁使用乐观锁,写入频繁使用悲观锁。

 

共享锁【S锁】

又称读锁,若事务T对数据对象A加上S锁,则事务T可以读A但不能修改A,其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。这保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。共享锁下其它用户可以并发读取,查询数据。但不能修改,增加,删除数据。

共享锁的写法:lock in share mode
例如:select English from Asd where English>60 lock in share mode;

排他锁【X锁】

又称写锁。若事务T对数据对象A加上X锁,事务T可以读A也可以修改A,其他事务不能再对A加任何锁,直到T释放A上的锁。这保证了其他事务在T释放A上的锁之前不能再读取和修改A。

排它锁的写法:for update
例如:select Math from Asd where Math>60 for update;

共享锁的使用:

在第一个连接中执行以下语句
begin tran
select * from table1 holdlock -holdlock人为加锁
where B='b2'
waitfor delay '00:00:30' --等待30秒
commit tran

在第二个连接中执行以下语句
begin tran
select A,C from table1
where B='b2'
update table1
set A='aa'
where B='b2'
commit tran
若同时执行上述两个语句,则第二个连接中的select查询可以执行
而update必须等待第一个事务释放共享锁转为排它锁后才能执行 即要等待30秒

排它锁的使用:

在第一个连接中执行以下语句
begin tran
update table1
set A='aa'
where B='b2'
waitfor delay '00:00:30' --等待30秒
commit tran

在第二个连接中执行以下语句
begin tran
select * from table1
where B='b2'
commit tran
若同时执行上述两个语句,则select查询必须等待update执行完毕才能执行即要等待30秒

参考:https://blog.csdn.net/puhaiyang/article/details/72284702

https://blog.csdn.net/L_BestCoder/article/details/79298417

https://blog.csdn.net/stuShan/article/details/51296098

10-06 21:25