面试官问:数据库如果不是SERIALIZABLE隔离级别,读和写操作可以并行(同时)执行,为什么分摊数据库压力要读写分离,不能简单的负载均衡吗?

我们先看读写分离的作用:

1、提高数据库性能: 将读和写分开,避免X锁(排他锁)和S锁(共享锁)的争用。读操作可以从从库获取数据,减少主库的压力,从而提高整体数据库性能。

2、节省技术成本:将读写分离,可以降低数据库的技术成本,更多的集中精力在主库的性

能优化和开发工作上。

3、数据安全:将读写分离,可以显著降低主库的注入风险,保障数据库系统的安全性。

4、更好的事务控制:将读写分离,可以更好的控制事务,保障数据库的完整性和一致性。

5、增加可用性:将读写分离,可以有效避免主库出现故障而影响系统的可用性。(某种意义上也起到了灾备的作用)

6、提高数据库可扩展性:将读写分离,可以在多个从库中实现数据库的水平扩展,从而提高数据库的可扩展性。

很多人应该是强记八股文,记下这几个读写分离的作用,当面试官再深入一点问:但是X锁和S锁在非SERIALIZABLE级别不是也可以共存吗?读和写事务也可以同时执行,那为什么会存在锁争用阻塞呢?这样看来也可以做负载均衡分摊压力,不一定要负载均衡啊?

关注我的公众号,带着大家一起来解答面试中的各种奇怪提问。

 更多资源分享,请关注我的公众号:搜索或扫码 砥砺code

数据库的加锁机制类似JDK AQS中的读写锁:AbstractQueuedSynchronizer下的ReentrantReadWriteLock。有时间的话,还是有必要读一读源码的。

共享锁(S锁)

        又称读锁,共享锁就是多个事务对于同一数据可以共享一把锁,都能访问数据。共享锁和共享锁是不冲突的,但是和排他锁是冲突的。
加共享锁可以使用select * from user where id =1 lock in share mode语句。

排他锁(X锁)

        又称写锁,排他锁就是不能与其他锁并存,如果一个事务获取了一条数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,但是对于已经获取了排他锁的事务是可以对数据进行读取和修改。
加排他锁可以使用select * from user where id =1 for update语句。

很多人的博客中会这样写:

注意:
        mysql InnoDB引擎默认的写语句,update、insert、delete都会自动给涉及的数据加上排他锁,select语句默认不会加任何锁类型。


加过排他锁的数据行在其他事务中是不能修改数据的,也不能通过for update和lock in share mode锁的方式查询数据,但是可以直接通过select ... from .... 查询数据,因为普通查询没有任何锁机制。

        其实屏蔽掉数据库服务内部代码实现逻辑,做一个类似的黑盒测试,从服务外面来看确实是这样的效果,因为只要隔离级别不是SERIALIZABLE如果有两个事务一边查询一边修改,这两个事务是可以同时进行的啊,倘若不是如此,排他锁和共享锁不能并存,那如果事务A查询加了共享锁,事务B做数据库修改加排他锁,A、B岂不是不能并存(并行执行)?

        但是数据库内部服务真的是这样的吗?在READ_COMMITED 或REPEATABLE READ隔离级别下,按照该事务级别的定义普通查询仍然会加共享锁。

下面是数据库四种隔离级别的加锁机制设定:


READ_UNCOMMITED 的原理:

  • 事务对当前被读取的数据不加锁;
  • 事务在更新某数据的瞬间(就是发生更新的瞬间),必须先对其加 行级排它锁,直到事务结束才释放。

表现:

  • 事务1读取某行记录时,事务2也能对这行记录进行读取、更新;当事务2对该记录进行更新时,事务1再次读取该记录,能读到事务2对该记录的修改版本,即使该修改尚未被提交。
  • 事务1更新某行记录时,事务2不能对这行记录做更新,直到事务1结束。

不防止任何隔离性问题,具有脏读/不可重复度/虚读(幻读)问题

造成问题场景:脏读

一个事务读取到另一个事务未提交的数据(比如A和B买鞋,A汇钱给B,汇钱这个操作还没有提交,A告诉B我打钱了,B查了一下,发现了A的汇的钱。给了A鞋子,A立刻回滚,B发现自己账户上面没有钱)


READ_COMMITED 的原理:

  • 事务对当前被读取的数据加 行级共享锁(当读到时才加锁),一旦读完该行,立即释放该行级共享锁;
  • 事务在更新某数据的瞬间(就是发生更新的瞬间),必须先对其加 行级排他锁,直到事务结束才释放。

表现:

  • 事务1读取某行记录时,事务2也能对这行记录进行读取、更新;当事务2对该记录进行更新时,事务1再次读取该记录,读到的只能是事务2对其更新前的版本,要不就是事务2提交后的版本。
  • 事务1更新某行记录时,事务2不能对这行记录做更新,直到事务1结束。

