Database/SQL

낙관적 λ™μ‹œμ„± μ œμ–΄λ₯Ό μ μš©ν•˜λ©΄μ„œ 마주친 문제: νƒ€μž„μŠ€νƒ¬ν”„

μ±”πŸ» 2024. 2. 11. 12:24

βœ”οΈΒ κ²°λ‘ 

πŸ‘€Β λ¬Έμ œ μ •μ˜

μ²˜μŒμ— λ™μ‹œμ„± 문제λ₯Ό ν•΄κ²°ν•˜λ €κ³  ν•˜μ˜€μ„ λ•Œ, μ²˜μŒμ— 가정을 Isolation Level이 Repeatable Read둜 ν•˜λ©΄ μ•ˆ λœλ‹€λŠ” 잘λͺ»λœ κ°œλ…μ„ κ°€μ§€κ³  μžˆμ—ˆμ–΄μ„œ Read Committed둜 λ‚΄λ €μ•Ό λ˜λŠ” 쀄 μ•Œκ³  μ–˜λ₯Ό μœ„ν•΄ Read Committed둜 λ‚΄λ¦¬λŠ” 건 말이 μ•ˆ λ˜λŠ” 것 κ°™μ•„ 비관적 λ™μ‹œμ„± μ œμ–΄λ‘œ ν•΄κ²°ν•˜λŠ” 방법을 μ„ νƒν•˜μ˜€λŠ”λ° γ…Ž,,

잘λͺ»λœ μ§€μ‹μœΌλ‘œ 잘λͺ»λœ νŒλ‹¨μ„ 내리고 μžˆμ—ˆλ‹€ γ… γ… γ… 

JPAλ₯Ό μ‚¬μš©ν•œλ‹€λ©΄ Version으둜 κΉ”λ”ν•˜κ²Œ κ΅¬ν˜„μ΄ κ°€λŠ₯ν•˜λ˜λ°,

λ‚˜λŠ” Spring + MyBatisμ—¬μ„œ μΏΌλ¦¬λ¬Έμ—μ„œ CAS(compare-and-set) λ°©μ‹μœΌλ‘œ 낙관적 λ™μ‹œμ„± μ œμ–΄ 방법을 κ΅¬ν˜„ν•˜λŠ” 것을 μ‚¬μš©ν•˜μ˜€λ‹€.

μœ„μ—μ„œ λ§ν•œ 잘λͺ»λœ 지식을 κ°€μ§€κ³  일단 낙관적 λ™μ‹œμ„± μ œμ–΄λ₯Ό μ μš©ν•΄λ³΄μ•˜λ‹€

  1. Isolation Level을 MySQL의 기본인 Repeatable Read β†’ Read Committed둜 λ³€κ²½
    @Transactional(rollbackFor = Exception.class, isolation = Isolation.READ_COMMITTED)
    public void order(Order order) throws Exception {
        // 0-1. μž¬κ³ μˆ˜λŸ‰ κ°μ†Œ
        decreaseProdQty(order.getOrdDtlList());
  1. μž¬κ³ μˆ˜λŸ‰ κ°μ†Œ 쿼리에 UPD_DTTM(λ³€κ²½μΌμ‹œ)이 λ§ˆμ§€λ§‰μœΌλ‘œ 읽은 ν›„λ‘œ λ³€κ²½λ˜μ§€ μ•Šμ•˜μ„ λ•Œλ§Œ μˆ˜μ •λ˜λ„λ‘ 쿼리 μˆ˜μ •
    <update id="decreaseProdQty" parameterType="Map">
        UPDATE prod_opt
        SET INV_QTY = INV_QTY - #{qty}
          , UPD_DTTM = now()
        WHERE OPT_COMB_NO = #{optCombNo}
        AND UPD_DTTM = #{updDttm}
    </update>

β‡’ μ΄λ ‡κ²Œ μˆ˜μ •ν•œ ν›„ μž¬κ³ κ°€ 1개 남은 μƒν’ˆμ„ 2개의 μ“°λ ˆλ“œκ°€ λ™μ‹œμ— μ£Όλ¬Έν•˜λ„λ‘ ν•˜κ³  λŒλ €λ³΄μ•˜λŠ”λ° λ‚΄κ°€ κΈ°λŒ€ν•œ κ²°κ³Όκ°€ μ•„λ‹Œ μž¬κ³ μˆ˜λŸ‰μ΄ 여전이 -1이 됨

❓ 뢄석&μ‹œλ„

μ‹œλ„1) μ•ˆ λ˜λŠ” 이유λ₯Ό λ‚˜λ¦„ λΆ„μ„ν•΄λ³΄μ•˜λ‹€

