MYSQL自增列引起死锁

Posted 冰冰与铛铛

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MYSQL自增列引起死锁相关的知识,希望对你有一定的参考价值。

这次开发支付对帐时,持久化对帐数据时线上突然出现下面这个死锁.
### Error updating database. Cause: com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction
### The error may involve com.imxiaomai.pay.service.reconcile.PayReconcileDao.insert-Inline
### The error occurred while setting parameters
### SQL: insert into pay_reconcile ( trade_time, shop_no, out_trade_no, out_order_no, total_fee, cost, system_id, payment_category, create_time, direction, refund_original_order_no ) select ?, ?, ?, ?, ?, ?, ?, ?, now(), ?, ? FROM dual WHERE NOT EXISTS ( SELECT 1 FROM pay_reconcile where out_trade_no=? AND `out_order_no`=? )
### Cause: com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction
; SQL []; Deadlock found when trying to get lock; try restarting transaction; nested exception is com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction[com.imxiaomai.pay.service.reconcile.GenericReconcileHandler:run]
org.springframework.dao.DeadlockLoserDataAccessException:
### Error updating database. Cause: com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction
经过初步分析,这个SQL是单条数据插入,mysql是行级锁,不应该发生死锁.然后找DBA查看数据库死锁,找到如下死锁语句.
018-01-18 11:03:15 2adb0dc40700TOO DEEP OR LONG SEARCH IN THE LOCK TABLE WAITS-FOR GRAPH, WE WILL ROLL BACK FOLLOWING TRANSACTION
 
*** TRANSACTION:
TRANSACTION 121106127, ACTIVE 0.041 sec setting auto-inc lock
mysql tables in use 2, locked 1
1 lock struct(s), heap size 360, 0 row lock(s)
MySQL thread id 1443086444, OS thread handle 0x2adb0dc40700, query id 195863600 10.171.134.40 bldhq executing
insert into pay_reconcile
( trade_time,
shop_no,
out_trade_no,
out_order_no,
total_fee,
cost,
system_id,
payment_category,
create_time,
direction,
refund_original_order_no )
select ‘2018-01-17 16:20:13.0‘,
‘XM0003‘,
‘4200000069201801175791162100‘,
‘118301180117162014‘,
2120,
13,
1,
1,
now(),
1,
null
FROM dual
WHERE NOT EXISTS (
SELECT 1 FROM pay_reconcile where out_trade_no=‘4200000069201801175791162100‘
AND `out_order_no`=‘118301180117162014‘
 
)
*** WAITING FOR THIS LOCK TO BE GRANTED:
TABLE LOCK table `bldshop`.`pay_reconcile` trx id 121106127 lock mode AUTO-INC waiting
*** WE ROLL BACK TRANSACTION (2)
产生原因:采用线程池(10个)插入数据时,自增列排队过长导致事务排队过长.而MYSQL自增列默认锁表.
 
【1】innodb_autoinc_lock_mode 的说明
  innodb_auto_lockmode有三个取值:
    1、0 这个表示tradition 传统
    2、1 这个表示consecutive 连续
    3、2 这个表示interleaved 交错
【1.1】tradition(innodb_autoinc_lock_mode=0) 模式:
  1、它提供了一个向后兼容的能力
  2、在这一模式下,所有的insert语句("insert like") 都要在语句开始的时候得到一个
     表级的auto_inc锁,在语句结束的时候才释放这把锁,注意呀,这里说的是语句级而不是事务级的,
       一个事务可能包涵有一个或多个语句。
  3、它能保证值分配的可预见性,与连续性,可重复性,这个也就保证了insert语句在复制到slave
          的时候还能生成和master那边一样的值(它保证了基于语句复制的安全)。
     4、由于在这种模式下auto_inc锁一直要保持到语句的结束,所以这个就影响到了并发的插入。
 
【1.2】consecutive(innodb_autoinc_lock_mode=1) 模式:
  1、这一模式下去simple insert 做了优化,由于simple insert一次性插入值的个数可以立马得到
          确定,所以mysql可以一次生成几个连续的值,用于这个insert语句;总的来说这个对复制也是安全的
          (它保证了基于语句复制的安全)
  2、这一模式也是mysql的默认模式,这个模式的好处是auto_inc锁不要一直保持到语句的结束,只要
          语句得到了相应的值后就可以提前释放锁
 
