可重入锁(递归锁):
允许同一个线程多次获取同一把锁。递归函数加锁,不会阻塞自己Reentrant开头,Lock实现类,synchronized都是可重入锁。不可重入锁只能实现。99%场景用
悲观锁(Pessimistic Lock):
担心被修改(疑心重很悲观),每次拿数据都上锁。用完再解锁。期间访问都等待。基本都是悲观锁
乐观锁(Optimistic Lock):
拿数据不上锁。更新数据判断该期间是否被改过(版本号等机制),期间可读
适合修改少,读多,提高吞吐量,少量冲突,省大量锁的开销。经常冲突(写多),上层不断retry,反而降低了性能,悲观锁更合适。
一、悲观锁应用:
以常用的mysql InnoDB存储引擎为例:加入商品表items表中有一个字段status(1未下单,2已下单),下单前确保status=1。假设有一件商品,其id为10000;如果不使用锁,那么操作方法如下:
//查出商品状态: select status from items where id=10000;
//根据商品信息生成订单:insert into orders(id,item_id) values(null,10000);
//修改商品状态为2:update Items set status=2 where id=10000;
高并发下可能出现问题:
先一步修改为2了,不知道被修改了,下单2次,数据不一致。
查询出信息后就锁定,直到修改完后再解锁。
注:要使用悲观锁,我们必须关闭mysql自动提交属性(默认autocommit模式)
set autocommit=0;(关闭)
//开始事务: begin;/begin work;/start transaction; (三者选一就可以)
同上。。。(select,insert,update)
//提交事务: commit;/commit work;
注:当我执行select status from items where id=10000 for update;后。我在另外的事务中如果再次执行select status from items where id=10000 for update;则第二个事务会一直等待第一个事务的提交,此时第二个查询处于阻塞的状态,查询不会。
使用select…for update会把数据给锁住,需要注意锁的级别:
MySQL InnoDB默认Row-Level Lock,MySQL 只有明确地指定主键,才会执行Row lock (只锁住被选取的数据) ,否则MySQL 将会执行Table Lock 。除了主键外,使用索引也会影响数据库的锁定级别。
二、乐观锁应用:
1.使用数据版本(Version)记录机制实现,即为数据增加一个版本标识,一般是通过为数据库表增加一个数字类型的 “version” 字段来实现。当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值+1。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据。用下面的一张图来说明:
2.乐观锁控制的table中增加一个字段,名称无所谓,字段类型使用时间戳(timestamp), 和上面的version类似,也是在更新提交的时候检查当前数据库中数据的时间戳和自己更新前取到的时间戳进行对比,如果一致则OK,否则就是版本冲突。
以mysql InnoDB存储引擎为例,还是拿之前的例子商品表items表中有一个字段status,status=1表示该商品未被下单,status=2表示该商品已经被下单,那么我们对每个商品下单前必须确保此商品的status=1。假设有一件商品,其id为10000;
下单操作包括3步骤:
select (status,version) from items where id=#{id}
根据商品信息生成订单
update items set status=2,version=version+1 where id=#{id} and version=#{version};
为了使用乐观锁,我们需要首先修改items表,增加一个version字段,数据默认version可设为1;
很多产品都有乐观锁的使用,比如分布式存储引擎Tair,Tair中存储的每个数据都有版本号,更新后都会递增,相应的,在Tair put接口中也有此version参数,这个参数是为了解决并发更新同一个数据而设置的,这其实就是乐观锁;
https://yq.aliyun.com/articles/6899?spm=5176.10695662.1996646101.searchclickresult.60fa74556Cu5kW
三、MySQL select…for update的Row Lock与Table Lock
举例说明:
数据库表t_goods,包括id,status,name三个字段,id为主键,数据库中记录如下;
注:为了测试数据库锁,我使用两个console来模拟不同的事务操作,分别用console1、console2来表示。
例1: (明确指定主键,并且有此数据,row lock)
console1:查询出结果,但是把该条数据锁定了
console2:查询被阻塞
mysql> select * from t_goods where id=1 for update;
console2:如果console1长时间未提交,则会报错
mysql> select * from t_goods where id=1 for update;
ERROR 1205 : Lock wait timeout exceeded; try restarting transaction
例2: (明确指定主键,若查无此数据,无lock)
console1:查询结果为空
mysql> select * from t_goods where id=3 for update;
Empty set
console2:查询结果为空,查询无阻塞,说明console1没有对数据执行锁定
mysql> select * from t_goods where id=3 for update;
Empty set
例3: (无主键,table lock)
console1:查询name=道具 的数据,查询正常
console2:查询name=装备 的数据,查询阻塞,说明console1把表给锁住了
mysql> select * from t_goods where name='装备' for update;
console2:若console1长时间未提交,则查询返回为空
mysql> select * from t_goods where name='装备' for update;
Query OK, -1 rows affected
例4: (主键不明确,table lock)
console1:查询正常
console2:查询被阻塞,说明console1把表给锁住了
mysql> select * from t_goods where id>1 for update;
console1:提交事务
mysql> commit;
Query OK, 0 rows affected
console2:console1事务提交后,console2查询结果正常