Mysql锁 锁使用与优化

399 阅读4分钟

锁使用与优化 

优化方案

  1. 尽量用相等条件访问数据,这样可以避免间隙锁对并发插入的影响 
  2. 不同的程序访问一组表时,应尽量约定以相同的顺序访问各表,对一个表而言,尽可能以固定的顺序存取表中的行;这样可以大大减少死锁的机会 
  3. 给记录集显式加锁时,最好一次性请求足够级别的锁;比如要修改数据的话,最好直接申请排他锁,而不是先申请共享锁,修改时再请求排他锁,这样容易产生死锁;但是也不要申请超过实际需要的锁级别 
  4. 除非必须,查询时不要显式加锁;MySQL的MVCC可以实现事务中的查询不用加锁,优化事务性能;MVCC只在COMMITTED READ(读提交)和REPEATABLE READ(可重复读)两种隔离级别下工作
  5. 尽量使用较低的隔离级别;但是也需要考虑业务场景,选择合适的隔离级别,默认隔离级别 RR(可重复读) 
  6. 选择合理的事务大小,小事务(事务中包含的sql语句较少、设计到的表的 数据/容量 较小)发生锁冲突的几率也更小,也会减少锁定资源量和时间长度 
  7. 对于一些特定的事务,可以使用表锁来提高处理速度或减少死锁的可能

加锁解锁过程示例讲解

2PL,两阶段加锁协议:主要用于单机事务中的一致性与隔离性 

在一个事务里面,分为 加锁(lock) 阶段和 解锁(unlock) 阶段,也即所有的lock操作都在unlock操作之前,如下图所示: 

引入2PL是为了保证事务的隔离性,即多个事务在并发的情况下等同于串行的执行 

在数学上证明了如下的封锁定理:
如果事务是良构的且是两阶段的,那么任何一个合法的调度都是隔离的 

在实际情况下,SQL是千变万化、条数不定的,数据库很难在事务中判定什么是加锁阶段,什么是解锁阶段

于是引入了S2PL(Strict-2PL),即:
在事务中只有提交(commit)或者回滚(rollback)时才是解锁阶段,其余时间为加锁阶段 

如下图所示:

上面很好的解释了两阶段加锁,现在我们分析下其对性能的影响。考虑下面两种不同的扣减库存的方案:

由于在同一个事务之内,这几条对数据库的操作应该是等价的;但在两阶段加锁下的性能确是有比较大的差距 

两者方案的时序如下图所示:

值得注意的是:

在更新到数据库的那个时间点才算锁成功 

提交到数据库的时候才算解锁成功 

这两个round_trip的前半段是不会计算在内的 

如下图所示: 

由于库存往往是最重要的热点,是整个系统的瓶颈;

那么如果采用第二种方案的话, tps应该理论上能够提升 3rt/rt=3倍,这还仅仅是业务就只有三条SQL的情况下, 多一条sql就多一次rt,就多一倍的时间 

从上面的例子中,可以看出,需要把最热点的记录,放到事务最后,这样可以显著的提高吞吐量 

更进一步: 越热点记录离事务的终点越近(无论是commit还是rollback) 

先后顺序如下图: 

死锁 是任何SQL加锁不可避免的,上文提到了按照记录Key的热度在事务中倒序排列 

那么写代码的时候任何可能并发的SQL都必须按照这种顺序来处理,不然会造成死锁 

如下图所示:

使用乐观锁来避免死锁,分析 select for update 和 update where 谓词计算 

我们可以直接将一些简单的判断逻辑写到update的谓词里面,以减少加锁时间,考虑下面两种方案:

时延如下图所示:

可以看到,通过在update中加谓词计算,少了1rt的时间 

由于update在执行过程中对符合谓词条件的记录加的是和select for update一致的排它锁,所以两者效果一样