悲观锁与乐观锁并不是真正意义上的锁,而是对数据的加锁策略
悲观锁(Pessimistic Lock)
是一种对数据的修改持有悲观态度的并发控制方式。总是假设最坏的情况,每次读取数据的时候都默认其他线程会更改数据,因此需要进行加锁操作,当其他线程想要访问数据时,都需要阻塞挂起。
优缺点与适用场景
优点:对每次读取数据都进行加锁,解决了脏读、幻读和不可重复读等可能存在的问题
缺点:每次都加锁降低了系统的吞吐量,并发量大时许多线程都被阻塞
适用场景:适用于读少写多的情况下,经常产生冲突的场景
实现方式(mysql里)
select for .... update
获取锁的前提:结果集中的数据没有使用排他锁或共享锁时,才能获取锁,否则将会阻塞。
例如、
select * from tbl_user where id=1 for update;
需要注意的是,for update 生效需要同时满足两个条件时才生效:
- 数据库的引擎为 innoDB
- 操作位于事务块中(BEGIN/COMMIT)
当执行 select … for update时,将会把数据锁住,因此,我们需要注意一下锁的级别。MySQL InnoDB 默认为行级锁。当查询语句指定了主键时,MySQL会执行「行级锁」,否则MySQL会执行「表锁」。
常见情况如下:
- 若明确指明主键,且结果集有数据,行锁;
- 若明确指明主键,结果集无数据,则无锁;
- 若无主键,且非主键字段无索引,则表锁;
- 若使用主键但主键不明确,则使用表锁;
实例
在窗口1上执行
// 关闭mysql数据库的自动提交属性
set autocommit=0;
// 开启事务
BEGIN;
SELECT * FROM tbl_user where id=1 for update;
然后再在窗口2执行获取锁语句 select * from tbl_user where id=1 for update;
那么窗口2并没有像窗口1一样,立刻返回结果,而是发生了阻塞,返回了一个error信息
ERROR 1205: Lock wait timeout exceeded; try restarting transaction
乐观锁(Optimistic Locking)
乐观锁是相对悲观锁而言的,是一种对数据的修改持有悲观态度的并发控制方式。乐观锁假设数据一般情况不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果冲突,则返回给用户异常信息,让用户决定如何去做。
优缺点与适用场景
优点:省去了锁的开销,加大了系统的整个吞吐量
缺点:1、ABA问题。 2、循环时间长开销大。 3、只能保证一个共享变量的原子操作
- ABA 问题
线程1从数据库中取出某数据为A,这时候线程2也从数据库中某数据为A;
并且线程2进行了一些操作将数据变成了 B、然后线程2又将数据变成 A,
这时候线程1进行 CAS 操作发现数据库中仍然是A,然后线程一操作成功。
尽管线程一的 CAS 操作成功,但是不代表这个过程就是没有问题的。
- 循环时间长开销大
自旋CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。
- 只能保证一个共享变量的原子操作
CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。
适用场景:适用于读多写少的情况(多读场景),冲突较少的场景
实现方式
1、版本号机制
在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加1。
当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。
2、CAS算法
即compare and swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。
CAS算法涉及到三个操作数
1、需要读写的内存值 V
2、进行比较的值 A
3、拟写入的新值 B
当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。