乐观锁和悲观锁
当要对数据库中的一条数据进行修改的时候,为了避免同时被其他人修改,最好的办法就是直接对该数据进行加锁以防止并发。这种借助锁机制,在修改数据之前先锁定,再修改的方式被称之为悲观并发控制

悲观锁的实现方式
Synchronized 关键字的实现
乐观锁假设数据一般情况不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果冲突,则返回给用户异常信息,让用户决定如何去做。乐观锁适用于读多写少的场景,这样可以提高程序的吞吐量
乐观锁的实现
Java 中 java.util.concurrent.atomic 包下面的原子变量使用了乐观锁的一种 CAS 实现方式Version 字段,表示数据被修改的次数。 当数据被修改时,Version 值会 +1;当线程 A 要更新数据时,在读取数据的同时也会读取 Version 值,在提交更新时, 若刚才读取到的 Version 值与当前数据库中的 Version 值相等时才更新,否则重试更新操作,直到更新成功;悲观锁的实现,往往依靠数据库提供的锁机制。在数据库中,悲观锁的流程如下:
以 MySQL InnoDB 引擎举例,说明 SQL 中悲观锁的应用
要使用悲观锁,必须关闭
MySQL数据库的自动提交属性set autocommit = 0。因为MySQL默认使用autocommit模式,也就是说,当执行一个更新操作后,MySQL会立刻将结果进行提交;
以电商下单扣减库存的过程说明一下悲观锁的使用:
-- 0、开始事务
begin;
-- 1、查询此商品库存信息
select quantity from items where id = 1 for update;
-- 2、修改商品库存为 2
update items set quantity = 2 where id = 1;
-- 3、提交事务
commit;
id = 1 的记录修改前,先通过 for update 的方式进行加锁,然后再进行修改。这就是比较典型的悲观锁策略;id = 1 的锁,其它的事务必须等本次事务提交之后才能执行。这样可以保证当前的数据不会被其它事务修改;select…for update 锁数据,需要注意锁的级别,MySQL InnoDB 默认行级锁。行级锁都是基于索引的,如果一条 SQL 语句用不到索引是不会使用行级锁的,会使用表级锁把整张表锁住,这点需要注意实践出真知
建表
CREATE TABLE `items` (
`id` int(11) NOT NULL,
`quantity` int(255) DEFAULT NULL COMMENT '库存',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

新开一个事务更新数据,会因为一直获取不到锁而超时

乐观锁不需要借助数据库的锁机制,主要就是两个步骤:冲突检测和数据更新。比较典型的就是 CAS (Compare and Swap)
当多个线程尝试使用 CAS 同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。比如前面的扣减库存问题,通过乐观锁可以实现如下
-- 查询出商品库存信息,quantity = 3
select quantity from items where id = 1
-- 修改商品库存为 2
update items set quantity = 2 where id = 1 and quantity = 3;
在更新之前,先查询一下库存表中当前库存数(quantity),然后在做 update 的时候,以库存数作为一个修改条件。当提交更新的时候,判断数据库表对应记录的当前库存数与第一次取出来的库存数进行比对,如果数据库表当前库存数与第一次取出来的库存数相等,则予以更新,否则认为是过期数据;
以上语句存在一个比较严重的问题,即 ABA 问题
CAS 操作发现数据库中仍然是 3,然后线程一操作成功;CAS 操作成功,但是不代表这个过程就是没有问题的;import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
public class TestAtomic {
static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
atomicReference.compareAndSet(100, 101);
atomicReference.compareAndSet(101, 100);
}, "t1").start();
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(1);
System.out.println(atomicReference.compareAndSet(100, 2019) + "\t" + atomicReference.get());
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t2").start();
TimeUnit.SECONDS.sleep(2);
}
}
/*
true 2019
*/
一个比较好的解决办法,就是通过一个单独的可以顺序递增的 version 字段
乐观锁每次在执行数据修改操作时,都会带上一个版本号,一旦版本号和数据的版本号一致就可以执行修改操作并对版本号执行 +1 操作,否则就执行失败。因为每次操作的版本号都会随之增加,所以不会出现 ABA 问题。除了 version 以外,还可以使用时间戳,因为时间戳天然具有顺序递增性
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicStampedReference;
public class TestAtomic {
static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicStampedReference.compareAndSet(100, 101, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
}, "t1").start();
new Thread(() -> {
try {
int stamp = atomicStampedReference.getStamp();
TimeUnit.SECONDS.sleep(3);
System.out.println(atomicStampedReference.compareAndSet(100, 2019, stamp, stamp + 1) + "\t" + atomicStampedReference.getReference());
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t2").start();
TimeUnit.SECONDS.sleep(2);
}
}
/*
false 100
*/
实践出真知
给数据库表添加 version 字段
CREATE TABLE `items` (
`id` int(11) NOT NULL,
`quantity` int(255) DEFAULT NULL COMMENT '库存',
`version` int(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
原始数据

开事务执行更新语句

执行后的结果

此时,另一个事务执行同样的语句,数据无变化

如何选择
转载请标明出处,原文地址:https://blog.csdn.net/weixin_41835916 如果觉得本文对您有帮助,请点击赞支持一下,您的支持是我写作最大的动力,谢谢。