可以防止脏读问题,但是不能防止不可重复度/虚读(幻读)问题

造成问题场景:不可重复读

在一个事务内读取表中的某一行数据,多次读取结果不同--- 行级别的问题(A在银行里面活期有1000元,定期有1000,固定资产有1000。B是银行职员,上司B查一下A在银行行里面总共有多少钱。B查到A活期,定期,固定资各1000.因为并发执行,这时A刚好过来取走了活期1000元,B此时在并发执行,统计A的总钱数,发现是2000元。B提交给上司,上司发现钱数目不对)。


REPEATABLE_READ 的原理:

  • 事务在读取某数据的瞬间(就是开始读取的瞬间),必须先对其加 行级共享锁,直到事务结束才释放;
  • 事务在更新某数据的瞬间(就是发生更新的瞬间),必须先对其加 行级排他锁,直到事务结束才释放。

表现:

  • 事务1读取某行记录时,事务2也能对这行记录进行读取、更新;当事务2对该记录进行更新时,事务1再次读取该记录,读到的仍然是第一次读取的那个版本。
  • 事务1更新某行记录时,事务2不能对这行记录做更新,直到事务1结束。

可以防止脏读/不可重复读问题,但是不能防止虚读(幻读)问题

造成问题场景:虚读(幻读)

一个事务内读取到了别的事务插入的数据,导致前后读取不一致 --- 表级别的问题(A银行账户1000元,B有1000元。D是银行业务人员,上司让D统计银行到底总共有多少钱,每个人有多少钱。D查询,发现总共2000元,银行用户总共2人,D刚要算平均值时。C来了,向银行里面存了4000元,D此时再算平均钱,结果是每人2000元。结果上次发现前后数据又不一致)。


SERIALIZABLE 的原理:

  • 事务在读取数据时,必须先对其加 表级共享锁 ,直到事务结束才释放;
  • 事务在更新数据时,必须先对其加 表级排他锁 ,直到事务结束才释放。

表现:

  • 事务1正在读取A表中的记录时,则事务2也能读取A表,但不能对A表做更新、新增、删除,直到事务1结束。
  • 事务1正在更新A表中的记录时,则事务2不能读取A表的任意记录,更不可能对A表做更新、新增、删除,直到事务1结束。

可以防止所有问题,但是效率特别低,使用场景极少


        所以上面引用的那一段加锁机制中:“select语句默认不会加任何锁类型” 这句只有在READ_UNCOMMITED 隔离级别下才成立。如果不做读写分离,数据库默认的隔离级别,常见的数据库,Mysql的隔离级别是REPEATABLE_READ,Oracle的隔离级别是READ_COMMITED。所以一般默认的隔离级别下,读和写是都会加锁的,分别是共享锁和排他锁。加锁过程其实是很复杂的,先不说加锁过程涉及到数据的快照的复制,快照复制和数据恢复的操作,本身就很耗费性能,就单纯的加锁操作也是会耗费数据库服务器资源的。

        那么上面的问题也有了答案:

  1. X锁和S锁确实不可以共存,不过大多数数据库产品,在一般默认的隔离级别下,读和写却可以同时执行(怎么实现的,这里先按下不表)。
  2. 虽然读写可以共存,但加锁过程也是很耗费服务器资源的,所以既然依靠数据库隔离级别做数据强一致性不可靠,那么干脆将读写分离的从数据库隔离级别都做成隔离要求最弱的READ_UNCOMMITED,主库未提交的更新操作,也不会产生binlog,也不会同步到从库,不用担心从库读到未提交的数据。这样至少读操作都不会加锁,彻底避免了S锁和X锁的对操作资源争用(虽然各数据库产品有自己的一套实现机制,使得不同的事务,读和写可以共存,但其实内部还是会有锁资源的争用,那么就会有锁的尝试获取,各种状态和占用线程id的判断,虽然不会阻塞,但还是会耗费cpu资源), 也彻底让查询不用做加锁操作,查询操作也不用做快照复制和快照丢弃的操作,节省不少的资源开销。
  3. 不过主数据库还是将隔离级别做到READ_COMMITED级别以上,因为即使是READ_UNCOMMITED级别,写操作还是会加锁,而且写语句的update、delete,其实还是分为读阶段和写阶段,读阶段确定目标数据后加锁,也就是先读再加锁,再写的。如果是READ_UNCOMMITED级别,则读的阶段会读到其他事物未提交的数据,这是非常危险的。而且主数据库还要作为运维数据库来用的。
  4. READ_COMMITED隔离级别会不会对主从同步的binlog串行化有影响呢?MySQL5.1之前的版本确实会有bug,但是新版本就不用担心这个问题了。

        上面说了半天,很多数据库产品可以实现读必须加S锁和写加X锁的情况下,读和写仍然共存并行执行,这是怎么实现的?因为他们压根锁住的都不是同一个东西。具体的看下面的讲解了:

        按照各隔离级别加锁的原理,RC和RR理论上两个事务不能同时进行读写,以MySQL为例,RC和RR之所以读和写能同时进行,是因为InnoDB的读使用MCC版本链的机制。

        RU级别下,事务B执行行数据更改的时候,对行数据本身加排它锁,而A事务查询的时候不加任何锁,所以可以可查询到B事务未提交的这行数据;

        RC级别下,事务B执行 行数据更改的时候,对行数据本身加排它锁(更新前会先将行数据原来版本复制到undo log快照),而A事务做查询的时候,在有排他锁的情况下,则不能对行数据本身查询(也不能加锁,排它锁和其他锁都不能共存),只能查询到行数据的undo log快照,而如果A事务先查询,A会给行数据也做一个镜像快照,就是所谓的创建readview,锁定该快照而已,B事务后做更新的时候,B事务则可以对行数据本身做修改。A读完数据之后马上释放锁,丢弃readview,再次查询的时候,重新建立的readview已经是最新的行数据了,所以产生了不可重读的情景;

        RR级别下与RC大体相同,不同的是,查询事务A在第一个select查询的时候就建立行数据的readview并且读完数据之后并不丢弃,给readview加行级共享锁,且随事务一直存在。所以只要在查询事务A不结束的情况下,不管对此行数据读多少次都是第一次读到的版本。

        而前面这几种级别都是加行级锁,自然不能控制整表的数据变化带来的同一个事务中,同一条数据查询语句,多次查询结果不一致的情景(幻读)。