μ•„λž˜λŠ” λ‚΄κ°€ μƒκ°ν•œ μœ„μ˜ μƒν™©μ˜ flowμ˜€λ‹€..

κ·ΈλŸ¬λ‹ˆκΉŒ 결둠은 νŠΈλžœμž­μ…˜1μ—μ„œ μž¬κ³ μˆ˜λŸ‰μ„ λ³€κ²½ν•΄ UPD_DTTM도 μ΅œμ‹ κ°’μœΌλ‘œ λ³€κ²½λ˜μ—ˆμœΌλ‚˜
Isolation Level이 Read Committedκ³  νŠΈλžœμž­μ…˜μ΄ λλ‚˜μ§€ μ•Šμ•„ 컀밋이 λ˜μ§€ μ•Šμ•˜κΈ° λ•Œλ¬Έμ— UPD_DTTMλŠ” μ—¬μ „νžˆ νŠΈλžœμž­μ…˜ μ‹œμž‘ μ‹œ μ½μ–΄μ˜¨ 값이닀.
λ”°λΌμ„œ UPD_DTTM μ»¬λŸΌμ„ λΉ„κ΅ν•˜λŠ” 검증 뢀뢄이 ν†΅κ³Όλ˜μ–΄ μž¬κ³ μˆ˜λŸ‰μ΄ κ°μ†Œλœλ‹€.

μ‹œκ°„ μˆœμ„œνŠΈλžœμž­μ…˜ 1 μƒνƒœνŠΈλžœμž­μ…˜ 2 μƒνƒœμ„€λͺ…
1Read: μž¬κ³ μˆ˜λŸ‰ & λ³€κ²½μΌμ‹œ
μž¬κ³ μˆ˜λŸ‰: 1개
λ³€κ²½μΌμ‹œ: 02/05/2024 10:02:18.000
2μž¬κ³ μˆ˜λŸ‰ 검증: 1개 이상이기 λ•Œλ¬Έμ— 톡과Read: μž¬κ³ μˆ˜λŸ‰ & λ³€κ²½μΌμ‹œ
μž¬κ³ μˆ˜λŸ‰: 1개
λ³€κ²½μΌμ‹œ: 02/05/2024 10:02:18.000
1decreaseProdQtyλ₯Ό μ‹€ν–‰ν•΄ μž¬κ³ μˆ˜λŸ‰μ„ κ°μ†Œμ‹œν‚¨λ‹€.
μ΄λ•Œ νŠΈλžœμž­μ…˜1이 μ‹œμž‘λœ μ΄ν›„λ‘œ μž¬κ³ μˆ˜λŸ‰μ΄ λ³€κ²½λ˜μ§€ μ•Šμ•˜κΈ° λ•Œλ¬Έμ—
UPD_DTTM μ»¬λŸΌμ„ λΉ„κ΅ν•˜λŠ” 검증 뢀뢄이 ν†΅κ³Όλ˜μ–΄ μž¬κ³ μˆ˜λŸ‰μ΄ κ°μ†Œλœλ‹€.
μž¬κ³ μˆ˜λŸ‰ 검증: 1개 이상이기 λ•Œλ¬Έμ— 톡과
2κ·Έ λ’€λ‘œ μ£Όλ¬Έ 둜직 생성decreaseProdQtyλ₯Ό μ‹€ν–‰ν•΄ μž¬κ³ μˆ˜λŸ‰μ„ κ°μ†Œμ‹œν‚¨λ‹€.
νŠΈλžœμž­μ…˜1μ—μ„œ μž¬κ³ μˆ˜λŸ‰μ„ λ³€κ²½ν•΄ UPD_DTTM도 μ΅œμ‹ κ°’μœΌλ‘œ λ³€κ²½λ˜μ—ˆμœΌλ‚˜
Isolation Level이 Read Committedκ³  νŠΈλžœμž­μ…˜μ΄ λλ‚˜μ§€ μ•Šμ•„ 컀밋이 λ˜μ§€ μ•Šμ•˜κΈ° λ•Œλ¬Έμ— UPD_DTTMλŠ” μ—¬μ „νžˆ νŠΈλžœμž­μ…˜ μ‹œμž‘ μ‹œ μ½μ–΄μ˜¨ 값이닀.
λ”°λΌμ„œ UPD_DTTM μ»¬λŸΌμ„ λΉ„κ΅ν•˜λŠ” 검증 뢀뢄이 ν†΅κ³Όλ˜μ–΄ μž¬κ³ μˆ˜λŸ‰μ΄ κ°μ†Œλœλ‹€.
3컀밋그 λ’€λ‘œ μ£Όλ¬Έ 둜직 생성
4κ²°κ³ΌλŠ” μž¬κ³ μˆ˜λŸ‰ 0컀밋
5κ²°κ³ΌλŠ” μž¬κ³ μˆ˜λŸ‰ -1

