实现并发控制的主要手段分为乐观并发控制和悲观并发控制两种。
1、啥叫悲观锁,乐观锁?
1️⃣悲观锁和乐观锁的字面意思:
悲观和乐观其实是对数据修改持有的一种态度。
悲观锁总是认为会出现最坏的情况,悲观锁认为每次在他读取数据的时候其他线程都会修改这个数据,因此他需要在读取数据时加锁控制,保证他想取的数据是未被他人修改过的。
乐观锁总是持乐观态度,他认为每次他读取数据的时候其他线程不会修改这个数据,但是他认为在提交更新的时候其他线程才会更新他的数据,所以他提交的时候加锁控制,从而保证更新正确性。
2️⃣官方意思:
悲观锁(Pessimistic Lock)
当要对数据库中的一条数据进行修改的时候,为了避免同时被其他人修改,最好的办法就是直接对该数据进行加锁以防止并发。这种借助数据库锁机制,在数据修改之前先锁定,再修改的方式被称之为悲观并发控制【Pessimistic Concurrency Control,缩写“PCC”,又名“悲观锁”】。
乐观锁是相对悲观锁而言的,乐观锁假设数据一般情况不会造成冲突,所以在数据进行提交修改时,才会正式对数据的冲突与否进行检测,如果冲突,则返回给用户异常信息,让用户决定如何去做。在数据修改时锁定的方式被称之为乐观并发控制。
2、问:悲观锁和乐观锁有啥区别?
1️⃣悲观锁侧重于查出来就是错误的数据,所以他侧重于查询时加控制;
乐观锁不认为查出来是错的,认为更新时数据是错的,所以他侧重于提交时加控制;
2️⃣悲观锁由于想在数据根源保证数据的准确性,所以悲观锁是和数据库打交道的,也就是说添加悲观锁必须依靠数据库的锁机制。
乐观锁不需要借助数据库锁机制,在提交的时候加控制,添加乐观锁其实就是添加版本比较,依靠版本比较来确定数据的准确性。
3、问:悲观锁和乐观锁的适用场景是啥?
因为悲观锁认为读的时候有问题,修改的时候没问题,所以适合少读多写。乐观锁认为读取的时候没问题,修改的时候有问题,所以适合多读少写。
4、问:锁有哪些?
锁主要有2种类型:悲观锁和乐观锁
悲观锁:又分为共享锁和排它锁;
乐观锁:主要是指CAS锁;
共享锁:顾名思义就是多个事务或者线程可以共享一把锁,他们都能访问到这个数据,但是都不能修改它(共享锁只能读不能改)
排它锁:顾名思义就是多个事务或者线程不能共用一把锁,其中一个事务或者线程操作一条数据时,其他线程就不能对该数据进行独读取和修改,只有获得排它锁的事务和线程才能对数据进行读取和修改。(排它锁能读能改)。
CAS锁:也叫做自旋,全称是compare and swap(比较并交换),顾名思义,意思就是它将内存位置的内容与给定值进行比较,只有在相同的情况下,才会将该内存位置的值更新为新给定的值。(大白话:我认为位置(V)现在应该包含值(A),如果包含该值,则将新值(B)放在这个位置上,否则,不进行任何更改,只告诉我这个位置现在的值即可)
5、问:悲观锁和乐观锁的缺点是什么呢?
悲观锁的缺点:
1️⃣因为悲观锁是和数据库打交道,所以锁机制加大数据库的开销。
2️⃣增加了死锁的概率。
3️⃣降低了程序的并行性,如果一个事务或者线程锁定了一行数据,那么其他数据就必须等这个事务或线程处理完了才可以处理那条数据。
解决方法:使用for update加锁策略,但需要注意锁的级别。
乐观锁的缺点:
乐观锁缺点,即使用CAS的弊端。
1️⃣线程竞争激烈会导致自旋次数过多,过度消耗CPU。
2️⃣引发ABA问题。
解决方法:使用LongAdder原子类,代价是消耗更多的空间,以空间换时间。
ABA问题解决方案:
1️⃣加版本号控制。一般是在数据库表中加一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version的值会+1。当线程需要更新数据时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值与当前数据库中的version值相等才去更新,否则重新获取版本号重试更新操作,直到更新成功。
2️⃣加时间戳。原理同版本控制。
附:什么叫ABA问题?
ABA问题是指数据从A被修改到B再从B修改到A引发的问题。
假设有3个线程
线程1,期望值为A,欲更新为B
线程2,期望值为A,欲更新为B
线程3,期望值为B,欲更新为A
解释:线程1抢先获得CPU时间片执行,A被改为B,接着执行线程2发现线程2因为某种原因阻塞,便先去执行线程3,B被改为A,此时线程2恢复正常,开始执行,再次取A更新为B,但是此时线程2却不知道此时的A其实是经历了(A→B→A)的变化,而不是他想更新的那个最开始未经变化的A,由此异常产生。
6、问:如何选择悲观锁和乐观锁
主要看适用场景。
1️⃣响应效率:如果需要非常高的响应速度,建议采用乐观锁方案,成功就执行,不成功就失败,,不需要等待其他并发去释放锁。乐观锁并未真正加锁,效率高。一旦锁的粒度掌握不好,更新失败的概率就会比较高,容易发生业务失败。
2️⃣冲突频率:如果冲突频率比较高,建议采用悲观锁,保证成功率。冲突频率大,选择乐观锁会需要多次重试才能成功,代价比较大。
3️⃣重试代价:如果重试代价大,建议采用悲观锁。悲观锁依赖数据库锁机制,效率低。更新失败的概率比较低。
4️⃣乐观锁如果有人在你之前更新了,你的更新应当是被拒绝的,可以让用户重新操作。悲观锁则会等待前一个更新完成。这也是区别。
随着互联网三高架构(高并发、高性能、高可用)的提出,悲观锁已经越来越少的被应用到生产环境中了,尤其是并发量比较大的业务场景。