๋๋ถ๋ถ์ ์๋น์ค์์๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ Master, Slave ๊ตฌ์กฐ๋ก Master์์๋ Create, Update, Delete ์ ๋ฌด๋ฅผ ์งํํ๊ณ Slave์์ Read ์ ๋ฌด๋ฅผ ์งํํ๋ ๊ตฌ์กฐ๋ก ์ค๊ณํฉ๋๋ค. Spring์ Master, Slave ํ๊ฒฝ์์์ ํธ๋์ญ์ ์ ๋ํด์ ํฌ์คํ ํด๋ณด๊ฒ ์ต๋๋ค.
MySQL์ ์์ ๊ฐ์ ๊ตฌ์กฐ๋ก ๋ง์คํฐ - ์ฌ๋ ์ด๋ธ ๊ตฌ์กฐ๋ฅผ ์ง์ํฉ๋๋ค. ๊ฐ๋ตํ๊ฒ ์ค๋ช ํ๋ฉด ๋ค์๊ณผ ๊ฐ์ด ๊ตฌ์ฑ๋์ด ์์ต๋๋ค.
- ๋ง์คํฐ์ ๋ณ๊ฒฝ์ ๊ธฐ๋กํ๊ธฐ ์ํ ๋ฐ์ด๋๋ฆฌ ๋ก๊ทธ
- ์ฌ๋ ์ด๋ธ์ ๋ฐ์ดํฐ๋ฅผ ์ ์กํ๊ธฐ ์ํ ๋ง์คํฐ ์ค๋ ๋
- ์ฌ๋ ์ด๋ธ์์ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์ ๋ฆด๋ ์ด ๋ก๊ทธ์ ๊ธฐ๋กํ๊ธฐ ์ํ I/O ์ค๋ ๋
- ๋ฆด๋ ์ด ๋ก๊ทธ์์ ๋ฐ์ดํฐ๋ฅผ ์ฝ์ด ์ฌ์ํ๊ธฐ ์ํ ์ฌ๋ ์ด๋ธ SQL ์ค๋ ๋
MySQL 5.7๋ถํฐ ACK๋ฅผ ๊ธฐ๋ค๋ฆฌ๋ ์์ ์ ๋ณ๊ฒฝ์ด ์๊ฒผ์ต๋๋ค. ๊ธฐ์กด COMMIT์ ์คํํ ๋ค์์ด ์๋๋ผ COMMIT์ ์คํํ๊ธฐ ์ ์ ACK๋ฅผ ๊ธฐ๋ค๋ฆฌ๋๋ก ๋ณ๊ฒฝ๋์์ต๋๋ค. ์ด๋ก ์ธํด ๋ง์คํฐ์์ COMMIT์ด ์๋ฃ๋ ํธ๋์ญ์ ์ ๋ชจ๋ ์ฌ๋ ์ด๋ธ์ ํ์คํ ์ ๋ฌ๋๊ฒ ๋์ด์ ๋ฌด์์ค ๋ ํ๋ฆฌ์ผ์ด์ ์ ๋ณด๋ค ์ ์ง์ํ๊ฒ ๋์์ต๋๋ค. ์์ธํ ๋ด์ฉ์ MySQL 5.7 ์๋ฒฝ ๋ถ์์ ์ ์ค๋ช ๋์ด ์์ต๋๋ค.
ํธ๋์ญ์
์์ readOnly
์ค์ ์ ๊ธฐ์ค์ผ๋ก false
๊ฒฝ์ฐ Master DataSource, true
๊ฒฝ์ฐ Slave DataSource๋ฅผ ๋ฐ๋ผ๋ณด๊ฒ ์ค์ ํ ์ ์์ต๋๋ค. Master, Slave์ DataSource์ ์ค์ ์ Spring Boot์์์ ์์ ์ ์ํ๋ฅผ ์ต๋ํ ์ด์ฉํ๋ ค ํ์ต๋๋ค. ๋ณธ ํฌ์คํ
์ DataSource ์ฝ๋ ๊ตฌ์ฑ์ ๋ค๋ฃจ๋ ์๋๊ธฐ ๋๋ฌธ์ ํด๋น ๋ด์ฉ์ ๋ค๋ฅธ ์๋ฃ๋ฅผ ๋ณด์๋ ๊ฒ์ ๊ถ์ฅ๋๋ฆฝ๋๋ค.
# application.yml
spring:
datasource:
initialization-mode: never
master:
hikari:
jdbc-url: jdbc:mysql://localhost:3306/study?useSSL=false&serverTimezone=UTC&autoReconnect=true&rewriteBatchedStatements=true&logger=Slf4JLogger&profileSQL=false&maxQuerySizeToLog=100000
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
slave:
hikari:
jdbc-url: jdbc:mysql://localhost:3307/study?useSSL=false&serverTimezone=UTC&autoReconnect=true&rewriteBatchedStatements=true&logger=Slf4JLogger&profileSQL=false&maxQuerySizeToLog=100000
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
const val PROPERTIES = "spring.datasource.hikari"
const val MASTER_DATASOURCE = "masterDataSource"
const val SLAVE_DATASOURCE = "slaveDataSource"
@Configuration
class DataSourceConfiguration {
@Bean(name = [MASTER_DATASOURCE])
@ConfigurationProperties(prefix = "spring.datasource.master.hikari")
fun masterDataSource() =
DataSourceBuilder.create()
.type(HikariDataSource::class.java)
.build()
@Bean(name = [SLAVE_DATASOURCE])
@ConfigurationProperties("spring.datasource.slave.hikari")
fun slaveDataSource() =
DataSourceBuilder.create()
.type(HikariDataSource::class.java)
.build()
.apply { this.isReadOnly = true }
@Bean
@DependsOn(MASTER_DATASOURCE, SLAVE_DATASOURCE)
fun routingDataSource(
@Qualifier(MASTER_DATASOURCE) masterDataSource: DataSource,
@Qualifier(SLAVE_DATASOURCE) slaveDataSource: DataSource
): DataSource {
val routingDataSource = RoutingDataSource()
val dataSources = hashMapOf<Any, Any>()
dataSources["master"] = masterDataSource
dataSources["slave"] = slaveDataSource
routingDataSource.setTargetDataSources(dataSources)
routingDataSource.setDefaultTargetDataSource(masterDataSource)
return routingDataSource
}
@Primary
@Bean
@DependsOn("routingDataSource")
fun dataSource(routingDataSource: DataSource) =
LazyConnectionDataSourceProxy(routingDataSource)
}
class RoutingDataSource : AbstractRoutingDataSource() {
override fun determineCurrentLookupKey(): Any =
when {
TransactionSynchronizationManager.isCurrentTransactionReadOnly() -> "slave"
else -> "master"
}
}
masterDataSource
, routingDataSource
DataSource์ Bean์ ์ค์ ํฉ๋๋ค. ๊ฐ๊ฐ 3306(Master), 3307(Slave) ํฌํธ๋ฅผ ์ฌ์ฉํ๋ฉฐ ํด๋น ๋๋น๋ Master, Slave ์ค์ ๊น์ง ์๋ฃ๋์ด ์์ต๋๋ค.
routingDataSource
์์๋ masterDataSource
, routingDataSource
๋ฐ์ดํฐ ์์ค๋ฅผ master
, slave
๋ฅผ HashMap์ ์ ์ฅํฉ๋๋ค. determineCurrentLookupKey
๋ฉ์๋์์ readOnly
์ค์ ์ฌ๋ถ์ ๋ฐ๋ผ master
, slave
์ DataSource
๋ฅผ ์ ํํ๊ฒ ๋ฉ๋๋ค.
๋ง์ง๋ง์ผ๋ก ๊ธฐ๋ณธ dataSource
Bean์ ์์ฑํฉ๋๋ค. ์ด๋ routingDataSource
Bean์ ์ด์ฉํด์ LazyConnectionDataSourceProxy
๋ฅผ ์์ฑํฉ๋๋ค.
@RestController
@RequestMapping("/api/book")
class BookApi(
private val bookRepository: BookRepository
) {
@GetMapping("/slave")
@Transactional(readOnly = true)
fun getSlave() = bookRepository.findAll()
@GetMapping("/master")
@Transactional(readOnly = false)
fun getMaster() = bookRepository.findAll()
}
/api/book/slave
๋ readOnly = true
์ค์ ์ผ๋ก Slave๋ฅผ ๋ฐ๋ผ๋ณด๊ฒ ํ๊ณ , ๊ทธ์ ๋ฐ๋๋ก /api/book/master
๋ readOnly = false
์ค์ ์ผ๋ก Master๋ฅผ ๋ฐ๋ผ๋ณด๊ฒ ์ค์ ํ๊ณ API ํธ์ถ ์ดํ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ก๊ทธ๋ฅผ ํ์ธํด๋ณด๊ฒ ์ต๋๋ค.
readOnly
์ฌ๋ถ์ ๋ฐ๋ผ์ DataSource๊ฐ ์ ์ ํ๊ฒ ๋ผ์ฐํ
๋๋ ๊ฒ์ ํ์ธํ ์ ์์ต๋๋ค.
@Component
class AppSetup(
private val bookRepository: BookRepository
) : ApplicationRunner {
override fun run(args: ApplicationArguments) {
bookRepository.saveAll(
(1..5).map { Book(title = "INIT") }
.toList()
)
}
}
Application์ด ์คํ๋ ๋ title์ด INIT
์ผ๋ก ๋ฐ์ดํฐ๋ฅผ 5๊ฐ๋ฅผ ์์ฑํ๋ ์ฝ๋๋ฅผ ์ถ๊ฐํ์ต๋๋ค. ๊ฐ ๋ฉ์๋๋ง๋ค readOnly
์ค์ ์ ๋ค๋ฃจ๊ฒ ๋๊ณ ์
๋ฐ์ดํธ ์์
์ ์งํํ๊ฒ ๋ฉ๋๋ค.
@RestController
@RequestMapping("/api/book")
class BookApi(
private val bookRepository: BookRepository,
private val bookService: BookService
) {
@GetMapping("/update/slave")
fun startSlave() {
bookService.updateSlave()
}
}
@Service
class BookService(
private val bookRepository: BookRepository
) {
@Transactional(readOnly = true)
fun updateSlave() {
updateTitle("new title(slave)")
}
@Transactional(readOnly = false)
fun updateMaster() {
updateTitle("new title(master)")
}
@Transactional(readOnly = false)
fun updateTitle(title: String) {
val books = bookRepository.findAll()
for (book in books) {
book.title = title
}
bookRepository.saveAll(books)
}
}
updateSlave()
๋ฉ์๋๋readOnly = true
์์ํ๊ณ ,updateTitle()
๋ฉ์๋์์readOnly = false
๋ก ์งํupdateMaster()
๋ฉ์๋๋readOnly = false
์์ํ๊ณ ,updateTitle()
๋ฉ์๋์์readOnly = false
๋ก ์งํ
select query๊ฐ slave์์ ์งํ๋ ๊ฒ์ ํ์ธํ ์ ์์ต๋๋ค. ์กฐํ๋ readOnly = true
์ค์ ์ด์ง๋ง title๋ฅผ ์
๋ฐ์ดํธํ ๋๋ readOnly = false
์ด๊ธฐ ๋๋ฌธ์ ์์์ฑ ์ปจํ
์คํธ๊ฐ flush๋ฅผ ์งํํ๋ฉด ํด๋น ๋ด์ฉ์ด ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ๋ฐ์๋ ๊น์? ํ์ธํด ๋ณด๊ฒ ์ต๋๋ค.
GET http://localhost:8080/api/book/master
HTTP/1.1 200
Content-Type: application/json
Transfer-Encoding: chunked
Date: Thu, 06 May 2021 13:53:28 GMT
Keep-Alive: timeout=60
Connection: keep-alive
[
{
"title": "INIT",
"id": 1,
"createdAt": "2021-05-06T22:42:33",
"updatedAt": "2021-05-06T22:42:33"
},
{
"title": "INIT",
"id": 2,
"createdAt": "2021-05-06T22:42:33",
"updatedAt": "2021-05-06T22:42:33"
},
{
"title": "INIT",
"id": 3,
"createdAt": "2021-05-06T22:42:33",
"updatedAt": "2021-05-06T22:42:33"
},
{
"title": "INIT",
"id": 4,
"createdAt": "2021-05-06T22:42:33",
"updatedAt": "2021-05-06T22:42:33"
},
{
"title": "INIT",
"id": 5,
"createdAt": "2021-05-06T22:42:33",
"updatedAt": "2021-05-06T22:42:33"
}
]
Response code: 200; Time: 31ms; Content length: 461 bytes
๊ฒฐ๊ณผ๋ ๋ณ๊ฒฝ๋์ง ์์์ต๋๋ค.
select, update query๊ฐ master์์ ์งํ๋ ๊ฒ์ ํ์ธํ ์ ์์ต๋๋ค. ๋ง์ฐฌ๊ฐ์ง๋ก API๋ฅผ ํธ์ถํด์ ๊ฒฐ๊ณผ๋ฅผ ํ์ธํด ๋ณด๊ฒ ์ต๋๋ค.
GET http://localhost:8080/api/book/master
HTTP/1.1 200
Content-Type: application/json
Transfer-Encoding: chunked
Date: Thu, 06 May 2021 14:01:40 GMT
Keep-Alive: timeout=60
Connection: keep-alive
[
{
"title": "new title(master)",
"id": 1,
"createdAt": "2021-05-06T22:42:33",
"updatedAt": "2021-05-06T22:59:06"
},
{
"title": "new title(master)",
"id": 2,
"createdAt": "2021-05-06T22:42:33",
"updatedAt": "2021-05-06T22:59:06"
},
{
"title": "new title(master)",
"id": 3,
"createdAt": "2021-05-06T22:42:33",
"updatedAt": "2021-05-06T22:59:06"
},
{
"title": "new title(master)",
"id": 4,
"createdAt": "2021-05-06T22:42:33",
"updatedAt": "2021-05-06T22:59:06"
},
{
"title": "new title(master)",
"id": 5,
"createdAt": "2021-05-06T22:42:33",
"updatedAt": "2021-05-06T22:59:06"
}
]
Response code: 200; Time: 51ms; Content length: 526 bytes
์ ์์ ์ผ๋ก title์ด ๋ณ๊ฒฝ๋ ๊ฒ์ ํ์ธํ ์ ์์ต๋๋ค.
@Service
class BookService(
private val bookRepository: BookRepository
) {
@Transactional(readOnly = true)
fun updateSlave() {
println("updateSlave CurrentTransactionName: ${TransactionSynchronizationManager.getCurrentTransactionName()}")
bookUpdateService.updateTitle("new title(slave)")
}
}
@Service
class BookUpdateService(
private val bookRepository: BookRepository
){
@Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW)
fun updateTitle(title: String) {
println("updateTitle CurrentTransactionName: ${TransactionSynchronizationManager.getCurrentTransactionName()}")
val books = bookRepository.findAll()
for (book in books) {
book.title = title
}
bookRepository.saveAll(books)
}
}
BookUpdateService ํด๋์ค๋ฅผ ๋ง๋ค๊ณ updateTitle
๋ฉ์๋๋ฅผ ํด๋น ํด๋์ค์์ propagation = Propagation.REQUIRES_NEW
์ค์ ์ ํตํด์ ์๋ก์ด ํธ๋์ญ์
์์ ์ฒ๋ฆฌํ๋๋ก ์์ฑํ๊ณ /api/book/update/slave
๋ฅผ ํธ์ถํ๊ณ ์กฐํ API๋ฅผ ํธ์ถํด์ ๊ฒฐ๊ณผ๋ฅผ ํ์ธํด ๋ณด๊ฒ ์ต๋๋ค.
๊ธฐ์กด ์๋น์ค ์ฝ๋์์ ๊ตฌํ์ ํ์ง ์๊ณ BookUpdateService ์๋น์ค ์ฝ๋๋ฅผ ๋ง๋ ์ด์ ๋ ๋์ผํ Bean์์ @Transactional
์์ํ๋ ๊ฒ๊ณผ ๋ค๋ฅด๊ฒ ๋์ํ๊ธฐ ๋๋ฌธ์
๋๋ค. ์์ธํ ๋ด์ฉ์ ์ด์ ๋์ผํ Bean(Class)์์ @Transactional ๋์ ๋ฐฉ์์ ์ฐธ๊ณ ํด ์ฃผ์ธ์
GET http://localhost:8080/api/book/slave
HTTP/1.1 200
Content-Type: application/json
Transfer-Encoding: chunked
Date: Sat, 08 May 2021 12:38:56 GMT
Keep-Alive: timeout=60
Connection: keep-alive
[
{
"title": "new title(slave)",
"id": 1,
"createdAt": "2021-05-08T21:37:43",
"updatedAt": "2021-05-08T21:38:44"
},
...
]
Response code: 200; Time: 107ms; Content length: 521 bytes
์๋ก์ด ํธ๋์ญ์
์ ๊ฒฝ์ฐ masterDataSource
๋ฅผ ๋ฐ๋ผ๋ณด๊ฒ ๋๋ฉฐ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ๋ฐ์๋๋๊ฒ์ ํ์ธํ ์ ์์ต๋๋ค.
ํธ๋์ญ์
์ด๋ฆ์ ๋ด๋ ํด๋น ํธ๋์ญ์
์ด ๋ค๋ฅด๋ค๋ ๊ฒ์ ํ์ธํ ์ ์์ต๋๋ค. ์ฆ com.example.springtransaction.BookService.updateSlave
ํธ๋์ญ์
์ salveDataSoruce
, com.example.springtransaction.BookUpdateService.updateTitle
๋ masterDataSource
๋ฅผ ๋ฐ๋ผ๋ด
๋๋ค.
์ฒซ ํธ๋์ญ์
์ ์ค์ ์ readOnly
์ ๋ฐ๋ผ salveDataSoruce
, masterDataSource
๊ฐ ๊ฒฐ์ ๋๋ฉฐ, ๋์ผํ ํธ๋์ญ์
์์๋ ์ง์ ๋ readOnly
์์ฑ์ ๋ณ๊ฒฝ๋์ง ์์ต๋๋ค. ๊ธฐ์กด ํธ๋์ญ์
๊ณผ ์ ํ ๋ค๋ฅธ ํธ๋์ญ์
์ ๋ง๋๊ฒ ๋๋ฉด ํด๋น ํธ๋์ญ์
์ readOnly
์ค์ ์ ๋ฐ๋ผ DataSource
๊ฐ ๊ฒฐ์ ๋ฉ๋๋ค. ๊ทธ๋ ๋ค๋ฉด ์ ์ด๋ ๊ฒ ๋๋ ๊ฑธ๊น์? ์ ๊ฐ ํ์ตํ๋ ๋ด์ฉ์ ํ ๋๋ก ์ค๋ช
๋๋ฆฌ๊ธฐ ๋๋ฌธ์ ํ๋ฆฐ ๋ถ๋ถ์ด ์์ ์๋ ์์ต๋๋ค.
์คํ๋ง์ ์์ ๊ฐ์ ๋ฐฉ์์ผ๋ก ํธ๋์ญ์ ๋๊ธฐํ๋ฅผ ์งํํฉ๋๋ค. ํด๋น ๋ฐฉ์์ ํธ๋์ญ์ ์ ์์ํ๊ธฐ ์ํด ๋ง๋ Connection ์ค๋ธ์ ํธ๋ฅผ ํน๋ณํ ์ ์ฅ์์ ๋ณด๊ดํด๋๊ณ , ์ดํ์ ํธ์ถ๋๋ ๋ฉ์๋์์ ์ ์ฅ๋ Connection์ ๊ฐ์ ธ๋ค๊ฐ ์ฌ์ฉํฉ๋๋ค.
(1)
Userservice Connection์ ์์ฑ(2)
์์ฑ๋ Connection์ ํธ๋์ญ์ ๋๊ธฐํ ์ ์ฅ์์ ์ ์ฅ, SetAutoCommit(false)๋ฅผ ํธ์ถํ์ฌ ํธ๋์ญ์ ์ ์์(3)
์ฒซ ๋ฒ์งธupdate()
๋ฉ์๋๊ฐ ํธ์ถ๋๊ณ(4)
ํธ๋์ญ์ ๋๊ธฐํ ์ ์ฅ์์ ํ์ฌ ์์๋ ํธ๋์ญ์ ์ ๊ฐ์ง Connection์ ํ์ธํฉ๋๋ค. ์ด ๊ฒฝ์ฐ๋(2)
์์ ์์ฑํ Connection์ ๊ฐ์ ธ์ต๋๋ค.(5)
Connection์ ์ด์ฉํด PreparedsStatment์ ๋ง๋ค์ด ํด๋น SQL์ ์คํํ๊ณ JdbcTemplated๋ Connection ๋ซ์ง ์์ ์ํ๋ก ์์ ์ ๋ง์นจ- ๋์ผํ ํ๋ก์ฐ๋ก
(6)
,(7)
,(8)
์ํ - ๋ ๋์ผํ ํ๋ก์ฐ๋ก
(9)
,(10)
,(11)
์ํํ๋ฉฐ ํธ๋์ญ์ ๋ด์ ๋ชจ๋ ์์ ์ด ์ ์์ ์ผ๋ก ๋๋ฌ์ผ๋ฉด(12)
Conntion commit์ ํธ์ถํด ํธ๋์ญ์ ์ ์๋ฃ (13)
ํธ๋์ญ์ ์ ์ฅ์๊ฐ ๋ ์ด์ Connection ๊ฐ์ฒด๋ฅผ ์ ์ฅํ์ง ์๋๋ก ์ด๋ฅผ ์ ๊ฑฐ
์์ ๊ฐ์ ํ๋ก์ฐ๋ก ํธ๋์ญ์ ์ด ์งํ๋ฉ๋๋ค.
updateSlave()
๋ฉ์๋๋readOnly = true
์์ํ๊ณ ,updateTitle()
๋ฉ์๋์์readOnly = false
๋ก ์งํ ๋๋ ๊ฒฝ์ฐ
์ด๋ฐ ๊ฒฝ์ฐ์๋ updateSlave()
์์ Connection(Slave)์ ์์ฑ ํ๊ณ updateTitle()
์์ ํธ๋์ญ์
๋๊ธฐํ ์ ์ฅ์์ ์ ์ฅ๋ ํธ๋์ญ์
์ ๊ฐ์ง Connection(Slave) ๊ธฐ๋ฐ์ผ๋ก ํธ๋์ญ์
์ ์์ํ๊ฒ ๋ฉ๋๋ค. ๊ทธ๋ฌ๊ธฐ ๋๋ฌธ์ updateTitle()
๋ฉ์๋์ ํธ๋์ญ์
readOnly = false
์ค์ ์ ๋์ํ์ง ์๊ฒ ๋ฉ๋๋ค.
- MySQL 5.7 ์๋ฒฝ ๋ถ์
- Spring, master-slave dynamic routing datasource ์ฌ์ฉํ๊ธฐ <<<<<<< HEAD
- ํ ๋น์ ์คํ๋ง 3.1 =======
53a12ae724b5eb7b28a7ab70bd966a3cd19a1b23