μ‹œλ„2)

μœ„μ—μ„œ μ •μ˜ν–ˆλ˜ μ΄μƒν•œ κ°€μ •μœΌλ‘œ 낙관적 λ™μ‹œμ„± μ œμ–΄λ₯Ό μ μš©ν•˜λŠ” κ³Όμ •μ—μ„œ ꡉμž₯히 μ‚½μ§ˆμ„ ν•˜κ³  μžˆμ—ˆλ‹€.

일반적으둜 λ™μ‹œμ„± 문제λ₯Ό ν•΄κ²°ν•˜λŠ” 방법은 크게 두 κ°€μ§€λ‹€.

  1. 비관적 λ™μ‹œμ„± μ œμ–΄(Pessimistic Concurrency Control)
  1. 낙관적 λ™μ‹œμ„± μ œμ–΄(Optimistic Concurrency Control)

μ—¬λŸ¬ μ±…μ—μ„œλŠ” 낙관적 λ™μ‹œμ„± μ œμ–΄λ₯Ό νƒ€μž„μŠ€νƒ¬ν”„λ₯Ό ν™œμš©ν•˜λŠ” λ°©λ²•μœΌλ‘œ μ†Œκ°œν•˜κ³  μžˆμ—ˆλ‹€. ν•˜μ§€λ§Œ μ‹€μ œλ‘œ μ μš©ν•΄ λ³΄μ•˜λŠ”λ° λ™μ‹œμ„± λ¬Έμ œκ°€ ν•΄κ²°λ˜μ§€ μ•Šμ•˜κ³ , 이유λ₯Ό λΆ„μ„ν•˜λ €λ‹€ 였히렀 문제 ν•΄κ²°κ³ΌλŠ” 점점 λ©€μ–΄μ§€λŠ” μ΄μƒν•œ 가정을 ν•˜κ²Œ λ˜μ—ˆλ‹€.

ꡬ글링을 톡해 λ‹€μ–‘ν•œ 해결책을 μ°Ύμ•„λ΄€μ§€λ§Œ, λŒ€λΆ€λΆ„ JPA의 @Version으둜 ν•΄κ²°ν•˜κ³  μžˆμ—ˆκ³  MyBatis μ‚¬μš© μ‚¬λ‘€λŠ” μ°ΎκΈ° μ–΄λ €μ› λ‹€. κ·ΈλŸ¬λ‹€ μ°Ύλ‹€μ°Ύλ‹€ λ“œλ””μ–΄ μš°μ—°νžˆ ν•œ λΈ”λ‘œκ·Έμ—μ„œ JPAκ°€ μ•„λ‹ˆλΌ λ‚΄κ°€ μ±…μ—μ„œ λ³Έ 쿼리λ₯Ό μ‚¬μš©ν•΄ λ™μ‹œμ„± 문제λ₯Ό ν•΄κ²°ν•œ 글을 λ°œκ²¬ν•˜κ²Œλ˜μ—ˆλ‹€..!

[DB] λ™μ‹œμ„± 문제 해결방법
λ™μ‹œμ„± λ¬Έμ œκ°€ 무엇이고 해결방법에 λŒ€ν•˜μ—¬ μ•Œμ•„λ³΄λ„λ‘ ν•œλ‹€.
https://chrisjune-13837.medium.com/db-λ™μ‹œμ„±-문제-해결방법-f5e52e2e3

