select .. for update ๋์ ์ ๋ฌด์ ๋ฐ๋ฅธ ์ ๊ธ ์ํ
๊ฐ์
InnoDB ์์ง์ ๊ธฐ๋ณธ์ ์ผ๋ก DDL ์ฟผ๋ฆฌ๋ฅผ ์ ์ธํ ๋ชจ๋ ๋ฐ์ดํฐ ์กฐ์ ์์ ์์ ํ ์ด๋ธ ๋ฝ์ ์ฌ์ฉํ์ง ์๊ณ ๋ ์ฝ๋ ๊ธฐ๋ฐ์ ์ ๊ธ ๋ฐฉ์์ ์ฌ์ฉํฉ๋๋ค. ๋ ์์ธํ๋ ๋ ์ฝ๋ ์์ฒด๋ณด๋ค๋ ์ธ๋ฑ์ค์ ์ ๊ธ์ ์ค์ ํ์ฌ ์ด๋ฃจ์ด์ง๋ฉฐ, ํ ์ด๋ธ์ ๋ช ์์ ์ธ ์ธ๋ฑ์ค๊ฐ ์๋ ๊ฒฝ์ฐ์๋ ๋ด๋ถ์ ์ผ๋ก ์์ฑ๋ ํด๋ฌ์คํฐ ์ธ๋ฑ์ค๋ฅผ ํตํด ์ ๊ธ์ด ์ด๋ฃจ์ด์ง๋๋ค. ๊ทธ๋ฆฌ๊ณ Repetable-Read ๊ฒฉ๋ฆฌ ์์ค์์ InnoDB๋ record lock๊ณผ gap lock์ ๊ฒฐํฉํ Next-key lock์ ํ์ฉํ์ฌ Phantom Read๋ฅผ ๋ฐฉ์งํฉ๋๋ค.
์กฐ๊ฑด์ ๋ถํฉํ๋ ํน์ ํ์ ์ฐพ๊ธฐ ์ํด ์ธ๋ฑ์ค๋ฅผ ์ค์บํ๋ ๊ณผ์ ์์, InnoDB๋ ํด๋น ์ธ๋ฑ์ค ๋ ์ฝ๋๋ฟ๋ง ์๋๋ผ ๊ทธ ์ด์ ๊ณต๊ฐ์๋ ์ ๊ธ์ ์ค์ ํฉ๋๋ค. ๋ฐ๋ผ์ ์ฒซ ๋ฒ์งธ ๋ฐ๊ฒฌ๋ ๋ ์ฝ๋์ ์ฟผ๋ฆฌ๊ฐ ์ ์ํ ๋ฒ์ ๋ด์ ๋น ๊ณต๊ฐ์์ ์๋ก์ด insert ์์ ์ ๋ฐฉ์งํฉ๋๋ค. ์๋ฅผ ๋ค์ด id ๊ฐ์ด 1, 8, 12, 13, 16์ธ ๋ ์ฝ๋๊ฐ ํฌํจ๋ ํ ์ด๋ธ์ ๋ํด ์๋์ ์ฟผ๋ฆฌ๋ฅผ ์คํํ๋ค๊ณ ๊ฐ์ ํด ๋ด ์๋ค.
SELECT * FROM t_user WHERE id > 10 FOR UPDATE;
์ด ์ฟผ๋ฆฌ๋ฅผ ์คํํ๋ฉด id๊ฐ 10 ๋ณด๋ค ํฐ ๋ ์ฝ๋๋ค์ ๋ํด record lock์ด ์ ์ฉ๋ฉ๋๋ค. ๋ํ ์กด์ฌํ์ง ์๋ ๊ฐ(ex: 11)๊ณผ ์ฟผ๋ฆฌ ์กฐ๊ฑด ๋ฒ์ ๋ฐ์ ์๋ ๊ฐ(ex: 9)์ ๋ํด์๋ ์ค์ ๋ก ์กด์ฌํ๋ ๋ ์ฝ๋ ์ฌ์ด์ ๊ฐ๊ฒฉ(gap)์ gap lock์ด ์ค์ ๋์ด, ํด๋น ๋ฒ์ ๋ด์์ ์๋ก์ด ๋ ์ฝ๋์ insert๋ฅผ ๋ฐฉ์งํฉ๋๋ค.
- id > 10์ ๋ง์กฑํ๋ ์ฒซ ๋ฒ์งธ ์ธ๋ฑ์ค ๋ ์ฝ๋ 12๋ฅผ ๋ฐ๊ฒฌ
- ์ฒซ ๋ฒ์งธ ์ธ๋ฑ์ค ๋ฐ๊ฒฌ ์ง์ ์ ์ธ๋ฑ์ค ๋ ์ฝ๋ id 8๋ถํฐ id 12 ์ฌ์ด์ gap lock ์ ์ฉ
- id> > 10 ์ธ ๋ชจ๋ ์ธ๋ฑ์ค ๋ ์ฝ๋๋ค์ ์ฌ์ด์๋ gap lock ์ ์ฉ
- id > 10์ธ ๋ชจ๋ ์ธ๋ฑ์ค ๋ ์ฝ๋๋ค์ ๊ฐ๊ฐ record lock ์ ์ฉ
์ ์ง์์ ๊ธฐ๋ฐ์ผ๋ก ์ ๋ 'select for update ๊ตฌ๋ฌธ์ ์ฌ์ฉ ์ ํ ์ด๋ธ์ ์ด๋ฌด ๊ฐ๋ ์์ผ๋ฉด ๋ฝ์ด ๊ฑธ๋ฆฌ๊น?'์ ๋ํ ์๋ฌธ์ ์ด ๋ค์์ต๋๋ค. ์๋ํ๋ฉด ๊ตฌ๊ฐ์ ์ค์บํ์ฌ ์กฐ๊ฑด์ ๋ง์กฑํ๋ ์ธ๋ฑ์ค ๋ ์ฝ๋์ ๋ํด Lock์ ์ค์ ํ๋ ๊ณผ์ ์ ๊ณ ๋ คํ์ ๋, ํ ์ด๋ธ์ ๋จ ํ ๊ฐ์ ๊ฐ๋ ์์ผ๋ฉด ์ ๊ธ์ ์ค์ ํ ๊ธฐ์ค์ด ๋๋ ์ธ๋ฑ์ค ๋ ์ฝ๋๊ฐ ๋ถ์ฌํ๋ฏ๋ก ์ ๊ธ์ด ์ค์ ๋์ง ์์ ๊ฒ์ด๋ผ ์์ํ๊ธฐ ๋๋ฌธ์ ๋๋ค.
์ด๋ฌํ ์ํฉ์ ๋ ๊ฐ์ง ์๋๋ฆฌ์ค ๋๋๋ฉด ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
- ํ์ฌ ํ ์ด๋ธ์ ์ด๋ค ๊ฐ(row)์ด ์กด์ฌํ ๋: select * for update๋ฅผ ํตํด ์ ์ฒด ๋ ์ฝ๋๋ฅผ ์กฐํํ๋ ๊ฒฝ์ฐ, ์ฌ๋ฌ ํธ๋์ญ์ ์์ ์๋ก์ด ๊ฐ(row)์ ์ถ๊ฐ(Insert)๊ฐ ๊ฐ๋ฅํ ๊น?
- ํ์ฌ ํ ์ด๋ธ์ ๋จ ํ ๊ฐ์ ๊ฐ(row)๋ ์กด์ฌํ์ง ์์ ๋: select * for update๋ฅผ ํตํด ์ ์ฒด ๋ ์ฝ๋๋ฅผ ์กฐํํ๋ ๊ฒฝ์ฐ, ์ฌ๋ฌ ํธ๋์ญ์ ์์ ์๋ก์ด ๊ฐ(row)์ ์ถ๊ฐ(Insert)๊ฐ ๊ฐ๋ฅํ ๊น?
์ค์ ์์๋ฅผ ๋ค์ด, ์๋ MemberSerivce์์ findAllWithLock ๋ฉ์๋๋ฅผ ํตํด ์ ์ฒด ํ์์ ์กฐํํ๊ณ , ํน์ ์กฐ๊ฑด์ ๋ฐ๋ผ ์๋ก์ด ํ์์ ์ถ๊ฐํ๋ ์ํฉ์ ๊ณ ๋ คํ์ต๋๋ค. Member ํ ์ด๋ธ์ 1๊ฐ ์ด์์ ๊ฐ(row)์ ์กด์ฌ ์ ๋ฌด์ ๋ฐ๋ผ, 10๊ฐ๋ณด๋ค ๋ง์ member๊ฐ ์ ์ฅ์ด ๋ ์ง์ ๋ํด ์ด ๊ธ์ ๋ณด์๋ ๋ถ๋ค๋ ํ๋ฒ ๊ณ ๋ฏผํด ๋ณด์๋ฉด ์ข์ ๊ฒ ๊ฐ์ต๋๋ค.
@Service
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
/**
* ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ํ์ฌ ๋ฉค๋ฒ ์๋ฅผ ์กฐํํ๊ณ , 10๋ช
๋ฏธ๋ง์ธ ๊ฒฝ์ฐ ์๋ก์ด ๋ฉค๋ฒ๋ฅผ ์ถ๊ฐ.
* ์ด ๊ณผ์ ์์ SELECT ... FOR UPDATE๋ฅผ ์ฌ์ฉํ์ฌ ๋์์ฑ์ ๊ด๋ฆฌํ๋ฉฐ,
* findAllWithLock() ๋ฉ์๋๊ฐ ๋ฉค๋ฒ ๋ชฉ๋ก์ ์ ๊ธ ์ํ๋ก ์กฐํ
*/
@Transactional
public void registerMember() {
if (memberRepository.findAllWithLock().size() < 10) {
memberRepository.save(new Member());
}
}
}
public interface MemberRepository extends JpaRepository<Member, Long> {
@Query(value = "SELECT * FROM member for update", nativeQuery = true)
List<Member> findAllWithLock();
}
๋จผ์ ์ฒซ ๋ฒ์งธ ์๋๋ฆฌ์ค์ธ 'ํ์ฌ ํ ์ด๋ธ์ ์ด๋ค ๊ฐ(row)์ด ์กด์ฌํ ๋, select * for update๋ฅผ ํตํด ์ ์ฒด ๋ ์ฝ๋๋ฅผ ์กฐํํ๋ ๊ฒฝ์ฐ ์ฌ๋ฌ ํธ๋์ญ์ ์์ ์๋ก์ด ๊ฐ(row)์ ์ถ๊ฐ(Insert)๊ฐ ๊ฐ๋ฅํ ๊น?'์ ๋ํด์๋ ์ ๋ ์ฒ์์ Member ํ ์ด๋ธ์ ํ์ฌ ์ด๋ ํ ๊ฐ์ด๋ผ๋ ์กด์ฌํ๋ ๊ฒฝ์ฐ๋ผ๋ฉด ์กด์ฌํ๋ ๊ฐ๋ค์ ๊ธฐ์ค์ผ๋ก ์ผ์ Next-Key Lock์ ๊ฑธ์ด 10๊ฐ ๋ณด๋ค ๋ง์ Member๊ฐ ์ ์ฅ๋์ง ์์ ๊ฒ์ด๋ผ๊ณ ์๊ฐํ์ต๋๋ค. ๊ทธ๋ฆฌ๊ณ ๊ฒฐ๋ก ์ ์ผ๋ก ์ด๋ ์ ๋ต์ด์์ต๋๋ค.
๋ ๋ฒ์งธ ์๋๋ฆฌ์ค์ธ 'ํ์ฌ ํ ์ด๋ธ์ ๋จ ํ ๊ฐ์ ๊ฐ(row)๋ ์กด์ฌํ์ง ์์ ๋, select * for update๋ฅผ ํตํด ์ ์ฒด ๋ ์ฝ๋๋ฅผ ์กฐํํ๋ ๊ฒฝ์ฐ ์ฌ๋ฌ ํธ๋์ญ์ ์์ ์๋ก์ด ๊ฐ(row)์ ์ถ๊ฐ(Insert)๊ฐ ๊ฐ๋ฅํ ๊น?'์ ๋ํด์๋ ์ ๋ ํ์ฌ ํ ์ด๋ธ์ ์ด๋ค ๊ฐ๋ ์กด์ฌํ์ง ์์ผ๋ฉด ๊ธฐ์ค์ด ๋ ์ธ๋ฑ์ค๊ฐ ์์ด Lock์ด ์ค์ ๋์ง ์์ ๊ฒ์ด๋ผ ์๊ฐํ์ต๋๋ค. ๋ฐ๋ผ์ ๋์์ ์ฌ๋ฌ ์ค๋ ๋์์ if๋ฌธ์ ํต๊ณผํ์ฌ 10๊ฐ ์ด์์ Member๊ฐ ์ ์ฅ๋ ์ ์๋ ๋์์ฑ ๋ฌธ์ ๊ฐ ๋ฐ์ํ ๊ฒ์ด๋ผ ์๊ฐํ์ต๋๋ค. ๊ทธ๋ฆฌ๊ณ ์ด๋ ๊ฒฐ๋ก ์ ์ผ๋ก ํ๋ ธ์ต๋๋ค.
ํ ์คํธ
์๋๋ select for update ๊ตฌ๋ฌธ์ ์ฌ์ฉํ ๋, ์กฐํ ๋์์ด ์๋ ๊ฒฝ์ฐ์๋ ์ ๊ธ์ด ๊ฑธ๋ฆด ์ ์๋์ง์ ๋ํ ํ ์คํธ์ ๋๋ค. InnoDB๋ฅผ ์ฌ์ฉํ๋ MariaDB ํ๊ฒฝ์์ ์งํํ์ผ๋ฉฐ, ํธ๋์ญ์ ์ ๊ฒฉ๋ฆฌ ์์ค์ ๋ณ๋๋ก ์ค์ ํ์ง ์์๊ธฐ ๋๋ฌธ์ ๊ธฐ๋ณธ๊ฐ์ธ REPETABLE-READ๊ฐ ์ ์ฉ๋ฉ๋๋ค.
MariaDB [(none)]> show variables like '%isol%';
+-----------------------+-----------------+
| Variable_name | Value |
+-----------------------+-----------------+
| transaction_isolation | REPEATABLE-READ |
| tx_isolation | REPEATABLE-READ |
+-----------------------+-----------------+
member ํ ์ด๋ธ์ ์์ฑํ๊ณ , ์ด๊ธฐ์๋ ํ ์ด๋ธ์ ์ด๋ ํ ๊ฐ๋ ์กด์ฌํ์ง ์๋ ์ํ๋ก ์์ํฉ๋๋ค.
CREATE TABLE member (
id bigint AUTO_INCREMENT PRIMARY KEY
);
MariaDB [test]> select * from member;
Empty set (0.002 sec)
1. ์กฐ๊ฑด ์๋ ์กฐํ
ํ ์ด๋ธ์ ์๋ฌด๋ฐ ์กฐ๊ฑด ์์ด select .. for update๋ฅผ ์คํํ์ฌ ์ ์ฒด ํ ์ด๋ธ์ ์ ๊ธ์ด ๊ฑธ๋ฆฌ๋์ง ํ์ธํด ๋ด ์๋ค.
ํธ๋์ญ์ A์์๋ ๋จผ์ select * from member for update๋ฅผ ์คํํ ํ, ์๋ก์ด ๊ฐ์ ์ถ๊ฐํ๋ฉฐ, ํธ๋์ญ์ B์์๋ ๋์ผํ๊ฒ select * from member for update๋ฅผ ์คํํ๋ ค๊ณ ์๋ํฉ๋๋ค.
-- Transaction A
start transaction;
select * from member for update;
insert into member (id) values(default);
commit;
-- Transaction B
start transaction;
select * from member for update;
insert into member (id) values(default);
commit;
๊ฒฐ๊ณผ๋ฅผ ๋ณด๋ฉด ํธ๋์ญ์ A๋ ์ฑ๊ณต์ ์ผ๋ก INSERT๋ฅผ ์ํํ์ง๋ง, ์์ง commit์ด๋ rollback์ ํ์ง ์์๊ธฐ ๋๋ฌธ์ ํธ๋์ญ์ B๋ ํธ๋์ญ์ A์ ์์ ์ด ์๋ฃ๋ ๋๊น์ง select ์์ ์ ์ํํ์ง ๋ชปํ๊ณ ๋๊ธฐ ์ํ์ ๋์ด๊ฒ ๋ฉ๋๋ค. ๋ฐ๋ผ์ ์ด๋ฅผ ํตํด select .. for update ๊ตฌ๋ฌธ์ด ์กฐํ ๋์ ๋ ์ฝ๋๊ฐ ์กด์ฌํ์ง ์๊ฑฐ๋ ์ฌ์ง์ด ํ ์ด๋ธ์ ๋จ ํ๋์ ๊ฐ๋ ์กด์ฌํ์ง ์๋ ๊ฒฝ์ฐ์๋ ์ ๊ธ์ด ๊ฑธ๋ฆฐ๋ค๋ ๊ฒ์ ํ์ธํ ์ ์์ต๋๋ค.
MariaDB [test]> SELECT
-> r.trx_id AS waiting_trx_id,
-> r.trx_mysql_thread_id AS waiting_thread,
-> r.trx_query AS waiting_query,
-> b.trx_id AS blocking_trx_id,
-> b.trx_mysql_thread_id AS blocking_thread,
-> b.trx_query AS blocking_query,
-> l.lock_id AS requested_lock_id,
-> l.lock_mode AS requested_lock_mode,
-> l.lock_table,
-> l.lock_index,
-> l.lock_type
-> FROM information_schema.innodb_lock_waits w
-> INNER JOIN information_schema.innodb_locks l ON w.requested_lock_id = l.lock_id
-> INNER JOIN information_schema.innodb_trx r ON w.requesting_trx_id = r.trx_id
-> INNER JOIN information_schema.innodb_trx b ON w.blocking_trx_id = b.trx_id;
+----------------+----------------+-------------------------------------------------------------------------------+
| waiting_trx_id | waiting_thread | waiting_query |
+----------------+----------------+-------------------------------------------------------------------------------+
| 9044 | 6 | SET STATEMENT SQL_SELECT_LIMIT=501 FOR select * from member for update |
+----------------+----------------+-------------------------------------------------------------------------------+
| blocking_trx_id| blocking_thread| blocking_query |
+----------------+----------------+-------------------------------------------------------------------------------+
| 9041 | 5 | NULL |
+----------------+----------------+-------------------------------------------------------------------------------+
| requested_lock_id | requested_lock_mode | lock_table | lock_index | lock_type |
+-------------------+---------------------+-----------------+------------+-----------------------------------+
| 9044:178:3:2 | X | `test`.`member` | PRIMARY | RECORD |
+-------------------+---------------------+-----------------+------------+-----------------------------------+
์ค์ ๋ก information_schema ์กฐํ๋ฅผ ํตํด ์์ธํ ๊ฒฐ๊ณผ๋ฅผ ๋ณด๋ฉด select * from member for update ์ฟผ๋ฆฌ๋ ํ ์ด๋ธ์ ๋ฐ์ดํฐ๊ฐ ์๋๋ผ๋ ํ ์ด๋ธ์ ๊ฐ๋ฅํ ๋ชจ๋ ๊ฐ์ ๋ ์ฝ๋์ ๋ํด ์ ๊ธ์ ์๋ํ๋ค๋ ๊ฒ์ lock_type์ด record๋ก ํ์๋์ด ์๋ ๊ฒ์์ ์ ์ ์์ต๋๋ค.
๊ฒฐ๋ก ์ ์ผ๋ก ํธ๋์ญ์ A์ B๋ ํ ์ด๋ธ์ ํ ์ด๋ธ์ ๊ฐ์ด ์์์๋ ๋ถ๊ตฌํ๊ณ ๋ชจ๋ ๋ ์ฝ๋๋ฅผ ๋์์ผ๋ก Exclusive Lock์ ์๋ํ๊ธฐ ๋๋ฌธ์, ํธ๋์ญ์ A๊ฐ select * for member for update๋ฅผ ์คํํ์ฌ ์ ๊ธ์ ๊ฑธ๊ณ ์๋ค๋ฉด ํธ๋์ญ์ B๋ ํธ๋์ญ์ A๊ฐ ์๋ฃ๋ ๋๊น์ง ๋๊ธฐํด์ผ ํฉ๋๋ค. ๊ทธ๋ฆฌ๊ณ ์ด์ ์ ์์ ์ฝ๋์ ๋ํด ์๋ ํ ์คํธ๋ฅผ ์ํํ๋ฉด 10๊ฐ ์ดํ์ ๋ฉค๋ฒ๋ง ์ ์ฅ๋๋ ๊ฒ์ ํ์ธํ ์ ์์ต๋๋ค.
@SpringBootTest(classes = TestProjectApplication.class)
class MemberServiceConcurrentTest {
@Autowired
private MemberService memberService;
@Autowired
private MemberRepository memberRepository;
@Test
@DisplayName("10๊ฐ ์ดํ์ ๋ฉค๋ฒ๋ง ๋ฑ๋ก๋์ด์ผ ํ๋ค.")
void ensure_at_most_ten_members_registered_concurrently() throws InterruptedException {
final int numberOfThreads = 200;
CountDownLatch latch = new CountDownLatch(numberOfThreads);
ExecutorService executor = Executors.newFixedThreadPool(64);
for (int i = 0; i < numberOfThreads; i++) {
executor.submit(() -> {
try {
memberService.registerMember();
}
finally {
latch.countDown();
}
});
}
latch.await();
long savedMembersCount = memberRepository.count();
// ์ ์ฅ๋ ๋ฉค๋ฒ์ ์๊ฐ 10 ์ดํ์ธ์ง ๊ฒ์ฆ
assertTrue(savedMembersCount <= 10, "๋ฑ๋ก๋ ๋ฉค๋ฒ ์๊ฐ 10 ์ดํ์ด์ด์ผ ํฉ๋๋ค.");
}
}
2. ์๋ก ๋ค๋ฅธ ๋ฒ์์ ์กฐํ
๋ง์ฐฌ๊ฐ์ง๋ก ๋น์ด์๋ member ํ ์ด๋ธ์ ๋์์ผ๋ก ์ด๋ฒ์๋ ์๋ก ๋ค๋ฅธ ๋ฒ์๋ฅผ ๊ฐ์ง select .. for update ์ฟผ๋ฆฌ๋ฅผ ๋ค๋ฅธ ํธ๋์ญ์ ์์ ๋์์ ์คํํ๋ฉด ์ด๋ป๊ฒ ๋๋์ง ์ดํด๋ด ์๋ค.
-- Transaction A
start transaction;
select * from member where id > 20 and id < 30 for update;
insert into member (id) values(default);
commit;
-- Transaction B
start transaction;
select * from member where id > 50 and id < 60 for update;
insert into member (id) values(default);
commit;
์ ๋ ์กฐ๊ฑด์ ์ ๋ฒ์๋ฅผ ์ง์ ํด ์ฃผ๋๋ผ๋ ํ ์ด๋ธ์ ๊ฐ์ด ์์ผ๋ฏ๋ก ์ฒซ ๋ฒ์งธ ํ ์คํธ ๊ฒฐ๊ณผ์ ๊ฐ์ด ํ ์ด๋ธ์ ๊ฐ๋ฅํ ๋ชจ๋ ๊ฐ์ ๋ ์ฝ๋์ ๋ํด ์ ๊ธ์ ์๋ํ์ฌ ํธ๋์ญ์ A์ ์ปค๋ฐ ๋๋ ๋กค๋ฐฑ์ด ์๋ฃ๋๊ธฐ ์ ๊น์ง๋ ํธ๋์ญ์ B๊ฐ select .. for update ์คํ ๋ถ๋ถ์์ ๋๊ธฐํ ๊ฒ์ด๋ผ ์์ํ์ต๋๋ค. ํ์ง๋ง ์ค์ ๋ก๋ ์๋ก ๋ค๋ฅธ ๋ฒ์์ ๋ํ select .. for update ์ฟผ๋ฆฌ๋ค์ ๋์์ ์คํ๋์ด, ๊ฐ๊ฐ์ ๋ฒ์์ ๋ํด ๋ ๋ฆฝ์ ์ธ ์ ๊ธ์ด ์ค์ ๋๋ฉฐ insert ๊ตฌ๋ฌธ์์ ๋๊ธฐํ๊ณ ์๋ ๊ฒ์ ํ์ธํ ์ ์์ต๋๋ค.
MariaDB [test]> SELECT
-> r.trx_id AS waiting_trx_id,
-> r.trx_mysql_thread_id AS waiting_thread,
-> r.trx_query AS waiting_query,
-> b.trx_id AS blocking_trx_id,
-> b.trx_mysql_thread_id AS blocking_thread,
-> b.trx_query AS blocking_query,
-> l.lock_id AS requested_lock_id,
-> l.lock_mode AS requested_lock_mode,
-> l.lock_table,
-> l.lock_index,
-> l.lock_type
-> FROM information_schema.innodb_lock_waits w
-> INNER JOIN information_schema.innodb_locks l ON w.requested_lock_id = l.lock_id
-> INNER JOIN information_schema.innodb_trx r ON w.requesting_trx_id = r.trx_id
-> INNER JOIN information_schema.innodb_trx b ON w.blocking_trx_id = b.trx_id;
+----------------+----------------+-----------------------------------------+-----------------+-----------------+----------------+-------------------+---------------------+-----------------+------------+-----------+
| waiting_trx_id | waiting_thread | waiting_query | blocking_trx_id | blocking_thread | blocking_query | requested_lock_id | requested_lock_mode | lock_table | lock_index | lock_type |
+----------------+----------------+-----------------------------------------+-----------------+-----------------+----------------+-------------------+---------------------+-----------------+------------+-----------+
| 9049 | 6 | insert into member (id) values(default) | 9048 | 5 | NULL | 9049:178:3:1 | X | `test`.`member` | PRIMARY | RECORD |
+----------------+----------------+-----------------------------------------+-----------------+-----------------+----------------+-------------------+---------------------+-----------------+------------+-----------+
์ด๋ฌํ ์ด์ ๋ ๊ฐ ํธ๋์ญ์ ์ด ์๋ก ๋ค๋ฅธ ๋ฒ์๋ฅผ ๋์์ผ๋ก select .. for update ์ฟผ๋ฆฌ๋ฅผ ์คํํ๋ฉด, InnoDB๋ ์ง์ ๋ ๊ฐ ๋ฒ์์ ๋ํด ๋ ๋ฆฝ์ ์ผ๋ก Gap Lock์ ์ค์ ํ๊ธฐ ๋๋ฌธ์ ๋๋ค. ๊ทธ๋ฆฌ๊ณ ๋ง์ฐฌ๊ฐ์ง๋ก ํ ์ด๋ธ์ ์ค์ ๋ก ์กด์ฌํ๋ ๊ฐ๋ฟ๋ง ์๋๋ผ ์กด์ฌํ์ง ์๋ ๊ฐ์ ๋ํด์๋ ์ง์ ๋ ๋ฒ์์ ๋ฐ๋ผ ๋ณ๋์ ์ ๊ธ์ด ๊ฑธ๋ฆฝ๋๋ค.
๊ฒฐ๋ก ์ ์ผ๋ก InnoDB๋ ์กฐํ๋๋ ๊ฐ์ด ์๋ ์ํฉ์์๋ ์กฐ๊ฑด์ ์ ์ฃผ์ด์ง ๋ฒ์์ ๊ธฐ๋ฐํ์ฌ ์ ๊ธ์ ์ค์ ํ๋ฉฐ, ์๋ก ๋ค๋ฅธ ๋ฒ์๋ฅผ ๋์์ผ๋ก ํ๋ ํธ๋์ญ์
๋ค์ ๊ฐ์์ ์ง์ ๋ ๋ฒ์ ๋ด์์๋ง ์ ๊ธ์ ๋ฐ๊ฒ ๋ฉ๋๋ค. ์ด๋ก ์ธํด ๋์ผ ํ
์ด๋ธ์์ ์กฐ๊ฑด์ ์ ๋ฐ๋ผ ์๋ก ๋ค๋ฅธ ๋ฒ์์ ๋ํ ์ฟผ๋ฆฌ๋ค์ด ๋์์ ์ํ๋ ์ ์์ต๋๋ค.
3. ๊ฒน์น๋ ๋ฒ์ ์กฐํ
๋ง์ฐฌ๊ฐ์ง๋ก ๋น์ด์๋ member ํ ์ด๋ธ์์ ๊ฒน์น๋ ๋ฒ์๋ฅผ ๋์์ผ๋ก select .. for update ์ฟผ๋ฆฌ๋ฅผ ์คํํ์ฌ, ์ด๋ฒ์๋ ๊ฒน์น๋ ๋ถ๋ถ์์ ์ ๊ธ์ด ์ด๋ป๊ฒ ๊ฑธ๋ฆฌ๋์ง ํ์ธํด ๋ด ์๋ค.
-- Transaction A
start transaction;
select * from member where id > 10 and id < 20 for update;
insert into member (id) values(default);
commit;
-- Transaction B
start transaction;
select * from member where id > 15 and id < 25 for update;
insert into member (id) values(default);
commit;
์ ๊ฒฐ๊ณผ๋ ์ด๋จ๊น์? ํ ์คํธ 1๋ฒ๊ณผ 2๋ฒ๊ณผ ๊ฒฐ๊ณผ๋ฅผ ๋ดค์ ๋ ์ด๋ฒ ํ ์คํธ๋ ์ผ๋จ์ ๋ฒ์๊ฐ ์ฃผ์ด์ง๊ธฐ ๋๋ฌธ์ ์ฃผ์ด์ง ๋ฒ์๋ก ๋ฝ์ด ๊ฑธ์ด์ง ๊ฒ์ผ๋ก ์์๋๋ฉฐ, ๋ฒ์๊ฐ ๊ฒน์น๊ธฐ ๋๋ฌธ์ ๋ณ๋๋ก ์ ๊ธ์ด ๊ฑธ๋ฆฌ์ง ์์ ํธ๋์ญ์ B๋ select ๋ถ๋ถ์์ ํธ๋์ญ์ A๋ฅผ ๊ธฐ๋ค๋ฆด ๊ฒ์ผ๋ก ๊ธฐ๋ํ ์ ์์ต๋๋ค.
๊ทธ๋ฐ๋ฐ ์ด๊ฒ ๋ฌด์จ ์ผ์ผ๊น์? ์ค์ ๊ฒฐ๊ณผ๋ ํธ๋์ญ์ B์์ select .. for update ์ฟผ๋ฆฌ๊ฐ ๋ฐ๋ก ์คํ๋๋ฉฐ, ๊ฒน์น๋ ๋ฒ์์๋ ๋ถ๊ตฌํ๊ณ ํธ๋์ญ์ A์ ์๋ฃ๋ฅผ select๊ฐ ์๋ insert ๊ตฌ๋ฌธ์์ ๋๊ธฐํ๋ ๊ฒ์ ํ์ธํ ์ ์์ต๋๋ค. ์ด์ ๋ ํธ๋์ญ์ A์ ํธ๋์ญ์ B๊ฐ ๊ฒน์น๋ ๋ฒ์์ ๋ํด ์ ๊ธ์ ์๋ํ์ง๋ง, ์ค์ ๋ก ๋ฐ์ดํฐ๊ฐ ์กด์ฌํ์ง ์๊ธฐ ๋๋ฌธ์ InnoDB๊ฐ Gap-Lock์ ์ฌ์ฉํ์ฌ ์กฐํ๋ ํ๋ฝํ๊ณ , ์ด ๋ฒ์์ ๋ํ ์๋ก์ด ๋ฐ์ดํฐ์ ์ฝ์ ์ ๋ฐฉ์งํ๊ธฐ ๋๋ฌธ์ ๋๋ค. ๋ฐ๋ผ์ ํธ๋์ญ์ B๊ฐ ๋๊ธฐํ๋ ์์น๋ ์ฝ์ ์ ์๋ํ๋ ์ง์ ์ด ๋ฉ๋๋ค.
MariaDB [test]> SELECT
-> r.trx_id AS waiting_trx_id,
-> r.trx_mysql_thread_id AS waiting_thread,
-> r.trx_query AS waiting_query,
-> b.trx_id AS blocking_trx_id,
-> b.trx_mysql_thread_id AS blocking_thread,
-> b.trx_query AS blocking_query,
-> l.lock_id AS requested_lock_id,
-> l.lock_mode AS requested_lock_mode,
-> l.lock_table,
-> l.lock_index,
-> l.lock_type
-> FROM information_schema.innodb_lock_waits w
-> INNER JOIN information_schema.innodb_locks l ON w.requested_lock_id = l.lock_id
-> INNER JOIN information_schema.innodb_trx r ON w.requesting_trx_id = r.trx_id
-> INNER JOIN information_schema.innodb_trx b ON w.blocking_trx_id = b.trx_id;
+----------------+----------------+-----------------------------------------+-----------------+-----------------+----------------+-------------------+---------------------+-----------------+------------+-----------+
| waiting_trx_id | waiting_thread | waiting_query | blocking_trx_id | blocking_thread | blocking_query | requested_lock_id | requested_lock_mode | lock_table | lock_index | lock_type |
+----------------+----------------+-----------------------------------------+-----------------+-----------------+----------------+-------------------+---------------------+-----------------+------------+-----------+
| 9073 | 6 | insert into member (id) values(default) | 9072 | 5 | NULL | 9073:179:3:1 | X | `test`.`member` | PRIMARY | RECORD |
+----------------+----------------+-----------------------------------------+-----------------+-----------------+----------------+-------------------+---------------------+-----------------+------------+-----------+
๋ ์์ธํ๋ ๋น๊ทผ ๊ธฐ์ ๋ธ๋ก๊ทธ์ ์๋ ๋ถ๋ถ์ ์ฐธ๊ณ ํ๋ฉด ๋์์ด ๋๋๋ฐ,
๋ ํธ๋์ญ์ ์ด ์๋ก ๋๊ธฐํ์ง ์๊ณ select .. for update ์ฟผ๋ฆฌ๋ฅผ ์คํํ ์ ์๋ ์ด์ ๋, Gap Lock์ด Shared Lock์ ํํ๋ก๋ง ์กด์ฌํ๊ธฐ ๋๋ฌธ์ ๋๋ค. ์ฆ ๋น์ด์๋ member ํ ์ด๋ธ์์ ํธ๋์ญ์ A์ B๊ฐ ๊ฒน์น๋ ๋ฒ์์ ๋ํด select .. for update ์ฟผ๋ฆฌ๋ฅผ ์คํํ ๋ ์ค์ ๋ก ๋ฐ์ดํฐ๊ฐ ์กด์ฌํ์ง ์์์๋ ๋ถ๊ตฌํ๊ณ ํด๋น ๋ฒ์์ ๋ํด Gap Lock์ด ์ ์ฉ๋์ง๋ง, Gap Lock์ Shared Lock ํํ๋ก๋ง ์กด์ฌํ๊ธฐ ๋๋ฌธ์ ์ฌ๋ฌ ํธ๋์ญ์ ์์ ๋์ผํ ๋ฒ์์ ๋ํด select .. for update๋ฅผ ์คํํด๋ ์ ๊ธ ๊ฒฝํฉ์ด ๋ฐ์ํ์ง ์์ต๋๋ค.
๊ทธ๋ ๋ค๋ฉด ๋ง์ฝ ๋ฒ์ ๋ด์ธ member id๊ฐ 16์ด๋ผ๋ ๊ฐ์ด ์๋ ๊ฒฝ์ฐ์๋ ์ด๋ป๊ฒ ๋ ๊น์?
MariaDB [test]> select * from member;
+----+
| id |
+----+
| 16 |
+----+
1 row in set (0.002 sec)
+----------------+----------------+--------------------------------------------------------------------------------------------------+-----------------+-----------------+----------------+-------------------+---------------------+-----------------+------------+-----------+
| waiting_trx_id | waiting_thread | waiting_query | blocking_trx_id | blocking_thread | blocking_query | requested_lock_id | requested_lock_mode | lock_table | lock_index | lock_type |
+----------------+----------------+--------------------------------------------------------------------------------------------------+-----------------+-----------------+----------------+-------------------+---------------------+-----------------+------------+-----------+
| 9086 | 6 | SET STATEMENT SQL_SELECT_LIMIT=501 FOR select * from member where id > 15 and id < 25 for update | 9083 | 5 | NULL | 9086:180:3:2 | X | `test`.`member` | PRIMARY | RECORD |
+----------------+----------------+--------------------------------------------------------------------------------------------------+-----------------+-----------------+----------------+-------------------+---------------------+-----------------+------------+-----------+
ํด๋น ๋ฒ์์ ํด๋นํ๋ ๊ฐ์ด ์กด์ฌํ๋ค๋ฉด ์ด๋ฒ์๋ ํธ๋์ญ์ B๊ฐ select .. for update ๊ตฌ๋ฌธ์์๋ถํฐ ํธ๋์ญ์ A์ ์์ ์๋ฃ๋ฅผ ๋๊ธฐํ๊ฒ ๋ฉ๋๋ค. ์ด๋ select .. for update ๊ตฌ๋ฌธ์ด ํด๋น ๋ฒ์ ๋ด ์กด์ฌํ๋ ๋ชจ๋ ์ธ๋ฑ์ค ๋ ์ฝ๋์ ๋ํด ์ ๊ธ์ ์ค์ ํ๊ธฐ ๋๋ฌธ์ด๋ฉฐ, ์ค์ ๋ฐ์ดํฐ๊ฐ ์๋ ๊ฒฝ์ฐ์๋ ๋ค๋ฅธ ํธ๋์ญ์ ์ด ๊ทธ ๋ฐ์ดํฐ์ ๋ํ ์์ ์ ์์ํ๊ธฐ ์ ์ ๊ธฐ์กด ํธ๋์ญ์ ์ ์๋ฃ๋ฅผ ๊ธฐ๋ค๋ ค์ผ ํ๊ธฐ ๋๋ฌธ์ ๋๋ค.
- ํธ๋์ญ์ A ์คํ: ํธ๋์ญ์ A๋ select .. for update ์ฟผ๋ฆฌ๋ฅผ ์ฌ์ฉํ์ฌ id ๊ฐ์ด 16์ธ ํ์ ๋ํด Exclusive Lock์ ์ค์ ํฉ๋๋ค. ์ด๋ฅผ ํตํด ํด๋น ํ์ ์์ ํ๊ฑฐ๋ ์ ๊ทผํ๋ ๋ค๋ฅธ ํธ๋์ญ์ ์ ์์ ์ ๋ฐฉ์งํฉ๋๋ค.
- ํธ๋์ญ์ B ๋๊ธฐ: ํธ๋์ญ์ B ์ญ์ ๊ฒน์น๋ ๋ฒ์์์ select .. for update๋ฅผ ์คํํ์ฌ id๊ฐ 16์ธ ํ์ ํฌํจํ๋ ค ํฉ๋๋ค. ๊ทธ๋ฌ๋ ์ด ํ์ ์ด๋ฏธ ํธ๋์ญ์ A์ ์ํด ์ ๊ฒจ ์๊ธฐ ๋๋ฌธ์, ํธ๋์ญ์ B๋ ํธ๋์ญ์ A์ ์ปค๋ฐ ๋๋ ๋กค๋ฐฑ์ด ์๋ฃ๋์ด ํด๋น ์ ๊ธ์ด ํด์ ๋ ๋๊น์ง ๋๊ธฐ ์ํ์ ๋ค์ด๊ฐ๋๋ค.
๊ฒฐ๋ก ์ ์ผ๋ก ๊ฒน์น๋ ๋ฒ์ ์กฐํ์ ๊ฒฝ์ฐ ๋น์ด ์๋ ํ ์ด๋ธ ์ํฉ์์๋ select .. for update ๊ตฌ๋ฌธ์ด ์ฆ์ ์คํ๋๊ณ ์ค์ ๋ฐ์ดํฐ ์ฝ์ ์์ ์์๋ง ๋ค๋ฅธ ํธ๋์ญ์ ์ ์๋ฃ๋ฅผ ๋๊ธฐํฉ๋๋ค. ๋ฐ๋ฉด ๋ฒ์ ๋ด์ ์ค์ ๋ฐ์ดํฐ๊ฐ ์กด์ฌํ๋ ๊ฒฝ์ฐ์๋ ์กฐํ ๋จ๊ณ๋ถํฐ ๋ค๋ฅธ ํธ๋์ญ์ ์ ์๋ฃ๋ฅผ ๋๊ธฐํ๊ฒ ๋ฉ๋๋ค.
์ ๋ฆฌ
select .. for update๋ฅผ ํตํด ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์๋ก์ด ๋ฐ์ดํฐ์ ์ฝ์ ์ ํจ๊ณผ์ ์ผ๋ก ๋ฐฉ์งํ ์ ์์์ ํ์ธํ์ต๋๋ค. ํ์ง๋ง ์ค์ ํ ์คํธ๋ฅผ ํตํด ํ์ธํ๋ฏ์ด ์ ๊ธ์ด ๋ฐ์ํ๋ ์์ ์ด ์ฃผ์ด์ง ์ํฉ์ ๋ฐ๋ผ select ๋ช ๋ น์ด์ ์คํ ๋จ๊ณ์์ ๋ฐ์ํ ์๋ ์๊ณ , insert ๋ช ๋ น์ด์ ์คํ ๋จ๊ณ์์๋ ๋ฐ์ํ ์ ์๋ค๋ ์ ์ ๊ณ ๋ คํ์ฌ ๋น์ฆ๋์ค ๋ก์ง์์ ์์์น ๋ชปํ ๊ฒฐ๊ณผ๊ฐ ๋ฐ์ํ ์ ์์ผ๋ฏ๋ก ์ฃผ์ํ๋ฉด ์ข์ ๊ฒ ๊ฐ์ต๋๋ค.
์๋ฅผ ๋ค์ด ์ฃผ์ด์ง ์ํฉ์ ๋ฐ๋ผ select ๋ถ๋ถ์ด ์ ์์ ์ผ๋ก ์คํ๋์ด ๋ค์๊ณผ ๊ฐ์ ์ฝ๋์์ something()๋ก์ง์ด ์ํ๋ ๊ฒฝ์ฐ ์์์น ๋ชปํ ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์ ์์ต๋๋ค.
@Service
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
@Transactional
public void registerMember() {
if (memberRepository.findAllWithLock().size() < 10) {
something();
memberRepository.save(new Member());
}
}
}
ํผ๋๋ฐฑ์ ์ธ์ ๋ ์ง ํ์์ด๋ฉฐ ํ๋ฆฐ ๋ถ๋ถ์ด ์๋ค๋ฉด ๋๊ธ ๋ฌ์์ฃผ์๋ฉด ๋น ๋ฅด๊ฒ ๋ฐ์ํ๊ฒ ์ต๋๋ค.
์ฐธ๊ณ
- https://miintto.github.io/docs/mysql-select-for-update
- https://medium.com/daangn/mysql-gap-lock-%EB%8B%A4%EC%8B%9C%EB%B3%B4%EA%B8%B0-7f47ea3f68bc
- https://medium.com/daangn/mysql-gap-lock-%EB%91%90%EB%B2%88%EC%A7%B8-%EC%9D%B4%EC%95%BC%EA%B8%B0-49727c005084
- https://dev.mysql.com/doc/refman/8.3/en/innodb-locking.html