有个业务主要逻辑就是新增订单、修改订单、查询订单等操作。然后因为订单是不能重复的,所以当时在新增订单的时候做了幂等性校验,做法就是在新增订单记录之前,先通过 select … for update 语句查询订单是否存在,如果不存在才插入订单记录。
    而正是因为这样的操作,当业务量很大的时候,就可能会出现死锁。
    本案例是Innodb的默认隔离级别可重复读

    我建了一张订单表,其中 id 字段为主键索引,order_no 字段普通索引,也就是非唯一索引:
    CREATE TABLE t_order (
    id int NOT NULL AUTO_INCREMENT,
    order_no int DEFAULT NULL,
    create_date datetime DEFAULT NULL,
    PRIMARY KEY (id),
    KEY index_order (order_no) USING BTREE
    ) ENGINE=InnoDB ;

    然后,先 t_order 表里现在已经有了 6 条记录
    image.png
    假设这时有两事务,一个事务要插入订单 1007 ,另外一个事务要插入订单 1008,因为需要对订单做幂等性校验,所以两个事务先要查询该订单是否存在,不存在才插入记录,过程如下:
    死锁是如何发生的? - 图2
    可以看到,两个事务都陷入了等待状态(前提没有打开死锁检测),也就是发生了死锁,因为都在相互等待对方释放锁。
    这里在查询记录是否存在的时候,使用了 select … for update 语句,目的为了防止事务执行的过程中,有其他事务插入了记录,而出现幻读的问题。
    如果没有使用 select … for update 语句,而使用了单纯的 select 语句,如果是两个订单号一样的请求同时进来,就会出现两个重复的订单,有可能出现幻读,如下图:
    死锁是如何发生的? - 图3
    在执行下面这条语句的时候:
    select id from t_order where order_no = 1008 for update;

    因为 order_no 不是唯一索引,所以行锁的类型是间隙锁,于是间隙锁的范围是(1006, +∞)。那么,当事务 B 往间隙锁里插入 id = 1008 的记录就会被锁住。
    因为当我们执行以下插入语句时,会在插入间隙上再次获取插入意向锁。
    insert into t_order (order_no, create_date) values (1008, now());

    插入意向锁与间隙锁是冲突的,所以当其它事务持有该间隙的间隙锁时,需要等待其它事务释放间隙锁之后,才能获取到插入意向锁。而间隙锁与间隙锁之间是兼容的,所以所以两个事务中 select … for update 语句并不会相互影响。
    案例中的事务 A 和事务 B 在执行完后 select … for update 语句后都持有范围为(1006,+∞)的间隙锁,而接下来的插入操作为了获取到插入意向锁,都在等待对方事务的间隙锁释放,于是就造成了循环等待,导致死锁。