熟悉MySQL数据库的朋友们都知道,查询数据常见模式有三种:数据库
1. select ... :快照读,不加锁编程
2. select ... in share mode:当前读,加读锁安全
3. select ... for update:当前读,加写锁并发
从技术层面理解三种方式的应用场景其实并不困难,下面咱们先快速复习一下这三种读取模式的在技术层面上的区别。负载均衡
注:为了简化问题的描述,下面全部结论均是针对MySQL数据库InnoDB储存引擎RR隔离级别的。分布式
读取当前事务开始时结果集的快照版本,快照版本也能够理解为历史版本。性能
由于只需读取一个历史版本,而历史不会被修改,故历史版本自己就是一个不可变版本,因此本读取模式对读取先后的资源处理相对简单:spa
1. 读取行为发生以前,若是有其余还没有提交的事务已经修改告终果集,本读取模式不会等待这些事务结束,天然也读取不到这些修改。对象
2. 读取行为发生以后,当前事务提交以前,本读取模式也不会阻止其余事务修改数据,产生更新版本的结果集。blog
读取结果集的最新版本,同时防止其余事务产生更新的数据版本。
因为数据的最新版本是不断变化的,因此本读取模式须要强制阻断最新版本的变化,保证本身读取到的是全部人都一致承认的名副其实的最新版本。
本读取模式在读取先后对资源处理以下:
1. 读取行为发生以前,获取读锁。这意味着若是有其余还没有提交的事务已经修改告终果集,本读取模式会等待这些事务结束,以确保本身稍后能够读取到这些事务对结果集的修改。
2. 读取行为发生以后,当前事务提交以前,本读取模式会阻塞其余事务对结果集的修改。
3. 当前事务提交后,释放读锁。这意味着全部以前被阻塞的事务可恢复继续执行。
本读取模式拥有select ... in share mode的一切功能,同时它还额外具有阻止其余事务读取最新版本的能力。
本读取模式在读取先后对资源的处理以下:
1. 读取行为发生以前,获取写锁。这意味着若是有其余还没有提交的事务已经修改告终果集,本读取模式会等待这些事务结束,以确保本身稍后能够读取到这些事务对结果集的修改。
2. 读取行为发生以后,当前事务提交以前,本读取模式会阻塞其余事务对结果集的修改,也会阻塞其余事务对结果集最新版本的读取(注:其余事务仍能够读取快照版本)。
3. 当前事务提交后,释放写锁。这意味着全部以前被阻塞的事务可恢复继续执行。
三种读取模式在技术层面的区别到此就复习完了,但是咱们在实际业务编程过程当中,读取数据库中的记录到底何时要加读锁,何时要加写锁呢?
读取快照版本的历史数据和读取最新版本的数据映射到业务层面是怎样的一种业务逻辑需求?难道每写一处数据库查询代码,都要从技术层面去细细思考不一样读取模式其读取行为发生以前、以后对资源的处理是否符合业务需求吗?这样编程也太辛苦啦。
带着上述疑问,本文将尝试从每种读取模式的技术性功能出发,将不一样模式下的技术功能差别转换为业务需求差别,从而总结出不一样功能的应用场景,最终产出少数的操做性强的场景断定规则,用于快速回答不一样业务场景下查询数据库是否应该加读锁或写锁这一问题。
不过在讨论数据库加锁的应用场景以前,咱们先弄清楚一个问题,应用层能够加锁,数据库也能够加锁,他们之间的功能彷佛有一点重叠,那么什么状况下须要使用数据库锁而不是应用层锁呢?
应用层加锁,指的是在同一个进程内,经过同步代码块(临界区)、信号量、Lock锁对象等编程组件,实现并发资源的有序访问。
理论上来讲,数据库加锁须要解决的问题,经过应用层锁都能解决。
可是应用层加锁最大的局限在于其做用范围是单进程内。在分布式集群系统盛行的今天,绝大部分模块都有可能会启动多个进程实例,以实现负载均衡功能。若是两个进程并发访问数据库,经过进程内的应用层锁,是没法将跨进程的多个处理流程协调成有序执行的。
同时咱们也应该认识到,数据库锁是稀缺资源,由于储存着状态的数据库难以横向扩展,几乎是整个系统的最终瓶颈。而无状态的计算处理模块能够轻松的弹性伸缩,一个性能不够启动两个,两个不够启动三个。。。
因此,咱们能够得出以下结论:
结论1:只会在单进程内造成的资源争用,进程内部应优先使用应用层锁本身解决,而不该该将其转嫁给数据库锁(虽然不少时候用巧妙地使用数据库锁可能编程更加方便)。数据库锁应主要用于解决多进程间并发处理数据库中的数据时可能造成的混乱。
下面咱们讨论的数据库加锁应用场景,其间说起的多个事务,均是指的这些事务在不一样进程中开启的状况。
select ... for update相对于select ... in share mode而言,对读取到的结果集的最新版本具备更强的独占性。select ... in share mode只是阻塞其余事务对结果集产生更新版本,而select .. for update还会阻塞其余事务对结果集最新版本的读取。
业务层面在什么状况下须要阻塞其余事务对结果集最新版本的读取呢?
不想让别人也能够读取到最新版本,每每是由于本身想在最新版本上进行修改,同时担忧其余人也和本身同样。由于你们在修改数据时,老是但愿本身的修改与数据的最新版本(而不是历史版本)合并后存入数据库中,因此你们在修改数据前,都会尝试获取数据的最新版本,基于最新版本进行修改。若是每一个人均可以同时获取到数据的最新版本并在最新版本上加入本身的修改,最后你们一块儿提交数据,必然会出现一我的的修改覆盖了其余人修改的状况,这就是经典的“更新丢失”问题。以下图所示:
其实这个问题还能够反过来问,什么状况下没必要阻塞其余事务对结果集的读取呢?
试想若是不管你阻不阻塞读取,其余事务读取到的结果集都是同样的,你又何须阻塞它呢?若是你不修改读取出的结果集,那么别人早读晚读又有什么区别?
丢失更新问题场景有一种特殊状况须要特别注意:当你尝试读取一条不存在的记录,确认其确实不存在后,插入该记录(常见的带查重的插入操做)。此场景等价于你读取了某个范围的结果集,而后要更新此结果集,若是不加写锁,判重逻辑可能会失效。
经过上面的思考,咱们能够得出以下结论:
结论2:若是读取出的某个范围的结果集本身不须要修改它,是确定不须要使用select ... for update的。
结论3:若是读取出的某个范围的结果集本身须要修改它,此时须要使用select ... for update。
select ... in share mode相对于select ... 而言,主要新增了两点约束:
1. 读取数据以前,等待修改了这些数据的事务提交。
2. 读取数据以后,防止其余事务修改这些数据。
咱们先用业务层面的语言将上述两点约束合并简述为:但愿读取到全部人都一致承认的最新版本的数据(即没有其余人还正在修改这些数据)并锁定它。
那么什么样的业务场景下,咱们须要达到这样的效果呢?
我能想到的有以下两个典型的场景:
例1. 基于更新时间戳增量处理数据
当这次读取并处理了时间点A以前的数据,下次就不会再读取并处理这个范围内的数据了,这就是增量处理的要求。若是读取以前有人已经修改这个范围内的数据,只是事务还没有提交(因为修改行为发生在时间点A以前,因此这些数据的更新时间戳也在时间点A以前),但读取以后这些修改提交了,会出现什么问题呢?
若是采用的是普通的select ... 意味着虽然读取并处理了时间点A以前的数据,可是在读取以后这个范围内又出现了新的数据。这就会漏掉部分还没有处理的数据。以下图所示:
若是采用的是select ... in share mode,则会等待待查询时间范围内的修改均提交后,再处理这个范围内的数据,就能够避免漏处理问题。
本例中出现的问题隐含了一个前提条件,那就是新的数据提交时,新增数据的一方并无主动通知咱们进行处理,而是由咱们基于时间戳扫描新增数据。至关于业务逻辑的完整性由咱们单方面保证,而另一方并不肯意为此事效劳。这种状况在基于更新时间戳增量处理数据的场景中是很常见的,由于一般咱们的处理程序是做为第三方,基于时间戳扫描增量数据是为了尽可能保证原数据表上应用系统无需修改,即减小侵入性。
(注:基于更新时间戳处理新增数据时,设置安全读取时延是更加经常使用的解决方式。即每次读取的时间点设置为当前时间X分钟前,X分钟大于系统中事物持续的最大时间,以保证抽取时间点以前的全部修改都已提交。可是这种方式会下降数据处理的实时性。)
那么,假设修改数据的每一方都愿意通力配合,不遗余力地保证数据的一致性和业务逻辑的完整性时,就不会出问题了么?请看下面这个例子。
例2. 更新关联关系
好比,好比有Books和Students两张表,一张BooksToStudents的多对多关联表。新增Book须要让每一个Studuent都有这个Book。新增Student须要让全部Book都属于该Student。不管什么时候,对数据一致性的要求是:全部Student都拥有全部的Book。
若是两我的A和B,同时开启事务,一人新增BookA,一人新增StuduentB,你们各自严格按照数据一致性要求去维护BooksToStudents关联表。
若是不使用select ... in share mode而是使用select ... ,因为每一个事务都没法读取到对方的还没有提交的新增实体,A不知道有StudentB,因此A的BookA不会属于StudentB;B不知道有BookA,因此B的StudentB下不会有BookA。最终两个事务提交后,结果就是StudentB没有拥有BookA。以下图所示:
A和B都有机会创建起StudentB下拥有BookA这一关联记录,可是这份关联记录的创建只在A添加BookA时,以及B添加StudentB时处理,若是这两个时刻均读取不到须要的记录,这份关联记录的创建将永远不会再被触发。
可是,若是使用select ... in share mode,当A读取Students表时,发现没有StudentB后,B也没法再往Students表中添加StudentB,直至A的事务提交。届时,B再读取Books表时,也能发现A提交的BookA,进而正确新增StudentB下拥有BookA这一关联记录。
本例虽以多对多关联关系为例,其实在一对多、多对一关联关系中也可能存在相似问题。原理都大同小异,只不过一对多、多对一的关联关系一般直接储存在关联实体的某一列中,而不是储存在独立的关联关系表中。
例1呈现出来的场景能够总结为:
结论4:当数据一致性和业务逻辑完整性只能由本身单方面保证时,且本身利用了数据的某种单调性增量处理数据时,需使用select ... in share mode查询更新数据。
例2呈现出来的场景能够总结为:
结论5:当有关联关系的两个实体可能同时新增时,一方因新增实体修改关联关系,需使用select ... in share mode查询另外一方数据进行关联关系的更新。
看了上面的介绍,你们可能巴不得全部查询都使用最严格的select ... for update,这样至少不会错。可是做为最多见的普通select语句,真的有那么危险吗?
快照读意味着读取历史数据,其实把时间放长远了看,基本上绝大部分数据后续都有更新的可能。因此即使是使用最严格的select ... for update读取模式,读到的数据也终究抵不过期间的流逝,沦为历史数据。用户更多关注的并非某份数据有多新,而是某份数据不要太过期,快照读读取的历史数据一般也就是最近几十毫秒到几秒前的历史版本,彻底可以知足用户的查看需求。
当读取数据是为了后台严格的逻辑控制断定时,咱们会担忧读取过程当中出现的更新版本的数据会错过本次事务中的处理逻辑,可是这个担忧通常来讲也是多余的,由于别人产生新版本的数据时,必然也会触发一系列的处理来保证数据的一致性和业务逻辑的完整性,没必要在本身的事务中过于操心别人的事情。
咱们的原则一般是,优先使用锁范围小的查询模式,以尽可能提高数据库的并发性能。即先选select ... ,不行再用select ... in share mode,再不行再提高为select ... for update。而结论2告诉咱们什么时候无需用select ... for update,在此原则下,咱们须要搞清楚的是什么时候须要用select ... for update,因此这个结论能够忽略。
咱们的平常开发中,大部分状况下不须要本身单方面保证数据的一致性和业务逻辑的完整性,全部数据的修改方均可以通力合做。因此结论4能够暂时忽略。
综上,平常开发过程当中,咱们需记住:
1. 只会在单进程内造成的资源争用,进程内部应优先使用应用层锁本身解决,而不该该将其转嫁给数据库锁。数据库锁应主要用于解决多进程间并发处理数据库中的数据时可能造成的混乱。
2. 优先使用select ...
3. 当有关联关系的两个实体可能同时新增时,一方因新增实体修改关联关系,需使用select ... in share mode查询另外一方数据进行关联关系的更新。
4. 若是读取出来的结果集须要修改后再提交,需使用select ... for update读取结果集。
若是你不幸须要与第三方系统(或难以修改的遗留系统)以数据库的方式进行集成时,需再多记住一点:
5. 当数据一致性和业务逻辑完整性只能由本身单方面保证时,且本身利用了数据的某种单调性增量处理数据时,需使用select ... in share mode查询更新数据。
若是还有其余漏掉的场景规则,欢迎你们补充。