Error 1213 (40001): Deadlock found when trying to get lock
InnoDB 인덱스는 행 사이에는 gap 이라는 빈 구간이 있다.
a행 - (gap) - b행 - (gap) - c행
다음과 같은 A,B 양쪽에서 insert 하기전 존재 여부를 판단하기 위해 for update 로 gap lock 을 획득하려고 했다.
A: SELECT d FOR UPDATE -> 없음 -> gap lock 획득
B: SELECT e FOR UPDATE -> 없음 -> gap lock 획득 (gap lock 끼리는 공존)
A: INSERT or UPDATE d -> B 의 gap lock 에 막혀 대기
B: INSERT or UPDATE e -> A 의 gap lock 에 막혀 대기
서로를 기다리는 데드락 상태가 된다.
InnoDB 기본설정에 innodb_deadlock_detect=ON 있어 데드락을 감지하고 한쪽을 롤백시킨다.
User (부모 테이블) - UserResource (자식 테이블) 있다고 했을때
// 있으면 update 없으면 insert
func saveUserResource(tx *gorm.DB, userID uint64, val int64) error {
// 수정 전 - 자식 테이블에 아직 없는 행을 잠그면 데드락 발생할 수 있다.
// var userRC entity.UserResource
// err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("`user_id`=?", userID).Take(&userRC).Error
// 수정 후
// 항상 존재하는 부모 users 행을 PK 로 잠가 같은 사용자의 저장을 줄 세운다 (record lock).
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
Model(&entity.User{}).
Select("`id`").
Where("`id`=?", userID).
Take(&entity.User{}).Error; err != nil {
return err // 부모가 없으면 ErrRecordNotFound
}
// 자식 행은 잠금 없이 조회한다. 부모 잠금 덕분에 같은 사용자의 다른 저장은 여기 들어오지 못하고 기다린다.
var userRC entity.UserResource
err := tx.Where("`user_id`=?", userID).Take(&userRC).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return tx.Create(&entity.UserResource{UserID: userID, Val: val}).Error
} else if err != nil {
return err
}
return tx.Model(&userRC).Update("val", val).Error
}