λ‚˜λŠ” 이미 잘λͺ»λœ 가정에 깊이 λΉ μ Έ μžˆμ–΄μ„œ "μ € λ°©λ²•μœΌλ‘œ 해결될 리가 μ—†λ‹€"κ³  μƒκ°ν•˜λ©° λ°˜μ‹ λ°˜μ˜ν•˜λ©° μ†λŠ”μ…ˆμΉ˜κ³  μ‹œλ„λ₯Ό ν•΄λ³΄μ•˜λ‹€.

μž¬κ³ μˆ˜λŸ‰ κ°μ†ŒμΏΌλ¦¬μ— INV_QTY >= #{qty} 쑰건을 μΆ”κ°€ν•΄ μ‹€μ œ 재고 μˆ˜λŸ‰μ΄ μΆ©λΆ„ν•  λ•Œλ§Œ μ—…λ°μ΄νŠΈν•˜λ„λ‘ ν–ˆλ‹€. λ³€κ²½ ν›„ 격리 μˆ˜μ€€μ— 상관없이 Repeatable Read와 Read Committed λͺ¨λ‘ λ¬Έμ œμ—†μ΄ μž‘λ™ν•˜λŠ” 것을 확인할 수 μžˆμ—ˆλ‹€. 이 κ²°κ³ΌλŠ” λ‚΄κ°€ μ„Έμš΄ κ°€μ •κ³Ό μ „ν˜€ λ‹¬λžκ³ .. μ˜ˆμƒμΉ˜ λͺ»ν•œ κ²°κ³Όμ˜€λ‹€. λ‚΄ μ˜ˆμƒμœΌλ‘œλŠ” -1둜 λ™μ‹œμ„± λ¬Έμ œκ°€ 해결이 μ•ˆ λ˜μ—ˆμ–΄μ•Ό ν–ˆλŠ”λ° μ‹€μ œλ‘œλŠ” 0으둜 λ™μ‹œμ„± λ¬Έμ œκ°€ ν•΄κ²°λ˜μ—ˆλ‹€..

κΈ°μ‘΄

<update id="decreaseProdQty" parameterType="Map">
    UPDATE prod_opt
    SET INV_QTY = INV_QTY - #{qty}
      , UPD_DTTM = now()
    WHERE OPT_COMB_NO = #{optCombNo}
    AND UPD_DTTM = #{updDttm}
</update>

λ³€κ²½

<update id="decreaseProdQty" parameterType="Map">
    UPDATE prod_opt
    SET INV_QTY = INV_QTY - #{qty}
      , UPD_DTTM = now()
    WHERE OPT_COMB_NO = #{optCombNo}
    AND INV_QTY >= #{qty}
</update>

κ·Έ μ΄μœ μ— λŒ€ν•΄μ„œλŠ” μ•„λž˜ κΈ€ μ°Έκ³ .

낙관적 λ™μ‹œμ„± μ œμ–΄λ₯Ό μ μš©ν•˜λ©΄μ„œ 마주친 λ¬Έμ œλ“€: μž¬κ³ μˆ˜λŸ‰ 비ꡐ
πŸ‘€ μ˜λ¬Έμ β“ 원인 μΆ”λ‘  & μ‹œλ„1) 일단 Repeatable Read둜 μ‹œλ‚˜λ¦¬μ˜€ 짜보고 ν…ŒμŠ€νŠΈ2) μ±…, 자료 찾아보기3) ν—·κ°ˆλ Έλ˜ λΆ€λΆ„2πŸ”œ λ‚˜κ°€λ©°λ” μ•Œμ•„λ³Ό 것Ref μ•„λž˜λŠ” 제 μ‚½μ§ˆ 과정을 κΈ°λ‘ν•œ 글이고 ν‹€λ¦° λ‚΄μš©μ΄ ν¬ν•¨λ˜μ–΄μžˆμ„ 수 μžˆμŠ΅λ‹ˆλ‹€β€¦! πŸ‘€ μ˜λ¬Έμ μœ„ κ²Œμ‹œκΈ€μ—μ„œ μ •μ˜ν–ˆλ˜ μ΄μƒν•œ κ°€μ •μœΌλ‘œ 낙관적 λ™μ‹œμ„± μ œμ–΄λ₯Ό μ μš©ν•˜λŠ” κ³Όμ •μ—μ„œ ꡉμž₯히 μ‚½μ§ˆμ„ ν•˜κ³  μžˆμ—ˆλ‹€.일반적으둜 λ™μ‹œμ„± 문제λ₯Ό ν•΄κ²°ν•˜λŠ” 방법은 크게 두 κ°€μ§€λ‹€.비관적 λ™μ‹œμ„± μ œμ–΄(Pessimistic Concurrency Control)낙관적 λ™μ‹œμ„± μ œμ–΄(Optimistic Concurrency Control) μ—¬λŸ¬ μ±…μ—μ„œλŠ” 낙관적 λ™μ‹œμ„± μ œμ–΄λ₯Ό νƒ€μž„μŠ€νƒ¬ν”„λ₯Ό ν™œμš©ν•˜λŠ” λ°©λ²•μœΌλ‘œ μ†Œκ°œν•˜κ³  μžˆμ—ˆλ‹€. ν•˜μ§€λ§Œ μ‹€μ œλ‘œ μ μš©ν•΄ λ³΄μ•˜λŠ”λ° λ™μ‹œμ„± λ¬Έμ œκ°€ ν•΄κ²°λ˜μ§€ μ•Šμ•˜κ³ ..
https://beppp.tistory.com/126

