在编程世界里,「锁」更是五花八门,多种多样,每种锁的加锁开销以及应用场景也可能会不同。
如何用好锁,也是程序员的基本素养之一了。
高并发的场景下,如果选对了合适的锁,则会大大提高系统的性能,否则性能会降低。
所以,知道各种锁的开销,以及应用场景是很有必要的。
读锁
也叫共享锁 (shared lock)
使用
SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE
即事务A 使用共享锁 获取了某条(或者某些)记录时,事务B 可以读取这些记录,可以继续添加共享锁,但是不能修改或删除这些记录(当事务B 对这些数据修改或删除时会进入阻塞状态,直至锁等待超时或者事务A提交)
场景
读取结果集的最新版本,同时防止其他事务产生更新该结果集
主要用在需要数据依存关系时确认某行记录是否存在,并确保没有人对这个记录进行UPDATE或者DELETE操作
写锁
也叫排它锁(exclusive lock)
使用
SELECT * FROM table_name WHERE ... FOR UPDATE
一个写锁会阻塞其他的读锁和写锁
即事务A 对某些记录添加写锁时,事务B 无法向这些记录添加写锁或者读锁(不添加锁的读取是可以的),事务B 也无法执行对 锁住的数据update、delete
使用场景
读取结果集的最新版本,同时防止其他事务产生读取或者更新该结果集。
例如:并发下对商品库存的操作
乐观锁
前面提到的互斥锁、自旋锁、读写锁,都是属于悲观锁。
悲观锁,认为多线程同时修改共享资源的概率比较高,于是很容易出现冲突,所以访问共享资源前,先要上锁。
那相反的,如果多线程同时修改共享资源的概率比较低,就可以采用乐观锁。
乐观锁,假定冲突的概率很低,它的工作方式是:先修改完共享资源,再验证这段时间内有没有发生冲突,如果没有其他线程在修改资源,那么操作完成,如果发现有其他线程已经修改过这个资源,就放弃本次操作。
放弃后如何重试,这跟业务场景息息相关,虽然重试的成本很高,但是冲突的概率足够低的话,还是可以接受的。
可见,乐观锁的心态是,不管三七二十一,先改了资源再说。另外,你会发现乐观锁全程并没有加锁,所以它也叫无锁编程。
乐观锁虽然去除了加锁解锁的操作,但是一旦发生冲突,重试的成本非常高,所以只有在冲突概率非常低,且加锁成本非常高的场景时,才考虑使用乐观锁。
总结
如果能区分读操作和写操作的场景,那读写锁就更合适了,它允许多个读线程可以同时持有读锁,提高了读的并发性。根据偏袒读方还是写方,可以分为读优先锁和写优先锁,读优先锁并发性很强,但是写线程会被饿死,而写优先锁会优先服务写线程,读线程也可能会被饿死,那为了避免饥饿的问题,于是就有了公平读写锁,它是用队列把请求锁的线程排队,并保证先入先出的原则来对线程加锁,这样便保证了某种线程不会被饿死,通用性也更好点。