RC隔离级别下的半一致性读

        修改语句锁定的是行记录,那么它肯定需要根据where条件判断,哪些行记录需要被锁定,就会有读取操作,同一个事务内是可以对同一行数据本身既做修改又做读取的,也就是同一个事务内S锁和X锁是可以并存的,参照ReentrantReadWriteLock(事务当然可以读取自己未提交的数据)。但是并发的不同的事务之中,读和写是不能并发操作的,既然修改语句锁定了行数据记录,其他的事务不能再对其做任何操作,那么两个做更新的事务,怎么知道它们操作的数据范围是否有碰撞的?

        有人说:那还不简单,只要是已经被其他事务加了锁的,都是冲突数据,不错,加了锁的确实都是冲突的数据,不过还是有些问题存在的:比如事务A对某行数据加了锁,B事务也要做更新,做更新之前,要确定哪些数据是它的更新目标,因为A事务加锁的原因,B事务不能确定A加锁的数据是不是它的目标数据了,就因为A加锁,所以B就要在该数据上加个阻塞节点Node(尝试加锁),监听A事务提交之后,就再来判断该行数据是否为自己的目标数据,如果是就加锁,不是就不加锁。那岂不是因为一个更新事务A,整张表的数据都被锁住了,其他的事务即使更新的数据范围和A不冲突,也被阻塞了?再说如果该数据本来就不是B的目标数据,那不是白白加了阻塞节点吗?如果能在加阻塞节点之前就判断,那不是可以提高效率,减少阻塞吗?

        「RC隔离级别下的半一致性读」,这是一种介于在普通读和锁定读之间的一种读取方式。它只在READ_COMMITTED隔离级别下(或者在开启了innodb_locks_unsafe_for_binlog系统变量的情况下)使用update语句才会使用。

        具体的含义就是当UPDATE语句(因为where条件是一个范围),在读取数据,获取它目标数据的时候,读取到已经被其他事务(更新事务)加了锁的记录时,因为不能对该数据记录本身读取了,InnoDB会将该记录的最新提交的版本(undo_log快照)读出来,然后判断该版本是否与update语句中的where条件相匹配,如果不匹配则不对该记录加锁,从而跳到下一条记录;如果匹配则对该记录进行加锁。这样子处理只是为了让update语句尽量少被别的语句阻塞。

        那么问题来了,该数据最近的提交版本,本来不在事务B的目标范围内,事务B是不会对其监听尝试加锁的,但是事务A做了修改,提交后的该数据恰好在事务B的目标范围内了,这时候怎么办?那么只有当作事务A是在事务B做修改操作之后才提交的(事实上是之前),事务B不能对在它提交之后的数据起作用。否则就是如上面所述,因为一个事务锁住了整张表,代价太大了。

解答上面的:“READ_COMMITED隔离级别会不会对主从同步的binlog串行化有影响呢 ?”的问题      

同样RC级别mysql为了做诸如RC隔离级别下的半一致性读此类的性能优化,在加排它锁时候并没有给间隙也加锁,但是RR级别下则会,所以利用这个间隙锁,可以实现防止主从同步时,binlog不会串行化写入日志的bug,当然MySQL5.1之后版本就支持binlog的另外一种格式row,将数据库隔离级别从默认级别换掉是没问题的。

  • 22
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值