DevLog ๐ถ
๋์์ฑ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํด๋ณด์! ๋ถ์ฐ๋ฝ ๊ตฌํํ๊ธฐ (๋ค์๋๋ฝ - Named Lock ํ์ฉํ๊ธฐ) ๋ณธ๋ฌธ
๋์์ฑ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํด๋ณด์! ๋ถ์ฐ๋ฝ ๊ตฌํํ๊ธฐ (๋ค์๋๋ฝ - Named Lock ํ์ฉํ๊ธฐ)
dolmeng2 2023. 6. 23. 17:39๐ฑ ๋ค์ด๊ฐ๊ธฐ ์
DB ๊ณต๋ถํ๋ค๊ฐ '๋ค์๋๋ฝ'์ ๋ํด์ ์๊ฒ ๋์๋๋ฐ, ๋ค์๋๋ฝ์ ์ฌ์ฉํ๋ฉด ๋ถ์ฐ๋ฝ์ ๊ตฌํํ ์ ์๋ค๋ ๊ธ์ ๋ณด๊ณ ํ ๋ฒ ํ ์คํธํด๋ณด๊ณ ์ถ์ด์ ๊ธ์ ์์ฑํด๋ณด๊ณ ์ ํ๋ค. ์ ์ฒด ์์ค์ฝ๋๋ ์ฌ๊ธฐ์์ ํ์ธ ๊ฐ๋ฅํ๋ค. (๋ญ๊ฐ ํ ์คํธ์ฉ ๋ ํฌ ๋ง๋ค๊ธฐ ์ ๋งคํด์ ๊ทธ๋ฅ ์ ์ฐ๋ ๋ ํฌ์๋ค๊ฐ ํ๋ ค๋ค ๋ณด๋ ์ฝํ๋ฆฐ์ผ๋ก ์์ฑํ๊ฒ ๋์๋ค.)
๐ฑ ๋ถ์ฐ๋ฝ์ด๋?
๋ถ์ฐ๋ฝ์ด๋ ๋ฉํฐ ์ค๋ ๋ ํ๊ฒฝ์์ ๊ณต์ ์์์ ์ ๊ทผํ ๋, ๋ฐ์ดํฐ์ ์ ํฉ์ฑ์ ์งํค๊ธฐ ์ํด ์ฌ์ฉํ๋ ๊ธฐ์ ์ด๋ค.
์ฌ๊ธฐ์ ์ฌ๋ฌ ์ค๋ ๋๊ฐ ๊ณต์ ์์์ ์ ๊ทผํ๋ฉฐ ๊ฒฝ์ํ๋ ์ํฉ์ 'Race Condition (๊ฒฝ์ ์ํ)'๋ผ๊ณ ๋ ๋ถ๋ฅด๋ฉฐ, ์๋ฐ์์๋ 'synchronized'๋ผ๋ ํค์๋๋ฅผ ํตํด์ ํ๋์ ์ค๋ ๋๋ง ์ ๊ทผํ ์ ์๋๋ก ๋๊ธฐํ ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ค. ํ์ง๋ง, ์คํ๋ง ์น ์ ํ๋ฆฌ์ผ์ด์ ํ๊ฒฝ์์ ๊ท๋ชจ๊ฐ ์ปค์ง๋ค๋ฉด ์๋ฒ ์ญ์ ์ฌ๋ฌ ๋๋ก ๋์ธ ํ๋ฅ ์ด ๋๋ค. ์ด๋ฌํ ๋ค์ค ์๋ฒ์์๋ synchronized ๋ง์ผ๋ก๋ ๋์์ฑ ์ด์๋ฅผ ํด๊ฒฐํ ์ ์๊ธฐ ๋๋ฌธ์ ๋ค๋ฅธ ๋ฐฉ๋ฒ์ ์ฌ์ฉํด์ผ ํ๋๋ฐ, ๋ถ์ฐ๋ฝ์ ๊ตฌํํ๋ ๋ค์ํ ๊ธฐ๋ฒ๋ค์ ํตํด์ ์ด๋ฅผ ํด๊ฒฐํ ์ ์๋ค.
๐ฑ ๋๋ฉ์ธ ์ค๊ณ
์ค์๊ฐ ํธ๋ํฝ์ด ๋ชฐ๋ฆฌ๋ ํ๊ฒฝ์ ๋ํด์ ๊ณ ๋ฏผํด๋ดค๋๋ฐ, '์จ๋ผ์ธ ํฐ์ผํ ์๋น์ค'๊ฐ ๋ฑ ๋ ์ฌ๋๋ค.
๊ทธ๋์ ๊ฐ๋จํ๊ฒ ํน์ ์ฝ์ํธ์ ๋ํด์ ํฐ์ผ์ ๋ฐํํด์ฃผ๋ ์๋น์ค๋ฅผ ๋ง๋ค๊ธฐ๋ก ๊ฒฐ์ ํ๋ค.
๋๋ฉ์ธ์ด๋ผ๊ณ ํ ๊ฒ๋ ์์ง๋ง, ๋ถ์ฐ๋ฝ ํ ์คํธ๋ฅผ ์ํด์ ์ต๋ํ ๊ฐ๊ฒฐํ๊ฒ ์ค๊ณํ์๋ค.
์ฝ์ํธ์ ํฐ์ผ์ 1:N ๊ด๊ณ๋ฅผ ๊ฐ์ง๊ณ , ์ฝ์ํธ๋ ๋ช ์ข์๊น์ง ๊ฐ์ง ์ ์๋์ง์ ๋ํ ๊ฐ์๋ฅผ ๊ฐ์ง๊ณ ์๋๋ค. (ticketLimit)
@Entity
class Concert(
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long = 0L,
@Column(nullable = false)
val name: String,
@Column(nullable = false)
val ticketLimit: Int,
concertTickets: MutableList<ConcertTicket> = Collections.emptyList()
) {
@OneToMany(fetch = FetchType.LAZY, mappedBy = "concert")
val concertTickets: MutableList<ConcertTicket> = concertTickets.toMutableList()
fun isFull(): Boolean {
return concertTickets.size >= ticketLimit
}
}
์ฝ์ํธ์๋ ์ ์์ด ๊ฐ๋ ์ฐผ๋์ง ํ์ธํ๋ ์ปค์คํ ๋ฉ์๋๋ง ๊ฐ๋จํ๊ฒ ์ถ๊ฐํด์ฃผ์๋ค.
@Entity
class ConcertTicket(
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long = 0L,
@Column(nullable = false)
val userId: Long,
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(
name = "concert_id",
foreignKey = ForeignKey(name = "fk_ticket_concert_id")
)
val concert: Concert
)
์ด๊ฑฐ๋ ์ฝ์ํธ์ ๋ํ ํฐ์ผ ๋๋ฉ์ธ์ด๋ค.
CREATE TABLE `concert_ticket` (
`id` bigint NOT NULL AUTO_INCREMENT,
`user_id` bigint NOT NULL,
`concert_id` bigint NOT NULL,
PRIMARY KEY (`id`),
KEY `fk_ticket_concert_id` (`concert_id`),
CONSTRAINT `fk_ticket_concert_id` FOREIGN KEY (`concert_id`) REFERENCES `concert` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3
CREATE TABLE `concert` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`ticket_limit` int NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3
ํ ์ด๋ธ์ ์์ ๊ฐ์ด JPA๊ฐ ๋ง๋ค์ด ์ฃผ์๋ค.
๐ฑ ์ฝ๋ ์ค๊ณ
๐ฌ Repository
fun ConcertRepository.getConcertById(id: Long): Concert {
return findConcertById(id) ?: throw NotFoundException("์ฝ์ํธ ์ ๋ณด๊ฐ ์กด์ฌํ์ง ์์ต๋๋ค.")
}
interface ConcertRepository : JpaRepository<Concert, Long> {
fun findConcertById(id: Long): Concert?
}
fun ConcertTicketRepository.getConcertTicketById(id: Long): ConcertTicket {
return findConcertTicketById(id) ?: throw NotFoundException("์ฝ์ํธ ํฐ์ผ ์ ๋ณด๊ฐ ์กด์ฌํ์ง ์์ต๋๋ค.")
}
interface ConcertTicketRepository : JpaRepository<ConcertTicket, Long> {
fun findConcertTicketById(id: Long): ConcertTicket?
}
๐ฌ Service
@Service
@Transactional(readOnly = true)
class ConcertService(
private val concertRepository: ConcertRepository,
private val concertTicketRepository: ConcertTicketRepository
) {
...
@Transactional
fun createConcertTicket(
concertId: Long,
concertTicketCreateRequest: ConcertTicketCreateRequest
): Long {
val concert = concertRepository.getConcertById(concertId)
if (concert.isFull()) {
throw ConcertFullException("์ ์์ด ๊ฐ๋ ์ฐผ์ต๋๋ค.")
}
val concertTicket =
ConcertTicket(userId = concertTicketCreateRequest.userId, concert = concert)
val savedConcertTicket = concertTicketRepository.save(concertTicket)
return savedConcertTicket.id
}
...
}
์ฌ๊ธฐ์ ์ค์ํ ๋ฉ์๋๋ ํฐ์ผ์ ๋ํ ์์ฑ ๋ฉ์๋์ด๊ธฐ ๋๋ฌธ์, ์ด ๋ถ๋ถ๋ง ์ฃผ๋ชฉํ์.
์ฝ์ํธ ํฐ์ผ ์์ฑ ์ ์ ์์ด ๊ฐ๋ ์ฐจ๋ฉด ์์ธ๊ฐ ๋ฐ์ํ๋๋ก ๋ง๋ค์๋ค. ๋ง์ฝ ์์ฒญ์ด ์ฌ๋ฌ ๊ฐ๊ฐ ์ค๋ค๊ฐ ์ ์์ด ๊ฐ๋์ฐจ๋ฉด ๋ ์ด์ insert๋ ์งํ๋์ง ์์ ๊ฒ์ด๋ค.
๐ฌ Controller
@RestController
@RequestMapping("/concerts")
class ConcertController(
private val concertService: ConcertService
) {
...
@PostMapping("/tickets/{concertId}")
fun createConcertTicket(
@PathVariable("concertId") concertId: Long,
@RequestBody concertTicketCreateRequest: ConcertTicketCreateRequest
) {
concertService.createConcertTicket(concertId, concertTicketCreateRequest)
}
}
ํฐ์ผ์ ์์ฑํ๋ API์ด๋ค. ์ฝ์ํธ๋ ํฐ์ผ ์กฐํ ๊ฐ์ ๊ฐ๋จํ API๋ ์ ์ฒด ์์ค์ฝ๋์ ๋จ๊ฒจ๋์๋ค.
๐ฑ JMeter ํ ์คํธ
๋ถํ ํ ์คํธ๋ฅผ ์ด๋ป๊ฒ ์งํํ ๊น ๊ณ ๋ฏผํ๋๋ฐ, JMeter๋ฅผ ํ์ฉํ๋ฉด ์ข์ ๊ฒ ๊ฐ์์ ์งํํด๋ณด์๋ค.
java -jar -Dserver.port=8080 ticketService.jar
java -jar -Dserver.port=8081 ticketService.jar
๋จผ์ , ๋ถ์ฐ๋ ํ๊ฒฝ์ ๋ง๋ค๊ธฐ ์ํด 8080, 8081 2๊ฐ์ ํฌํธ๋ฅผ ๋์์ ์งํํ์๋ค.
์ํ ๋ฐ์ดํฐ๋ก ์ฝ์ํธ 1๋ฒ์ ๋ํ ๋ฐ์ดํฐ๋ฅผ ๋ฏธ๋ฆฌ ์ฝ์ ํด๋์๋ค.
ํด๋น ์ฝ์ํธ์ ์ ํ ์ธ์์ 100๋ช ์ผ๋ก, ์์ ์๋๋ฆฌ์ค๋ผ๋ฉด 100๋ช ์ด์์ด ์๋ฆฌ์ก์์ ๋ ์ค๋ฅ๊ฐ ๋ฐ์ํ ๊ฒ์ด๋ค.
200๊ฐ์ ์ค๋ ๋๋ฅผ ํ์ฉํ์ฌ 2๊ฐ์ ํ๋ก์ธ์ค์์ ๋์์ ์ฝ์ํธ ํฐ์ผ ๋ฐํ ์์ฒญ์ ๋ ๋ ธ๋ค.
select count(*) from concert_ticket;
๊ทธ ๊ฒฐ๊ณผ, 100๋ช ๊น์ง๋ง ๋ฐ์์ผ ํ๋๋ฐ๋ ๋ถ๊ตฌํ๊ณ 111๊ฐ์ ๊ฐ์ด ๋ค์ด๊ฐ ๊ฒ์ ํ์ธํ ์ ์๋ค.
์ ์ด๋ฐ ๋ฌธ์ ๊ฐ ๋ฐ์ํ ๊ฒ์ผ๊น?
์ฌ๋ฌ ๊ฐ์ ์ค๋ ๋๊ฐ ์กฐํํ๋ ์์ ์์๋ ๋ถ๋ช ๊ฐ๋์ฐจ์์ง ์์์ง๋ง, ๊ฒ์ฆ์ ํต๊ณผํด๋ฒ๋ฆฐ ์ค๋ ๋๊ฐ ๋์์ ์ฌ๋ฌ ๊ฐ๊ฐ ๋ฐ์ํ๋ฉด์ insert ์ฐ์ฐ ์ญ์ ํ ๋ฒ์ ์งํ๋์ด 100๊ฐ๊ฐ ๋๋ ๋ฐ์ดํฐ๊ฐ ์ฝ์ ๋ ๊ฒ์ด๋ค.
๐ฑ Named Lock์ผ๋ก ๋ถ์ฐ๋ฝ ๊ตฌํํ๊ธฐ
๊ทธ๋ ๋ค๋ฉด, ์์ ์ํฉ์ ํด๊ฒฐํ๊ธฐ ์ํด์๋ ์ด๋ป๊ฒ ํด์ผ ํ ๊น?
๊ฐ๋จํ๋ค. ํฐ์ผ์ ์ ์ฅํ๋ ๋ก์ง์ ๋ํด์ ์์ฐจ์ ์ผ๋ก ์ฒ๋ฆฌํ ์ ์๋๋ก ๋ง๋ค๋ฉด ๋๋ ๊ฒ์ด๋ค.
fun LockRepository.executeWithLock(
lockName: String, timeout: String, action: () -> Unit) {
try {
getLock(lockName, timeout)
action()
} finally {
releaseLock(lockName)
}
}
interface LockRepository: JpaRepository<ConcertTicket, Long> {
@Query("select get_lock(:name, :time)", nativeQuery = true)
fun getLock(@Param(value = "name") name: String,
@Param(value = "time") time: String)
@Query("select release_lock(:name)", nativeQuery = true)
fun releaseLock(@Param(value = "name") name: String)
}
๋ค์๋๋ฝ์ ์ ์ํด์ฃผ๊ธฐ ์ํด์ ์์ ๊ฐ์ด ๋ฝ์ ํ๋ํ๋ getLock()๊ณผ ๋ฝ์ ํด์ ํ๋ releaseLock() ๋ฉ์๋๋ฅผ ์ ์ธํ์๋ค.
๋ ๊ฐ์ง ๋ชจ๋ jpql์ด ์๋ native query๋ฅผ ์ฌ์ฉํด์ผ ํ๊ธฐ ๋๋ฌธ์, nativeQuery ์ต์ ์ true๋ก ์ ์ธํด์ฃผ์๋ค.
getLock์ ๊ฒฝ์ฐ ํ๋ผ๋ฏธํฐ๋ก ์ ๋ ฅ๋ฐ์ name์ ๋ํด์, ์ ๋ ฅ๋ฐ์ time ๋งํผ ๋ฝ์ ํ๋ํ๊ธฐ๋ฅผ ์๋ํ๋ค.
๋ง์ฝ time ๊ฐ์ผ๋ก ์์๋ฅผ ๋ฃ์ผ๋ฉด ๋ฝ์ ํ๋ํ ๋๊น์ง ๋ฌดํ ๋๊ธฐํ๊ธฐ ๋๋ฌธ์ ์ฃผ์ํด์ผ ํ๋ค.
๋๋ MySQL 8.0์ ์ฌ์ฉํ๊ณ ์์ด์ name์ผ๋ก 60์ ์ด๋ด๋ฅผ ๋ฃ์ด์ผ ํ๋ค.
releaseLock์ ๊ฒฝ์ฐ ์ ๋ ฅ๋ฐ์ name์ ๋ํด์ ํ๋ํ ๋ฝ์ ํด์ ํ๋ ์ญํ ์ ํ๋ค.
โญ๏ธ ๋ช ์์ ์ผ๋ก ๋ฝ์ ํ๋ํ๋ค๋ฉด ๋ช ์์ ์ผ๋ก ๋ฝ์ ๊ผญ ํด์ ํด์ค์ผ ํ๊ธฐ ๋๋ฌธ์ ์์๋์.
๋ํ, ๋๋ค๋ฅผ ํตํด์ ์ํ๋ ๋ฉ์๋ ์ ํ๋ก ๋ฝ์ ๊ฑธ๊ณ ํด์ ํ๋๋ก ๋ง๋ค๊ธฐ ์ํด executeWithLock() ๋ฉ์๋๋ฅผ ๋ง๋ค์ด ์ฃผ์๋ค.
๋ฝ ํด์ ์ ๊ฒฝ์ฐ finally๋ฅผ ํตํด ์์ธ๊ฐ ๋ฐ์ํ๋๋ผ๋ ๊ผญ ์คํ๋๋๋ก ๋ง๋ค์๋ค.
@Component
class ConcertServiceFacade(
private val lockRepository: LockRepository,
private val concertService: ConcertService
){
@Transactional
fun createConcertTicket(
concertId: Long,
concertTicketCreateRequest: ConcertTicketCreateRequest
) {
val lockName = "concert_$concertId"
val timeout = "3000"
lockRepository.executeWithLock(lockName, timeout) {
concertService.createConcertTicket(concertId, concertTicketCreateRequest)
}
}
}
๊ทธ๋ฆฌ๊ณ , facade ํจํด์ ์ฌ์ฉํ์ฌ lockRepository์ ๋ํ ๋ถ๋ถ์ ๋ถ๋ฆฌํ์๋ค.
๋น์ฆ๋์ค ๋ก์ง๊ณผ DB ๊ด๋ จ ๋ก์ง์ ๋ถ๋ฆฌํ๋ ์ฐจ์์์ ๋ณ๋์ ์๋น์ค๋ฅผ ์์ฑํ๋ค๊ณ ์๊ฐํ๋ฉด ๋๋ค.
lockName์ผ๋ก๋ ์ค๋ณต๋์ง ์๊ฒ ํ๊ฒ ์ํด์ concert_๋ผ๋ prefix์ concertId๋ฅผ ์์ฑํด์ฃผ์๊ณ , ํ์์์์ 3์ด ์ ๋ ์ฃผ์๋ค.
@Transactional(propagation = Propagation.REQUIRES_NEW)
fun createConcertTicket(
concertId: Long,
concertTicketCreateRequest: ConcertTicketCreateRequest
) {
...
}
๊ทธ๋ฆฌ๊ณ , โญ๏ธ ๊ธฐ์กด์ ์ฝ์ํธ ํฐ์ผ ๋ฐํ ๋ก์ง์์ ํธ๋์ญ์ ์ ํ ์์ฑ์ ๋ณ๊ฒฝํด์ค์ผ ํ๋ค.
์ด๋, ๋ฝ์ ์ ์ดํ๋ ์ปค๋ฅ์ ๊ณผ ๋น์ฆ๋์ค ๋ก์ง์ ์ํ ์ปค๋ฅ์ ์ ๋ถ๋ฆฌํ๊ธฐ ์ํด์์ด๋ค.
๋ง์ฝ ๋ฝ์ ์ ์ดํ๋ ๋ก์ง๊ณผ ๋น์ฆ๋์ค ๋ก์ง์ด ๋์ผํ ํธ๋์ญ์ ์ผ๋ก ๋ฌถ์ฌ ์๋ค๋ฉด, ๋น์ฆ๋์ค ๋ก์ง์์ ์ฟผ๋ฆฌ๊ฐ ๋ ๋ผ๊ฐ๊ณ ์ปค๋ฐ์ด ๋์์ ๋ ์ปค๋ฅ์ ์ด ๋ฐํ๋ ๋ ๋ฝ์ด ํจ๊ป ๋ฐํ๋์ง ์์ ์ ์๋ค. ๋ํ, ๋น์ฆ๋์ค ๋ก์ง์ด ์ํํ๋ ์ญํ ์ด ๋ง๋ค๋ฉด (์ ์ํฉ์ ์๋์ง๋ง, ์ธ๋ถ ํธ์ถ์ ์งํํ๊ฑฐ๋, ๋ก์ง ์์ฒด๊ฐ ๊ธธ๊ฑฐ๋) ํธ๋์ญ์ ์ด ๊ธธ์ด์ง๊ธฐ ๋๋ฌธ์ ๋ฝ์ ๋ํ ์ ์ด๊ณผ ๋น์ฆ๋์ค ๋ก์ง์ ํธ๋์ญ์ ์ ๋ถ๋ฆฌํ๋ ๊ฒ์ด ์ข๋ค.
@PostMapping("/tickets-lock/{concertId}")
fun createConcertTicketWithLock(
@PathVariable("concertId") concertId: Long,
@RequestBody concertTicketCreateRequest: ConcertTicketCreateRequest
) {
concertServiceFacade.createConcertTicket(concertId, concertTicketCreateRequest)
}
์ด์ , ๋ง๋ facade ํด๋์ค๋ฅผ ํธ์ถํ๋ ๊ฐ๋จํ end-point๋ฅผ ์ถ๊ฐํ์ฌ ํธ์ถํด๋ณด์.
์คํ์ ํด๋ณด๋ฉด ์ด๋ฐ ์์ผ๋ก ๋ฝ์ ์ป๊ธฐ ์ํด์ ์์ฒญ๋ ํธ์ถ์ด ์ผ์ด๋๋ค.
2023-06-23T16:57:19.359+09:00 ERROR 81614 --- [o-8080-exec-289] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.transaction.CannotCreateTransactionException: Could not open JPA EntityManager for transaction] with root cause java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available, request timed out after 30005ms. at com.zaxxer.hikari.pool.HikariPool.createTimeoutException(HikariPool.java:696) ~[HikariCP-5.0.1.jar!/:na] at com.zaxxer.hikari.pool.HikariPool.getConnection(HikariPool.java:181) ~[HikariCP-5.0.1.jar!/:na] at com.zaxxer.hikari.pool.HikariPool.getConnection(HikariPool.java:146) ~[HikariCP-5.0.1.jar!/:na] at com.zaxxer.hikari.HikariDataSource.getConnection(HikariDataSource.java:128) ~[HikariCP-5.0.1.jar!/:na]
๋ง์ฝ ํ์์์์ผ๋ก ์ค์ ํ ์๊ฐ ๋ด์ ๋ฝ์ ์ป์ง ๋ชปํ๋ฉด ์์ ๊ฐ์ด ๋ฝ์ ์ป์ง ๋ชปํ๋ค๋ ์ค๋ฅ๊ฐ ๋ฐ์ํ๋ค.
ํ์ง๋ง, ์ฑ๊ณต์ ์ผ๋ก ๋ฝ์ ์ป์๋ค๋ฉด insert ์ฟผ๋ฆฌ ์ดํ ๋ฝ์ ํด์ ํ๊ฒ ๋๋ค.
์ฆ, ์์ฐจ์ ์ผ๋ก ๋ฝ์ ์ก๊ณ , ๋ฝ์ด ์กํ์๋ ๋์์๋ ๋ ๋ค์ ๋์ผํ ์ด๋ฆ์ ๋ฝ์ ๊ฑธ ์ ์๊ธฐ ๋๋ฌธ์ ํด๋น ๋ฝ์ด ํ๋ฆด ๋๊น์ง ๋๊ธฐํ๊ฒ ๋๋ค.
์ต์ข ์ ์ผ๋ก ์คํ ๊ฒฐ๊ณผ๋ฅผ ๋ณด๋ฉด ์์ ๊ฐ์ด ๋ฑ 100๊ฐ์ ์ ์๋ง ์ฐจ๊ฒ ๋๋ค.
๐ฑ Connection Pool Size ์กฐ์ ํ๊ธฐ
spring:
datasource:
hikari:
maximum-pool-size: 100
์ค๋ ๋๋ฅผ 200๊ฐ๋ก ํ๊ธฐ ๋๋ฌธ์ connection pool size๋ฅผ ์กฐ๊ธ ๋ ๋๋ ค์ ํ ์คํธ๋ฅผ ํด๋ด๋ ๊ด์ฐฎ์ง ์์๊น ์๊ฐ์ด ๋ค์๋ค.
๊ทธ๋์ connection pool size๋ฅผ 100์ผ๋ก ์ค์ ํ๊ณ ํ ์คํธ๋ฅผ ์งํํด๋ณด์๋ค. (default๋ 10์ด๋ค)
Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.transaction.CannotCreateTransactionException: Could not open JPA EntityManager for transaction] with root cause java.sql.SQLNonTransientConnectionException: Data source rejected establishment of connection, message from server: "Too many connections" at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:111) ~[mysql-connector-j-8.0.33.jar!/:8.0.33]
์ด์ ์ ๋นํด์ ํฐ์ผ ์ฝ์ ์ ๋ํ ์๋๋ ๋นจ๋ผ์ก์ง๋ง, ์์ ๊ฐ์ด 'Too many connections' ์ค๋ฅ๊ฐ ๋ฐ์ํ์๋ค.
show variables like 'max_connections';
ํ์ธํด๋ณด๋, ๊ธฐ๋ณธ์ ์ผ๋ก mysql์์ ์ ๊ณตํ๋ ์ต๋ ์ปค๋ฅ์ ์๋ 151๊ฐ์ด๋ค.
2๋์ ์๋ฒ์์ 100๊ฐ์ฉ ์ ์ ํ๊ณ , ์ด 200๊ฐ์ ์์ฒญ์ด ์ค๋ค ๋ณด๋ ์์ ๊ฐ์ด ์ค๋ฅ๊ฐ ๋ฐ์ํ ๊ฒ์ผ๋ก ํ์ ๋์๋ค.
์ด๋ฐ ์ํฉ์์๋ mysql ์๋ฒ์ max_connections ์๋ฅผ ์ถฉ๋ถํ ๋๋ฆฌ๊ฑฐ๋, ํน์ hikari pool size๋ฅผ ์ข ๋ ์ค์ด๋ ๋ฐฉํฅ์ด ์๋ค. ๋๋ hikari pool size๋ฅผ ์ค์ด๋ ๋ฐฉํฅ์ผ๋ก ์งํํ๋ค. 75๋ก ์งํํ๋ค๋ฉด ๋ฑ 151๊ฐ๋๊น ์ค๋ฅ๊ฐ ๋ฐ์ํ์ง ์์ ๊ฒ์ด๋ผ๊ณ ์์ธกํ๋ค. (1๊ฐ๋ ์๋ฌด ํด๋ผ์ด์ธํธ๊ฐ ์ฐ๊ฒฐ๋์ด ์์ง ์์๋ ๊ธฐ๋ณธ์ ์ผ๋ก ์ ์ ํ๊ณ ์๋ค. ์๋ง MySQL ๋ด๋ถ์์ ์ฌ์ฉํ๋ ์ค๋ ๋๊ฐ ์๋๊น ์ถ์ธกํ๋ค.)
show status where `variable_name` = 'Threads_connected';
์ค์ ๋ก 2๋์ ์๋ฒ๋ฅผ ๋์ด ๋ค์, ์ฐ๊ฒฐ๋ ์ค๋ ๋์ ์๋ฅผ ๋ณด๋ฉด ์ ํํ๊ฒ 151๊ฐ๋ฅผ ์ ์ ํ ๊ฒ์ ๋ณผ ์ ์๋ค.
ํฐ์ผ ์์ฑ ๊ฒฐ๊ณผ๋ฅผ ๋ณด๋๊น ์ ํํ๊ฒ 100๊ฐ๋งํผ ์ฐจ์๋ ๊ฒ์ ํ์ธํ ์ ์๋ค.
๋ํ, grep์ ํตํด too many connections ์ค๋ฅ๊ฐ ๋ฐ์ํ๋์ง ํ์ธํ๋๋ฐ, 100๊ฐ๊ฐ ์ฐจ๋ ๋์ ๋ฐ์ํ์ง ์์๋ค.
์ด๋ฒ ํฌ์คํ ์์๋ ๊ฐ๋จํ๊ฒ named lock์ ํตํด์ ๋ถ์ฐ๋ฝ์ ๊ตฌํํด๋ณด์๋ค.
๋จ์ผ ์๋ฒ๊ฐ ์๋ ๋ค์ค ์๋ฒ์์ ๋์์ฑ์ด ์ค์ํ ๊ธฐ๋ฅ์ ๊ตฌ์ถํด์ผ ํ ๋ redis ๊ฐ์ ์ธ๋ถ ์ธํ๋ผ ์๋น์ค ๊ตฌ์ถ ๋น์ฉ์ด ๋ถ๋ด๋๋ค๋ฉด ์ด๋ ๊ฒ ๋ค์๋๋ฝ์ ํ์ฉํด์๋ ์ถฉ๋ถํ ์งํํ ์ ์๋ค๋ ์๊ฐ์ด ๋ค์๋ค. ๋ค๋ง, ๋ค์๋๋ฝ์ ๋ํ ์ดํด๋ ์์ด์ผ ํ๊ธฐ ๋๋ฌธ์ ๋ฝ์ ๋ํ ๋ชจ๋ํฐ๋ง์ ์ฒ ์ ํ๊ฒ ํด์ผ๊ฒ ๋ค๋ ์๊ฐ์ด ๋ค์๋ค. ํนํ, ํ์์์์ ์ ์ ํ๊ฒ ์ค์ ํด์ ๊ผญ ์ ์ ํ ๋ฝ์ ๋ํด์ ํด์ ํ ์ ์๋๋ก ํ๋ ๊ฒ ์ค์ํ๋ค๋ ๊ฑธ ๋๊ผ๋ค. ๋ํ, ์ง๊ธ์ ์์ฒญ ์์ ๊ท๋ชจ์ ํ ์คํธ์์ง๋ง, ์ค์ ์ด์์์ thread ์์ connection ์๋ฅผ ์ ์ค์ ํด์ ์ฌ์ฉ์์ ์์ฒญ์ ์ฒ๋ฆฌํด์ผ๊ฒ ๋ค๊ณ ์๊ฐํ๋ค. ๋ค์ ํฌ์คํ ์์๋ redis๋ฅผ ํ์ฉํด์ ๋ถ์ฐ๋ฝ์ ๊ตฌํํด๋ด์ผ๊ฒ ๋ค.
๐ฑ REFERENCE