接上篇《数据库技术知识点总结之三——索引相关内容》
四. 乐观锁与悲观锁
参考地址: 《【MySQL】悲观锁&乐观锁》 《数据库并发控制 你选乐观锁还是悲观锁?》
乐观锁与悲观锁是概念上的意义,主要解决的问题是对于并发冲突的检测。
乐观锁本质上并不属于锁,它只是一种冲突检测机制,但被这样称呼的时间比较长,就被称为乐观锁。乐观锁允许并发的获取内容进行读写,但在提交的时候会进行并发控制。比如 A, B 同时获得了一个数据,而且都要对其进行处理,A 先提交了该条数据,B 后来也要提交该条数据,这时候乐观锁的策略检测到两者发生了冲突,便会拒绝 B 提交的内容,并抛出冲突,交给 B 进行处理。
乐观锁的处理策略,通常是版本控制,或者是时间戳控制(本质与前者相同)。对数据进行一个版本的记录,每次提交后都标上版本号。当提交时的版本号小于等于当前版本号,则抛出异常,待解决冲突后重新执行。
笔者看到这里,就想到了一个很常见的乐观锁——即笔者项目中使用的 SVN 源代码版本控制器。我和同事一起编辑同一个 java 文件,是被允许的,但如果我们两个人提交的内容有冲突,则 SVN 会提示我们冲突,并让我们决定如何解决冲突(采用谁的内容,或者如何合并内容),然后再提交(再提交就是将冲突抛出后再解决的过程)。
悲观锁本质上属于锁,它相对于乐观锁,属于冲突避免机制。悲观锁不允许并发时统一的对数据进行修改,A, B 同时获取数据且都要对其进行修改时,如果 A 首先开启修改的事务且添加了悲观锁,则 B 就不能开启修改事务,直到 A 将事务修改完成并提交。
这样联想一下,悲观锁的策略虽然也是可以在版本控制器的实现上套用,但明显没有乐观锁的策略方便。生活中比较常用的悲观锁,比如 Word。如果一个进程打开了某个 Word 文档的编辑模式,则其他进程被禁止进入该文档的编辑模式,直到第一个进程将 Word 退出;或者其他进程只允许进入文档的只读模式(只允许读,不允许写)。
乐观锁与悲观锁的选择策略:
选择重点在于比较冲突发生的频率与后果严重性。在冲突发生频率不高,或者冲突发生后的后果不严重(以 SVN 举例,冲突发生后仅仅是告知用户冲突的位置,用户处理一下冲突的部分即可)的情况下,适合使用乐观锁。
但如果冲突发生频率过高,则抛出冲突的次数过多,即需要多次重复的提交事务,这样会加大处理量。这种情况下适合用悲观锁。
此外如果冲突发生后的后果比较严重,也比较适合使用悲观锁。比如填写信息时,如果用户在填写内容出现错误而不提示,则用户费了老大劲儿后提交时发现填写内容错误而需要重新填写,会令用户很不爽。但如果在出现填写的错误时,就禁止用户继续往下填写,直至填写正确为止,这样虽然降低了灵活性,但冲突发生后继续填写的那部分内容是没有意义的,使用悲观锁策略可以令用户可以避免无用功的付出。
SQL 实现:
- 悲观锁:select * for update,使用这种带有行排他锁的语句,本质上就是一种悲观锁的实现,数据库执行 select * for update 时首先获得了该行的排他锁,如果其他事务也执行了 select * for update 语句,则需要等第一个 select for update 语句结束完毕后将锁释放(通过锁达到了互斥效果),然后才能执行。
- 乐观锁:在执行事务过程中,只对需要被锁的数据添加一个版本号,事务正常执行。在事务提交时,比较数据对应的数据库版本与待提交版本,如果数据库版本在待提交版本之后,说明在事务执行过程中该数据已经被并发修改过了,此次事务不能执行,所以需要回滚。
Java 实现方面的乐观锁、悲观锁实现,见 Java 并发部分。