Mysql的行锁并不是Mysql的主体,也就是server层来实现的,而是由它的插件,存储引擎来实现的,所以并不是所有的存储引擎都支持行锁,只有实现了行锁机制的存储引擎才能支持行锁,比如Innodb存储引擎,没有实现行锁机制的存储引擎则不支持行锁,比如Myisam存储引擎。
    那么支持行锁和不支持行锁各自有什么优缺点呢?支持行锁的优点很明显,多个线程可以同时对同一张表上的数据进行操作,也就是说行锁支持多个同一个表上的语句同时执行,这样显然会增加单位时间内执行的语句数量,如果某个存储引擎不支持行锁,那么这个存储引擎在访问某张表上的数据时为了保证数据的安全只能给这张表加上表级锁来阻止别的线程对该表的访问,这样做的话在同一时刻只能有一个线程在访问该表,这种机制一定会影响单位时间内执行的语句数量。这也是现在更多使用Innodb存储引擎的重要原因。那么支持行锁的缺点是什么呢?
    现在给你一个场景,按照下面的顺序执行语句,那么等到执行事务B中的update语句时会发生什么呢?如果id是主键会发生什么呢?如果id不是主键会发生什么呢?
    image.png
    由于事务A在更新id=1的数据行时给该数据行加上了行锁,但是事务A加上的行锁在事务提交时才会释放,所以事务B中的更新语句会被一直阻塞,直到事务A提交之后才会真正执行。也就是说,Innodb存储引擎会在执行更新语句的时候给数据行加上行锁,但是并不会在执行完后立即释放,而是会在提交事务时才释放,这个设定被称作两阶段协议。
    那么根据这个设定我们可以怎么更好的安排事务中语句的执行顺序呢?如果你的事务会给很多数据行加上行锁,那么我们就可以尽可能地把会造成锁冲突地语句放在后面,尽量减少其它事务中语句被阻塞地时间。
    假设现在有这么一个业务场景,顾客到电影院买票,那么就要经过这么一个流程,首先从顾客地账户余额中扣除电影票地价格,然后在电影院地账户余额中加上电影票的票价,然后在交易日志表中记录这次交易,这个流程需要保证原子性吗?需要放到一个事务中执行吗?如果不放在事务中会发生什么呢?如果不放在事务中执行,然后在扣除顾客余额电影票的价格后,在给电影院账户加上电影票的票价之前,服务器出现了问题,那么就会出现数据不一致的问题,所以一定要放在事务中执行,既然要放在事务中,那么这三条语句该怎么安排它们的顺序呢?假如在某个时刻有两个顾客同时买电影票,分别由两个事务来执行各自的流程,在两个事务中都需要修改电影院账户余额所在的数据行,所以就会发生锁冲突,为了减少锁冲突的时间,既然这三个语句的顺序可以任意安排,那么我们就可以把修改电影院账户余额的语句放在最后面来执行。
    把用户账户的余额和电影院账户的余额放在一张表中, 并且电影院的账户余额只有一行记录,如果在同一时间有很多用户购买电影票,会发生什么呢?
    什么是死锁?在Mysql中有哪些解决死锁的办法?死锁就是多个线程相互等待对方释放自己需要的资源,最终处于一个停顿的状态,资源是什么?行锁就是一种资源,那么Mysql是怎么解决死锁的问题的呢?第一种方法简单粗暴,那就是直接设置一个等待时间,如果在指定时间内拿不到需要的资源,就会回滚自己所在的事务,自己指的是什么?指的是拿不到需要资源的语句,第二种方法则是依赖于Mysql自身实现的死锁检测机制,如果检测到死锁存在了,就会主动回滚死锁链条中的某一个事务,优先满足其它事务需要的资源,Mysql是怎么知道死锁链条是由哪些事务组成的?又是依据什么选择出回滚的事务的?
    在Innodb存储引擎中,默认的超时时间是50s,那么死锁链条中的某个线程就需要等待50s来回滚自己正在执行的事务,其它线程如果在超时时间内拿到了回滚事务的线程所释放的资源,就会正常执行,如果也超时了,那么也会发生回滚,显然大部分的业务场景都不会允许等待这么长时间,但是我们也不能简单的把等待时间设置为一个很短的值,这样在出现死锁的时候,确实可以很快的解开,但是如果只是正常的锁等待,仅仅是因为等待锁的时间稍长,就对其所在事务发起回滚,显然也是不合理的,这个阈值很难设定的满足语句执行时会遇到的各种情况,所以这个解决死锁的思路优先级并不高,所以通常我们会采取第二种方法来解决死锁问题,也就是利用Mysql自身实现的死锁检测机制来检测死锁,并且在检测到死锁后,主动回滚死锁链条中的某个事务,但是检测死锁是有代价的,每个线程在执行自己的事务之前,都会先去检测自己需要的资源是否被现在正在执行的事务所持有,如果需要的资源已经被现在正在执行的事务所持有,那么死锁检测机制就会去检测这个正在执行的事务所需要的资源是否又被别的事务所持有,这样就会构成一个依赖链条,如果这个依赖链条的最后一个事务所依赖的资源正是将要执行的事务持有的,那么就会构成一个环依赖,也就是死锁,如果最后一个事务所依赖的资源和将要执行的事务无关,那么就不会发生死锁,此时所有的事务都可以正常执行。那么死锁检测机制的时间复杂度是多少呢?检测某个事务所需要的资源是否被其它事务持有的时间复杂度是o(n),最差的情况就是所有的事务之间是连续依赖的,最终构成了一个环形,在这种情况下需要检测n次某个事务需要的资源是否被其它事务持有,所以死锁检测机制的最坏时间复杂度是o(n2)。当事务的数量级上来时,所需要占用的cpu执行时间是难以想象的,cpu的大部分时间可能就被用来检测死锁了,只有小部分的cpu性能会用来执行sql语句,那么单位时间内执行的sql语句数量就会变少,所以我们要避免很多事务同时依赖某一个资源的场景。
    那么怎么解决这种由于多个事务同时更新某一个数据行导致的性能问题呢?出现性能的原因在于死锁检测会占用大量的cpu时间,那么有一个简单的办法是,如果你能保证业务中不会出现死锁问题,那么就可以把死锁检测机制给关掉,显然这点很难做到,还有一个很简单的办法是把电影院的账户余额信息记录在表中的多行,每次更新电影院的账户余额时随机选取其中一行进行更新,这样就可以避免很多事务同时依赖同一个行锁,从而消耗太多cpu时间在死锁检测上。