【1.3】interleaved(innodb_autoinc_lock_mode=2) 模式
  1、由于这个模式下已经没有了auto_inc锁,所以这个模式下的性能是最好的;但是它也有一个问题,就是
          对于同一个语句来说它所得到的auto_incremant值可能不是连续的。
 
【2】如果你的二进制文件格式是mixed | row 那么这三个值中的任何一个对于你来说都是复制安全的。
  由于现在mysql已经推荐把二进制的格式设置成row,所以在binlog_format不是statement的情况下最
  好是innodb_autoinc_lock_mode=2 这样可能知道更好的性能。
 
解决方法:
1.服务程序生成唯一ID,不使用数据库自增ID.(采用twitter-的snowflake算法.源于UUID变种)
2.调整线程池大小,由10个变成2个.减少数据库压力..
3.待线上验证....(线上验证一周,未发现死锁.)
4.部署时需要配置启动参数... -Dinstance.group 与 -Dinstance.id参数,且这两个值的取值范围为0-31.

  1 public class Snowflake implements SerialNumber {
  2     // ==============================Fields===========================================
  3     /** 开始时间截 (2015-01-01) */
  4     private final long twepoch = 1420041600000L;
  5 
  6     /** 机器id所占的位数 */
  7     private final long workerIdBits = 5L;
  8 
  9     /** 数据标识id所占的位数 */
 10     private final long datacenterIdBits = 5L;
 11 
 12     /** 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数) */
 13     private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
 14 
 15     /** 支持的最大数据标识id,结果是31 */
 16     private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
 17 
 18     /** 序列在id中占的位数 */
 19     private final long sequenceBits = 12L;
 20 
 21     /** 机器ID向左移12位 */
 22     private final long workerIdShift = sequenceBits;
 23 
 24     /** 数据标识id向左移17位(12+5) */
 25     private final long datacenterIdShift = sequenceBits + workerIdBits;
 26 
 27     /** 时间截向左移22位(5+5+12) */
 28     private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
 29 
 30     /** 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095) */
 31     private final long sequenceMask = -1L ^ (-1L << sequenceBits);
 32 
 33     /** 工作机器ID(0~31) */
 34     private long workerId;
 35 
 36     /** 数据中心ID(0~31) */
 37     private long datacenterId;
 38 
 39     /** 毫秒内序列(0~4095) */
 40     private long sequence = 0L;
 41 
 42     /** 上次生成ID的时间截 */
 43     private long lastTimestamp = -1L;
 44 
 45     private static Snowflake singleInstance;
 46     private static final String INSTANCE_GROUP="instance.group";
 47 
 48     private static final String INSTANCE_ID="instance.id";
 49     public static Snowflake create(){
 50         if(null==singleInstance){
 51             synchronized (Snowflake.class){
 52                 if(null==singleInstance){
 53                     String group=System.getProperty(INSTANCE_GROUP);
 54                     String id=System.getProperty(INSTANCE_ID);
 55                     if(StringUtils.isBlank(group)){
 56                         throw  new RuntimeException("instance.group must be exist and great -1 and less 32");
 57                     }
 58                     if(StringUtils.isBlank(id)){
 59                         throw  new RuntimeException("instance.id must be exist and great -1 and less 32");
 60                     }
 61                     if(!NumberUtils.isDigits(group)){
 62                         throw  new RuntimeException("instance.group must be exist and great -1 and less 32");
 63                     }
 64                     if(!NumberUtils.isDigits(id)){
 65                         throw  new RuntimeException("instance.id must be exist and great -1 and less 32");
 66                     }
 67                     int g=Integer.valueOf(group);
 68                     int i=Integer.valueOf(id);
 69                     if(g<0||g>=32){
 70                         throw  new RuntimeException("instance.group must be exist and great -1 and less 32");
 71                     }
 72                     if(i<0||i>=32){
 73                         throw  new RuntimeException("instance.id must be exist and great -1 and less 32");
 74                     }
 75                     singleInstance=new Snowflake(i,g);
 76                 }
 77             }
 78         }
 79         return singleInstance;
 80     }
 81     //==============================Constructors=====================================
 82     /**
 83      * 构造函数
 84      * @param workerId 工作ID (0~31)
 85      * @param datacenterId 数据中心ID (0~31)
 86      */
 87     private Snowflake(long workerId, long datacenterId) {
 88 
 89 
 90         if (workerId > maxWorkerId || workerId < 0) {
 91             throw new IllegalArgumentException(String.format("worker Id can‘t be greater than %d or less than 0", maxWorkerId));
 92         }
 93         if (datacenterId > maxDatacenterId || datacenterId < 0) {
 94             throw new IllegalArgumentException(String.format("datacenter Id can‘t be greater than %d or less than 0", maxDatacenterId));
 95         }
 96         this.workerId = workerId;
 97         this.datacenterId = datacenterId;
 98     }
 99 
100     // ==============================Methods==========================================
101     /**
102      * 获得下一个ID (该方法是线程安全的)
103      * @return SnowflakeId
104      */
105     @Override
106     public synchronized long nextId() {
107         long timestamp = timeGen();
108 
109         //如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
110         if (timestamp < lastTimestamp) {
111             throw new RuntimeException(
112                     String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
113         }
114 
115         //如果是同一时间生成的,则进行毫秒内序列
116         if (lastTimestamp == timestamp) {
117             sequence = (sequence + 1) & sequenceMask;
118             //毫秒内序列溢出
119             if (sequence == 0) {
120                 //阻塞到下一个毫秒,获得新的时间戳
121                 timestamp = tilNextMillis(lastTimestamp);
122             }
123         }
124         //时间戳改变,毫秒内序列重置
125         else {
126             sequence = 0L;
127         }
128 
129         //上次生成ID的时间截
130         lastTimestamp = timestamp;
131 
132         //移位并通过或运算拼到一起组成64位的ID
133         return ((timestamp - twepoch) << timestampLeftShift) //
134                 | (datacenterId << datacenterIdShift) //
135                 | (workerId << workerIdShift) //
136                 | sequence;
137     }
138 
139     /**
140      * 阻塞到下一个毫秒,直到获得新的时间戳
141      * @param lastTimestamp 上次生成ID的时间截
142      * @return 当前时间戳
143      */
144     protected long tilNextMillis(long lastTimestamp) {
145         long timestamp = timeGen();
146         while (timestamp <= lastTimestamp) {
147             timestamp = timeGen();
148         }
149         return timestamp;
150     }
151 
152     /**
153      * 返回以毫秒为单位的当前时间
154      * @return 当前时间(毫秒)
155      */
156     protected long timeGen() {
157         return System.currentTimeMillis();
158     }
159 
160     public static void main(String[] args) {
161         Snowflake snowflake=new Snowflake(1,1);
162         ExecutorService executorService= Executors.newFixedThreadPool(50);
163         Map<Long,Integer> map=new ConcurrentHashMap<> (1000000);
164         Integer tag=Integer.valueOf(0);
165         for (long i=0;i<1000000;++i) {
166             executorService.execute(new Runnable() {
167                 @Override
168                 public void run() {
169                     Long val=Long.valueOf(snowflake.nextId());
170                     map.put(val,tag);
171                     System.out.println(val);
172                 }
173             });
174         }
175         System.out.println(map.size());
176         try {
177             System.in.read();
178             while (1000000!=map.size()){
179                 System.out.println(map.size());
180             }
181             System.out.println(map.size());
182             executorService.shutdown();
183             System.in.read();
184 
185         }catch (Throwable throwable){
186             System.out.println(throwable.getMessage());
187         }
188     }
189 }

 

以上是关于MYSQL自增列引起死锁的主要内容,如果未能解决你的问题,请参考以下文章

主键非自增列 EF 插入数据库引起的 ID 列不能为 NULL 的错误

MySQL插入自增列数据的问题

mysql如何增加一自增列

怎么重置mysql的自增列AUTO

MySQL--自增列持久化问题

Mysql 自增列 主键