本质: 悲观锁和乐观锁都是一种概念和认知。数据库有java语言都有对应的实现方式。
悲观锁
悲观锁(Pessimistic Lock),顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。
悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。
SELECT ... LOCK IN SHARE MODE - 共享锁
SELECT ... FOR UPDATE -排它锁(悲观锁)
SELECT * FROM tb_product_stock WHERE product_id=101 FOR UPDATE -悲观锁,每次查询数据时候都认为会有其他人修改,都加锁。
Java synchronized锁 就属于悲观锁的一种实现,每次线程要修改数据时都先获得锁,保证同一时刻只有一个线程能操作数据,其他线程则会被block。
乐观锁
乐观锁(Optimistic Lock),顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在提交更新的时候会判断一下在此期间别人有没有去更新这个数据。乐观锁适用于读多写少的应用场景,这样可以提高吞吐量。
乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。
java乐观锁实现 CAS锁
数据库乐观锁一般来说有以下2种方式:
1.使用数据版本(Version)记录机制实现,这是乐观锁最常用的一种实现方式。何谓数据版本?即为数据增加一个版本标识,一般是通过为数据库表增加一个数字类型的 “version” 字段来实现。当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加一。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据。
UPDATE tb_product_stock SET number=number-1, version=version+1 WHERE product_id=#{productId} and version=#{version} AND number=#{number} -乐观锁,每次取数据时候认为别人不会修改,只在提交更新时候对比version,版本跟当初取数据时候版本一致则更新成功,否则更新失败。提高吞吐量。缺点: 高并发情况下由于并发更新频繁,导致乐观锁频繁更新失败,处理效率不高
2.使用时间戳(timestamp)。乐观锁定的第二种实现方式和第一种差不多,同样是在需要乐观锁控制的table中增加一个字段,名称无所谓,字段类型使用时间戳(timestamp), 和上面的version类似,也是在更新提交的时候检查当前数据库中数据的时间戳和自己更新前取到的时间戳进行对比,如果一致则OK,否则就是版本冲突。
示例: 商品扣库存
CREATE TABLE `tb_product_stock` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增ID',
`product_id` bigint(32) NOT NULL COMMENT '商品ID',
`number` INT(8) NOT NULL DEFAULT 0 COMMENT '库存数量',
`version` INT(8) NOT NULL DEFAULT 0 COMMENT '数据版本',
`create_time` DATETIME NOT NULL COMMENT '创建时间',
`modify_time` DATETIME NOT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `index_pid` (`product_id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='商品库存表';
不考虑并发的情况下,更新库存代码如下:
// 更新库存(不考虑并发)
public boolean updateStockRaw(Long productId){
ProductStock product = query("SELECT * FROM tb_product_stock WHERE product_id=#{productId}", productId);
if (product.getNumber() > 0) {
int updateCnt = update("UPDATE tb_product_stock SET number=number-1 WHERE product_id=#{productId}", productId);
if(updateCnt > 0){ //更新库存成功
return true;
}
}
return false;
}
悲观锁
// 更新库存(使用悲观锁)
public boolean updateStock(Long productId){
//先锁定商品库存记录
ProductStock product = query("SELECT * FROM tb_product_stock WHERE product_id=#{productId} FOR UPDATE", productId);
if (product.getNumber() > 0) {
int updateCnt = update("UPDATE tb_product_stock SET number=number-1 WHERE product_id=#{productId}", productId);
if(updateCnt > 0){ //更新库存成功
return true;
}
}
return false;
}
乐观锁
public boolean updateStock(Long productId){
int updateCnt = 0;
while (updateCnt == 0) {
ProductStock product = query("SELECT * FROM tb_product_stock WHERE product_id=#{productId}", productId);
// 首先读取行记录 version字段,然后通过乐观锁 带着version字段去更新原纪录,如果没有并发更新则会成功更新,如果有并发更新则会更新失败,需要重试处理。
updateCnt = update("UPDATE tb_product_stock SET number=number-1, version=version+1 WHERE product_id=#{productId} and version=#{version} AND number=#{number}", productId, product.getVersion(), product.getNumber());
if(updateCnt > 0){ //更新库存成功
return true;
} else {
return false;
}
}
乐观锁与悲观锁的区别
乐观锁的思路一般是表中增加version版本字段,更新时where语句中增加版本的判断,算是一种CAS(Compare And Swep)操作,商品库存场景中起到了版本控制的作用
悲观锁之所以是悲观,在于他认为本次操作会发生并发冲突,所以一开始就对商品加上锁(SELECT ... FOR UPDATE),然后就可以安心的做判断和更新,因为这时候不会有别人更新这条商品库存。
数据库乐观锁高并发性能不是很好,更好的解决方案:
分布式锁: 1.Redis分布式锁(AP模型) 2.zk分布式锁 3.etcd分布式锁(CP模型)