3) 잘λͺ»λœ κ°œλ… λ°”λ‘œμž‘μ€ ν›„, νƒ€μž„μŠ€νƒ¬ν”„λ‘œ μ•ˆ λ˜λŠ” 이유 뢄석

μœ„ 글을 ν†΅ν•΄μž˜λͺ»λœ κ°œλ… λ°”λ‘œμž‘κ³ , κ·Έλž˜μ„œ μ™Όμͺ½μ΄λ‚˜ 였λ₯Έμͺ½μ΄λ‚˜ 같은 λ©”μ»€λ‹ˆμ¦˜μΈλ°.. μ•ˆ λ˜λŠ” 이유λ₯Ό λΆ„μ„ν•΄λ³΄μ•˜λ‹€.

처음 μˆ˜μ •μΌμ‹œλ₯Ό μ‘°νšŒν•΄μ˜¨ 값을 μ°μ–΄λ³΄μ•˜λŠ”λ°,

thread2

Executing order # on thread: μž¬κ³ μˆ˜λŸ‰ κ°μ†Œ ν›„ μˆ˜μ •μΌμ‹œ ν™•μΈμš©pool-1-thread-2prodQtyList2: [ProdOptDTO{optItemNm='null', optCombNo=1, optPrc=0, invQty=0, prodId=10001, shoesSize='null', regDttm=null, regrId=0, updDttm=Mon Feb 05 18:39:56 KST 2024, updrId=0, delDttm=null, delrId=0, delYn='null'}]

thread1

Executing order # on thread: μž¬κ³ μˆ˜λŸ‰ κ°μ†Œ ν›„ μˆ˜μ •μΌμ‹œ ν™•μΈμš©pool-1-thread-1prodQtyList2: [ProdOptDTO{optItemNm='null', optCombNo=1, optPrc=0, invQty=-1, prodId=10001, shoesSize='null', regDttm=null, regrId=0, updDttm=Mon Feb 05 18:39:56 KST 2024, updrId=0, delDttm=null, delrId=0, delYn='null'}]

updDttm이 μ‹œλΆ„β€™μ΄ˆβ€™κΉŒμ§€λ§Œ 값을 κ°€μ Έμ˜¨ κ±° 보고 ν˜Ήμ‹œ νŠΈλžœμž­μ…˜μ΄ λ„ˆλ¬΄ λΉ λ₯΄κ²Œ μ§„ν–‰λ˜μ–΄μ„œ κ·ΈλŸ°κ°€..? λΌλŠ” 생각이 번뜩 λ“€μ—ˆλ‹€.

결둠은 이게 λ§žμ•˜λ‹€..!

λͺ¨λ“  둜직이 μ „λΆ€ 56초 내에 μˆ˜ν–‰λ˜μ–΄

