近期有一个业务需求,多台机器须要同时从Mysql一个表里查询数据并作后续业务逻辑,为了防止多台机器同时拿到同样的数据,每台机器须要在获取时锁住获取数据的数据段,保证多台机器不拿到相同的数据。mysql
咱们Mysql的存储引擎是innodb,支持行锁。解决同时拿数据的方法有不少,为了更加简单,不增长其余表和服务的状况下,咱们考虑采用select... for update的方式,这样X锁锁住查询的数据段,表里其余数据没有锁,其余业务逻辑仍是能够操做。sql
这样一台服务器好比select .. for update limit 0,30时,其余服务器执行一样sql语句会自动等待释放锁,等待前一台服务器锁释放后,该台服务器就能查询下一个30条数据。若是要求更智能,oracle支持for update skip locked跳过锁区域,这样能不等待立刻查询没有被锁住的下一个30条记录。服务器
下面说下mysql for update致使的死锁。oracle
通过分析,mysql的innodb存储引擎实务锁虽然是锁行,但它内部是锁索引的,根据where条件和select的值是否只有主键或非主键索引来判断怎么锁,好比只有主键,则锁主键索引,若是只有非主键,则锁非主键索引,若是主键非主键都有,则内部会按照顺序锁。但一样的select .. for update语句怎么就死锁了呢?一样的sql语句查询条件和结果顺序都一致,按理不会致使一个锁了主键索引,等待锁非主键索引,另一个锁了非主键索引,等待主键索引致使的死锁。测试
最后通过分析,咱们项目里发现是for update的sql语句,和另一个update非select数据的sql语句致使的死锁。blog
好比有60条数据,select .. for update查询第31-60条数据,update在更新1-10条数据,按照innodb存储引擎的行锁原理,应该不会致使不一样行的锁致使的互相等待。开始觉得是行锁在数据量较大状况下,会锁数据块。致使一个段的数据被锁住,但通过大量数据测试,发现感受把整个表都锁住了,但实际不是。索引
下面举几个例子说明:ip
数据从id =400000的数据开始,IsSuccess和GetTime字段都为0,如今若是400000数据的IsSuccess为1了。执行下面两条sql.get
-- 1: set autocommit=0; begin; select * from table1 where getTime < 1 and IsSuccess=0 order by id asc limit 0,30 for update; commit; -- 2: update table1 a set IsSuccess=0 where id =400000;
第一条sql语句先不commit,则第二条sql语句将只能等待,所以第二条sql语句把IsSuccess修改成0,IsSuccess非主键索引锁了值为0的索引数据,第二条sql语句将没法把数据更新到被锁的行里。it
再执行下面的sql语句
-- 1: set autocommit=0; begin; select * from table1 where getTime < 1 and IsSuccess=0 order by id asc limit 0,30 for update; commit; -- 2: update table1 a set IsSuccess=2 where id =400000;
这样第二条sql语句将能够执行。由于IsSuccess=2的索引段没有被锁。
上面的例子知道了锁索引段后还比较容易看懂,下面就奇葩一点:
先把id =400000数据的GetTime修改成1,IsSuccess=0,而后一次执行sql:
-- 1: set autocommit=0; begin; update ctripticketchangeresultdata a set issuccess=1 where id =400000; commit; -- 2: select * from table1 where getTime < 1 and IsSuccess=0 order by id asc limit 0,30 for update;
第1个sql先不commit,按照道理只会锁40000这行记录,第二个sql执行,按照道理只能查询从400001记录的30条记录,但第二个sql语句会阻塞等待。
缘由是第一个sql语句尚未commit也没有rollback,所以它先锁主键索引,再锁IsSuccess的非主键索引,第二个sql语句因为where里要判断IsSuccess字段的值,因为400000这条数据之前的IsSuccess是0,如今更新为1还不肯定,可能会回滚,所以sql2须要等待肯定400000这条数据的IsSuccess是否被修改。sql2的sql语句由于判断了GetTime<1,实际400000这条记录已经不知足了,但按照锁索引的原理,因此sql2语句会被阻塞。
所以若是根据业务场景,能够把sql2语句的IsSuccess条件取消掉,而且这里GetTime查询条件由GetTime<1修改成GetTime=0,这样便可不阻塞直接查询出来。
GetTime用范围查询致使的锁影响通过分析,还不是间隙锁的问题,感受应该是用范围做为条件,全部从第0行开始的全部查找范围都会被锁住。 好比这里更新400000会被阻塞,但更新400031不会被阻塞。
咱们项目出现死锁,就是这个原理,一条sql语句先锁主键索引,再锁非主键索引;另一条sql语句先锁非主键索引,再锁主键索引。虽然两个sql语句指望锁的数据行不同,但两个sql语句查询或更新的条件或结果字段若是有相同列,则可能会致使互相等待对方锁,2个sql语句即引发了死锁。
我的总结一下innodb存储引擎下的锁的分析,可能会有问题:
一、更新或查询for update的时候,会在where条件中开始为每一个字段判断是否有锁,若是有锁就会等待,由于若是有锁,那这个字段的值不肯定,只能等待锁commit或rollback后数据肯定后再查询。
二、另外还和order by有关系,由于可能前面数据有锁,但从后面查询一个范围就能够查询。
三、另外limit也有关系,好比limit 20,30从第20条记录取30行数据,但第一行数据若是被锁,由于不肯定回滚仍是提交,也会锁等待。
所以从筛选查询条件通过的地方都会判断锁,若是有锁,由于数据不肯定,都会等待锁释放。本文是我的测试结果,没有深刻分析内部原理,可能有不许确的地方。留做本身之后参考。