在并发线程的业务中,经常需要对数据进行CURD操作,所以保证数据的一致性,经常会在业务层进行加锁后再做数据库操作。
这时会用到乐观锁和悲观锁
乐观锁:
概念:认为所有操作都是不会穿插在一起的,也就是说我在操作时,很天真地认为其他人不会来碰我的数据,直接进行操作,不加锁,直到更新数据时,判断在此期间,其他人有没有来修改我的数据,使用版本号和时间戳记录和CAS操作等机制来实现。
版本号的方式:在数据表中加上一个version字段,表示数据被修改时,version的值+1,当线程更新数据时,在读取数据的同时也会读取version的值,在提交更新前,若读到的version值为当前数据库中version值相等时才更新,否则重试更新操作,直到更新成功。(具体实现:redis分布式锁中使用incr操作实现即为乐观锁)
时间戳:在数据表中添加一个time字段,在线程开始时更新这个字段,更新提交时检查这个字段与之前开始时的字段是否一致,不一致就是版本冲突。
CAS方式:即compare and swap或compare and set,涉及到三个操作数,数据所在的内存值,预期值,新值。当需要更新时,判断当前内存值与之前取到的值是否相等,若相等,则使用新值更新,若失败则重试,一般是一个自旋锁的操作。
场景:javaJUC包中的atomic就是乐观锁的实现、GIT和SVN
悲观锁:
概念:认为所有操作都会被互相影响,所以在操作前都将这个操作进行上锁,谨慎所以悲观,其他线程想要拿到这个线程的锁就会一直等待,直到解锁后才能获取到这个锁。实现方式可以使用同步关键字等
同步关键字:synchronized这个关键字就将包括起来的代码块上了锁,任何线程想进入这个代码都必须等待上一个线程执行完所有代码才能进入。
场景:数据库中的行锁,表锁,读锁,写锁等,都在操作上进行上锁
乐观锁和悲观锁的选择:
选择乐观锁的利弊
- 乐观锁适用于写操作很少发生时,即“乐观”情况下,没有非常大的并发冲突,就没有锁的开销,加大系统的整个吞吐量。
- 一般只用在高并发、多读少写的场景下。
- 乐观锁还有一个最好的好处就是共享资源,让所有访问的线程都拿到了资源
- 但是如果出现大量的写操作,数据冲突的可能性非常大,为了保证数据的一致性,应用层需要不断地重新获取数据,增加大量地查询操作,降低吞吐量。
选择悲观锁的利弊
- 适用于写入操作比较频繁的情况,一般只用在并发量不是很大,并且出现并发情况下导致的异常难以接受的情况下才会用悲观锁。
- 一般只用在不高并发,多写少读的场景下。
- 但是如果出现大量的读操作,每次读操作都需要进行加锁,这样会增加大量锁的开销,降低吞吐量。
- 悲观锁的最大坏处就是减少了线程的并发,让线程一个一个拿锁执行
总结:两种锁都有利弊,读取频繁就使用乐观锁,写入频繁就使用悲观锁,读写混合就用悲观锁。