νŠΈλžœμž­μ…˜1이 값을 λ³€κ²½ν•œ ν›„ now()둜 updDttm을 μˆ˜μ •ν–ˆμ§€λ§Œ 처음 μ‘°νšŒν•΄μ˜¨ κ°’κ³Ό 같은 18:39:56이 μ €μž₯λ˜μ–΄μ„œ νŠΈλžœμž­μ…˜2κ°€ λ³€ν™”λ₯Ό κ°μ§€ν•˜μ§€ λͺ»ν•΄ λ§ˆμ§€λ§‰ 읽은 ν›„λ‘œ λ³€κ²½λ˜μ§€ μ•Šμ•˜λ‹€κ³  νŒλ‹¨ν•΄ μž¬κ³ μˆ˜λŸ‰μ„ 또 차감해 -1이 λœκ²ƒ..

μž¬κ³ κ°€ 1개 남은 μƒν’ˆμ„ 2개의 μ“°λ ˆλ“œκ°€ λ™μ‹œμ— μ£Όλ¬Έ

  1. μ²˜μŒμ— μž¬κ³ μˆ˜λŸ‰μ„ μ‘°νšŒν•΄μ˜¬ λ•Œ λ³€κ²½μΌμ‹œλ„ μ‘°νšŒν•΄μ˜¨λ‹€.
  1. A νŠΈλžœμž­μ…˜μ΄ λ¨Όμ € μˆ˜λŸ‰ 변경을 ν•œλ‹€.
  1. 그리고 또 거의 λ™μ‹œμ— B νŠΈλžœμž­μ…˜μ΄ μˆ˜λŸ‰μ„ λ³€κ²½ν•œλ‹€.
  1. 2번, 3번 λ³€κ²½ μ‹œμ°¨ 차이가 거의 μ—†λ‹€λ³΄λ‹ˆκΉŒ 2κ°€ λ³€κ²½λ˜μ–΄μ„œ μ»€λ°‹λ˜κΈ° 전에 3도 λ³€κ²½ μ‹œλ„λ₯Ό 함

πŸ” 원인

μˆ˜μ •μΌμ‹œ 컬럼이 λ°€λ¦¬μ΄ˆ(millisecond) λ‹¨μœ„κΉŒμ§€ μ €μž₯ν•˜μ§€ μ•Šκ³ , μ‹œλΆ„μ΄ˆ(second) λ‹¨μœ„κΉŒμ§€λ§Œ μ €μž₯ν•˜κΈ° λ•Œλ¬Έμ— λ°œμƒν•˜λŠ” λ¬Έμ œμ˜€λ‹€. 이 경우 같은 초 내에 μ—¬λŸ¬ νŠΈλžœμž­μ…˜μ΄ λ™μ‹œμ— 데이터λ₯Ό μˆ˜μ •ν•˜κ²Œ 되면 μˆ˜μ •μΌμ‹œλ₯Ό 기반으둜 변경을 κ°μ§€ν•˜λŠ” 경우 μ •ν™•ν•˜κ²Œ λ™μž‘ν•˜μ§€ μ•Šμ„ 수 μžˆλ‹€. 특히 κ³ μ†μœΌλ‘œ μ²˜λ¦¬λ˜λŠ” νŠΈλžœμž­μ…˜ ν™˜κ²½μ—μ„œλŠ” 같은 초 내에 μ—¬λŸ¬ 데이터가 변경될 κ°€λŠ₯성이 λ†’λ‹€.

βœ…Β ν•΄κ²°λ°©λ²•

  1. prod_opt ν…Œμ΄λΈ”μ˜ timestamp 컬럼의 νƒ€μž…μ„ timestamp(3)으둜 λ³€κ²½
  1. UPD_DTTM 값을 넣어쀄 λ•Œ now()κ°€ μ•„λ‹Œ CURRENT_TIMESTAMP(3)둜 넣어주도둝 쿼리 λ³€κ²½
<select id="selectProductQty" parameterType="int" resultType="ProdOptDTO">
    SELECT PO.PROD_ID, PO.OPT_COMB_NO, PO.INV_QTY, PO.UPD_DTTM
    FROM PROD_OPT PO
    WHERE OPT_COMB_NO IN
    <foreach collection="array" item="optCombNoArr" open="(" close=")" separator=",">
        #{optCombNoArr}
    </foreach>
</select>

<update id="decreaseProdQty" parameterType="Map">
    UPDATE prod_opt
    SET INV_QTY = INV_QTY - #{qty}
      , UPD_DTTM = CURRENT_TIMESTAMP(3)
    WHERE OPT_COMB_NO = #{optCombNo}
      AND UPD_DTTM = #{updDttm}
</update>

Uploaded by N2T