задний план
Исключение взаимоблокировки MySQL произошло в производственной среде, версия MySQL 5.6, уровень изоляции RC.
[CommandConsumer-pool-thread-1] Process error :
org.springframework.dao.DeadlockLoserDataAccessException:
### Error querying database. Cause: com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction
### The error may exist in class path resource [mybatis/mapper/sequence.xml]
### The error may involve defaultParameterMap
### The error occurred while setting parameters
### SQL: SELECT current_seq FROM sequence WHERE type = ? AND `date` = ? FOR UPDATE
### Cause: com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction
анализ кода
Согласно записям журнала, код ключа, вызывающий взаимоблокировку, выглядит следующим образом.
/**
* 根据传入参数,生成一个序列号。
*
* @param type 序列号类型
* @param date 时间
* @return 一个新的序列号,第一次调用返回1,后续根据调用次数递增。
*/
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.READ_COMMITTED)
public int getSequence(String type, LocalDate date) {
// select * from sequence where type = #{type} and date = #{date} for update
Sequence seq = mapper.selectForUpdate(type, date);
// seq 还未初始化,select for update 就没锁住
if (seq == null) {
// insert ignore into sequence(type, date, current_seq) values(#{type}, #{date}, #{currentSeq})
if (mapper.insertIgnore(type, date, 1)) {
return 1;
}
// insert ignore 竞争失败,重试
return getSequence(type, date);
}
// update sequence set current_seq = current_seq + 1 where id = #{id}
mapper.forwardSeq(seq.getId(), 1);
return seq.getCurrentSeq() + 1;
}
CREATE TABLE `sequence` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`type` varchar(32) NOT NULL COMMENT '类型',
`date` date NOT NULL COMMENT '时间',
`current_seq` int(11) NOT NULL COMMENT '当前最大序号',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_seq` (`date`,`type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='序列号'
Краткое описание функций
Этот код в основном реализует функцию получения серийного номера, которая часто используется для генерации номеров документов.
Например: нам нужно сгенерировать номер платежного поручения для каждого платежного поручения в формате: A-20200101, который представляет платежное поручение компании А в день 20200101.
Однако компания А имеет более одного платежного поручения в день.Чтобы обеспечить уникальность номера платежного поручения, нам также необходимо добавить автоматически увеличивающийся серийный номер. Например: A-20200101-1, что означает первую платежную квитанцию A от 01.01.2020 и т. д., номера второй и третьей платежной квитанции: A-20200101-2, A-20200101- 3...
Код
Чтобы гарантировать, что серийный номер не будет повторяться в параллельной среде, код сначала блокирует строку данных с помощью выбранного уникального индекса для обновления, затем обновляет current_seq = current_seq + 1 строки данных и возвращает current_seq. .
Но есть граничное условие, требующее специальной обработки, то есть при первом вызове функции данные еще не существуют, единственный индекс выборки для обновления возвращает нуль, а исходные данные с порядковым номером Необходимо вставить 1. Чтобы предотвратить возврат null при обновлении, блокировка приводит к множественным вставкам. В коде используется игнорирование вставки. Когда игнорирование вставки не удается, повторно вызывается (рекурсивно) getSequence для получения следующего порядкового номера.
После прочтения кода явных исключений обнаружено не было, мы попытались воспроизвести дедлок локально.
Локально воспроизводимый тупик:
Чтобы воспроизвести вручную:
- Условия приготовления
- MySQL 5.6
- Уровень изоляции транзакции RC
- Подготовьте два подключения к базе данных A, B
- Наблюдая за журналом SQL и после многих экспериментов, было обнаружено, что следующие две операции могут воспроизвести взаимоблокировку.
- Шаг операции 1
- Начало; вставить (игнорировать) xxx; не удалось, поскольку xxx уже существует.
- B начало; выберите xxx для обновления; блокирует, потому что A вставка уже удерживает блокировку
- Выберите xxx для обновления; успех
- Блокировка B заканчивается, вызывая тупик
- Шаг операции 2
- A начало; выберите xxx для обновления; успешно выполнено, эксклюзивная блокировка
- B начать; выбрать xxx для обновления; заблокировать, ожидая, пока A снимет монопольную блокировку
- Вставить (игнорировать) xxx; успешно выполнено
- Блокировка B заканчивается, вызывая тупик
- Общность запуска операций взаимоблокировки
- Все данные уже существуют. В транзакции получение блокировки с помощью вставки, а затем операция выбора для обновления или получение блокировки с помощью выбора для обновления, а затем операция вставки приведет к тому, что другие транзакции обновления, ожидающие блокировок, вызовут взаимоблокировку.
- Принцип тупика
- Пока не ясно (проходя мимо знакомых, просветите пожалуйста)
Воспроизведение модульного теста:
@Autowired
private ISequenceService sequenceService;
@Test
public void test() throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(10);
List<Runnable> runnableList = Lists.newLinkedList();
for (int i = 0; i < 100; i++) {
runnableList.add(() -> sequenceService.getSequence("TX", LocalDate.now()));
}
runnableList.forEach(executorService::execute);
executorService.shutdown();
executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
}
решение
- Вручную воспроизведя взаимоблокировку локально, мы обнаружили, что взаимоблокировка возникает только в том случае, если вставка игнорирования терпит неудачу в транзакции и выбираете для обновления, поэтому этого достаточно, чтобы избежать двух операций в одной и той же транзакции.
- код после замены
// SequenceDao
/**
* 根据传入参数,生成一个序列号。
*
* @param type 序列号类型
* @param date 时间
* @return 一个新的序列号,第一次调用返回1,后续根据调用次数递增。
*/
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.READ_COMMITTED)
public int getSequence(String type, LocalDate date) {
// select * from sequence where type = #{type} and date = #{date} for update
Sequence seq = mapper.selectForUpdate(type, date);
// seq 还未初始化,select for update 就没锁住
if (seq == null) {
// insert ignore into sequence(type, date, current_seq) values(#{type}, #{date}, #{currentSeq})
if (mapper.insertIgnore(type, date, 1)) {
return 1;
}
// insert ignore 竞争失败,返回-1,由调用方重试。
return -1;
}
// update sequence set current_seq = current_seq + 1 where id = #{id}
mapper.forwardSeq(seq.getId(), 1);
return seq.getCurrentSeq() + 1;
}
// 调用方代码
@Override
public int newSequence(String type, LocalDate date) {
int sequence = dao.getSequence(type, date);
if (sequence < 0) {
// 第一次生成,insert 失败的重试
return dao.getSequence(type, date);
}
return sequence;
}
- После проверки модульного теста проблема взаимоблокировки была успешно решена.
Суммировать
- Следует избегать рекурсии в методах, содержащих несколько блокировок, поскольку рекурсия может привести к получению нескольких блокировок в непоследовательном порядке, что приведет к взаимоблокировкам.
- В данном примере есть две операции по ручному воспроизведению дедлока.Первая это вставка (игнорирование) после выбора для обновления.Такой код написать вообще невозможно.По логике вещей мы будем выбирать не для того,чтобы узнать данные,а также вставьте те же данные. Но второй тип вставки (игнорировать) после выбора для обновления, мы можем непреднамеренно написать такой код, и он в основном такой же, как вставка после выбора для обновления с точки зрения блокировки, которая вызовет взаимоблокировку, и ежедневное написание кода. обратить особое внимание на этот случай.