๐DB Replication
https://jarvics.tistory.com/68
[MYSQL]ํจ์จ์ ์ธ ํธ๋ํฝ ๋ถ์ฐ์ ์ํ Master/Slave ๋์ ๋ผ์ฐํ
์ฌ์ฉ์๊ฐ ์ง์์ ์ผ๋ก ์ฆ๊ฐํ๋ฉด ๋ง์ ์์ ํธ๋ํฝ์ด ๋ฐ์ํ๋ค. ๊ทธ๋ฌ๋ฏ๋ก ํ๋์ DB์๋ฒ๋ก ์ฐ๊ธฐ์ ์ฝ๊ธฐ ์์ ์ด ๋ชจ๋ ์งํ๋๋ค๋ฉด ์ฝ๊ฒ DB์๋ฒ์ ๋ถํ๊ฐ ๋ฐ์ํ ์ ์๋ค. ์ด ๋ฌธ์ ์ ๋ํ ํด๊ฒฐ์ฑ ์ผ๋ก
jarvics.tistory.com
- ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ด์คํ ๋ฐฉ์ ์ค ํ๋๋ก Master DB + Slave DB๋ก ๊ตฌ์ฑํ๋ค.
- Master DB์ ๋ฐ์ดํฐ ๋ณ๊ฒฝ์ด ๊ฐ์ง๋๋ฉด Master DB ๋ก๊ทธ๋ฅผ ๊ธฐ๋ฐ์ผ๋ก Slave DB์ ๋ณต์ ํ๋ค.
- Master DB์๋ ๋ฐ์ดํฐ ๋ณ๊ฒฝ์ด ํ์ํ INSERT,DELETE,UPDATE ๋ฑ์ ์ฟผ๋ฆฌ๊ฐ ์ฌ์ฉํ๊ณ , Slave DB์๋ SELECT๋ฌธ์ด ์ฌ์ฉํ๋ค.
Master DB ์ Slave DB๋ฅผ ๋๋ ๊ตฌ์ฑํ ๋ค์ @Transaction์ readonly ์์ฑ์ด true์ด๋ฉด Slave DB๋ฅผ ์ฌ์ฉํ๊ณ false์ด๋ฉด Master DB๋ฅผ ์ฌ์ฉํ๋ค.
์ฐ์ SpringBoot์ RDS(MYSQL)์ด ์ฐ๋์ด ์๋ฃ๋์๋ค๋ ๊ฐ์ ํ์ ์งํํ ๊ฒ์ด๋ค.
์ฐ์ AWS ์ ์ํ ๋ค ์๋น์ค์์ RDS๋ก ๋ค์ด๊ฐ ๋ณธ์ธ์ด ์์ฑํ DB์ธ์คํด์ค๋ฅผ ์ ํํ๋ค.
์ ํํ ๋ค์ ์์ ์ ํด๋ฆญํ์ฌ ์ฝ๊ธฐ ์ ์ฉ ๋ณต์ ๋ณธ ์์ฑ์ ํด๋ฆญํ๋ค.
ํด๋ฆญํ ๋ค์ ํผ๋ธ๋ฆญ ์์ธ์ค๋ฅผ ํ์ฉ ํด์ค๋ค. ๊ทธ๋ฌ๋ฉด ๋ณต์ ๋ณธ ์์ฑ์ด ์๋ฃ๋๋ค.
์ฌ๊ธฐ๊น์ง๋ ๋ฑํ ์ด๋ ค์์ด ์๋ค.
๋ค์์ ์คํ๋ง ํ๋ก์ ํธ๋ก ๊ฐ์ ์ค์ ์ ํด๋ณด์
spring:
datasource:
url: jdbc:mysql://๋ณธ์ธ RDS ์ค๋ํฌ์ธํธURL:3306/์ฌ์ฉํ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ด๋ฆ?useSSL=false&useUnicode=true&characterEncoding=utf8
slave-list:
- name: slave
url: jdbc:mysql://๋ณธ์ธ RDS ๋ณต์ ๋ณธ ์ค๋ํฌ์ธํธ URL:3306/์ฌ์ฉํ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ด๋ฆ?useSSL=false&useUnicode=true&characterEncoding=utf8
username: RDS์ค์ username
password: RDS์ค์ password
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
show-sql: true
database-platform: org.hibernate.dialect.MySQL8Dialect
database: mysql
hibernate:
ddl-auto: update
generate-ddl: true
์ฐ์ RDS์ค์ ์ ๋ณด๋ฅผ ์ ์ด ๋์ ymlํ์ผ์ slave-list,name,url ์ ์ถ๊ฐํด์ค๋ค.
๋ค์์ DataSource๋ฅผ ์ง์ ์ค์ ํด์ผํ๊ธฐ ๋๋ฌธ์ Spring์ ์คํํ ๋ DataSourceAutoConfiguration์ ์ ์ธ์์ผ์ค๋ค.
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
์ด์ DB ์ค์ ํ์ผ์ ๊ฐ์ ธ์ฌ DatabasePropertyํด๋์ค๋ฅผ ๋ง๋ ๋ค.(yml์์ ์ค์ ์ ๊ฐ์ ธ์ค๋ ํด๋์ค์ด๋ค.)
@Getter
@Setter
@Component
@ConfigurationProperties("spring.datasource")
public class DatabaseProperty {
private String url;
private List<Slave>slaveList;
private String username;
private String password;
private String driverClassName;
@Getter
@Setter
public static class Slave {
private String name;
private String url;
}
}
์ฌ๋ฌ๋์ Slave DB๋ฅผ ์์๋๋ก ๋ก๋๋ฐธ๋ฐ์ฑ ํ๊ธฐ์ํด CircularListํด๋์ค๋ฅผ ๋ง๋ ๋ค.
public class CircularList<T> {
private List<T> list ;
private Integer counter = 0;
public CircularList(List<T> list) {
this.list = list;
}
public T getOne(){
if (counter + 1 >= list.size()){
counter = -1;
}
return list.get(++counter);
}
}
- ์ฌ๋ฌ๊ฐ์ DataSource๋ฅผ ๋ฌถ๊ณ ํ์์ ๋ฐ๋ผ ๋ถ๊ธฐ์ฒ๋ฆฌ๋ฅผ ์ํด AbstractRoutingDataSourceํด๋์ค๋ฅผ ์ฌ์ฉํด์ผ ํ๋ค.
- ์ฌ๋ฌ๋์ Slave DB๋ฅผ ์์๋๋ก ์ฌ์ฉํ๊ธฐ ์ํด CircularList์ Slave DB ํค๋ฅผ ์ถ๊ฐ ํด์ค๋ค.
- determineCurrentLookup ๋ฉ์๋์์ ํ์ฌ @Transactional(readOnly = true)์ผ ๊ฒฝ์ฐ Slave DB๋ก, ์๋ ๊ฒฝ์ฐ Master DB์ DataSource์ ํค๋ฅผ ๋ฆฌํดํ๋๋ก ์ค์ ํด์ค๋ค.
public class ReplicationRoutingDataSource extends AbstractRoutingDataSource {
private CircularList<String> dataSourceList;
@Override
public void setTargetDataSources(Map<Object, Object> targetDataSources) {
super.setTargetDataSources(targetDataSources);
dataSourceList = new CircularList<>(
targetDataSources.keySet()
.stream()
.filter(key -> key.toString().contains("slave"))
.map(key -> key.toString())
.collect(toList()));
}
@Override
protected Object determineCurrentLookupKey() {
boolean isReadOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly();
if (isReadOnly){
return dataSourceList.getOne();
}else{
return "master";
}
}
}
์ด์ ์ต์ข ์ ์ผ๋ก DataSource, TransactionManager, EntityManager ์ค์ ์ ํด์ผํ๋ค.
์ฐ์ DataConfig ํด๋์ค๋ฅผ ์์ฑํ๋ค.
@Configuration
public class DatabaseConfig {
@Autowired
private DatabaseProperty databaseProperty;
//์๋ routingDataSource์์ ์ฌ์ฉํ ์ค์ ๋ฉ์๋
public DataSource routingDataProperty(String url){
HikariDataSource hikariDataSource = new HikariDataSource();
hikariDataSource.setJdbcUrl(databaseProperty.getUrl());
hikariDataSource.setDriverClassName(databaseProperty.getDriverClassName());
hikariDataSource.setPassword(databaseProperty.getPassword());
hikariDataSource.setUsername(databaseProperty.getUsername());
return hikariDataSource;
}
@Bean
public DataSource routingDataSource(){
ReplicationRoutingDataSource replicationRoutingDataSource = new ReplicationRoutingDataSource();
//#1
DataSource master = routingDataProperty(databaseProperty.getUrl());
Map<Object,Object> dataSourceMap = new LinkedHashMap<>();
dataSourceMap.put("master",master);
//#2
databaseProperty.getSlaveList().forEach(slave -> {
dataSourceMap.put(slave.getName() , routingDataProperty(slave.getUrl()));
});
//#2
replicationRoutingDataSource.setTargetDataSources(dataSourceMap);
//#3
replicationRoutingDataSource.setDefaultTargetDataSource(master);
return replicationRoutingDataSource;
}
@Bean
public DataSource dataSource() {
return new LazyConnectionDataSourceProxy(routingDataSource());
}
#routingDataSource
- ์ฐ์ ์ด์ ์ ๋ง๋ค์๋ ReplicationRoutingDataSource ํด๋์ค์ Master DB์ Slave DB๋ฅผ ์ถ๊ฐํด์ค๋ค.
- replicationRoutingDataSource ์ replicationRoutingDataSourceNameList ์ธํ ํ๋ค.(Slave Key ์ด๋ฆ ๋ฆฌ์คํธ ์ธํ )
- ๋ํดํธ๋ Master๋ก ์ค์
#dataSource
- LazyConnectionDataSourceProxy๋ ์ค์ ์ฟผ๋ฆฌ๊ฐ ์คํ๋ ๋ Connection์ ๊ฐ์ ธ์จ๋ค.
- LazyConnectionDataSourceProxy๋ ์ค์ง์ ์ธ ์ฟผ๋ฆฌ ์คํ ์ฌ๋ถ์ ์๊ด์์ด ํธ๋์ญ์ ์ด ๊ฑธ๋ฆฌ๋ฉด ๋ฌด์กฐ๊ฑด Connection๊ฐ์ฒด๋ฅผ ํ๋ณดํ๋ Spring์ ๋จ์ ์ ๋ณด์ํ๋ฉฐ ํธ๋์ญ์ ์์์์ Connection Proxy๊ฐ์ฒด๋ฅผ ๋ฆฌํดํ๊ณ ์ค์ ๋ก ์ฟผ๋ฆฌ๊ฐ ๋ฐ์ํ ๋ ๋ฐ์ดํฐ ์์ค์์ getConnection()์ ํธ์ถํ๋ ์ญํ ์ ํ๋ค.
- TransactionSynchronizationManager๊ฐ ํ์ฌ ํธ๋์ญ์ ์ ์ํ๋ฅผ ์ฝ์ด์ฌ ์ ์์ง๋ง ํธ๋์ญ์ ๋๊ธฐํ ์์ ๊ณผ Connection์ด ์ฐ๊ฒฐ๋๋ ์์ ์ด ๋ค๋ฅด๊ธฐ ๋๋ฌธ์ LazyConnectionDataSourceProxy๋ฅผ ์ฌ์ฉํ์ฌ Connection๊ฐ์ฒด๋ฅผ ๊ฐ์ ธ์จ๋ค.
์ด์ ๊ธฐ๋ณธ์ค์ ์ ์ด๋์ ๋ ๋ง๋ฌด๋ฆฌ ๋์๊ณ JPA์์ ์ฌ์ฉํ EntityManager๊ณผ TransactionManager ์ค์ ์ ํด์ค๋ค.
@Configuration
public class DatabaseConfig {
.
.
.
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory(){
LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean();
entityManagerFactoryBean.setDataSource(dataSource());
entityManagerFactoryBean.setPackagesToScan("์ํฐํฐ๊ฐ ์์นํ ํจํค์ง ๊ฒฝ๋ก" ex)com.example);
JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
entityManagerFactoryBean.setJpaVendorAdapter(vendorAdapter);
return entityManagerFactoryBean;
}
@Bean
public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory){
JpaTransactionManager tm = new JpaTransactionManager();
tm.setEntityManagerFactory(entityManagerFactory);
return tm;
}
}
์ด์ JPA ์ค์ ๊น์ง ๋ง๋ฌด๋ฆฌ ํ๋ค.
ํ ์คํธ๋ฅผ ํ๋ฒ ํด๋ณด์
Test
@Entity
@Table(name = "member")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@Builder
public Product(String name) {
this.na,e = name;
}
}
public interface MemberRepository extends JpaRepository<Member, Long> {}
@RestController
@RequestMapping("/api/members")
public class MemberController {
@Autowired
private MemberService memberService;
@GetMapping
public ResponseEntity<?> getMembers() {
List<Member> memberList = memberService.getMembers();
return new ResponseEntity<>(memberList, HttpStatus.OK);
}
@GetMapping("/masterDB")
public ResponseEntity<?> getMembersFromMasterDB() {
List<Member> memberList = memberService.getMembersMaster();
return new ResponseEntity<>(memberList, HttpStatus.OK);
}
}
@Service
public class MemberService {
@Autowired
private MemberRepository memberRepository;
@Transactional(readOnly = true)
public List<Member> getMembers() {
return memberRepository.findAll();
}
@Transactional
public List<Member> getMembersMaster() {
return memberRepository.findAll();
}
}
Master/Slave DB์ ์ฟผ๋ฆฌ๊ฐ ๋ ์๊ฐ๋ ๊ฒ์ ํ์ธํ๊ธฐ ์ํด yml ํ์ผ์ ์ค์ ์ ์ถ๊ฐํด์ค๋ค.
logging:
level:
org.springframework.jdbc.datasource.SimpleDriverDataSource: DEBUG
org.hibernate.SQL: DEBUG
์ด์ DB์ member Table์ ์์ฑํ ๊ฒ์ด๋ค. ๊ทธ๋ฌ๋ ์๋ JPA๋ ์๋์ผ๋ก Table์ ์์ฑํ ์ ์์ง๋ง ์ฒ์์ DataSourceAutoConfiguration๋ฅผ ์ ์ธ์์ผฐ๊ธฐ ๋๋ฌธ์ ์ง์ ์ค์ ํด์ฃผ์ด์ผํ๋ค.
SQL
CREATE TABLE `member` (
`id` long NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
insert into `member` (name) values ('name1'),
('name2'), ('name3');
์๋ฒ๋ฅผ ์คํ ํ /api/members ์ GET์์ฒญ์ ํ๋ฉด ์๋์ ๊ฐ์ ๊ฒฐ๊ณผ๊ฐ์ด ๋์จ๋ค.
[
{
"id": 1,
"name": "name1",
},
{
"id": 2,
"name": "name2",
},
{
"id": 3,
"name": "name3",
}
]
SimpleDriverDataSource์ ๋ก๊ทธ๋ฅผ ํ์ธํด๋ณด๋ฉด
Creating new JDBC Driver Connection to [jdbc:mysql://campshop-slave.concrp2jli...
์ด๋ ๊ฒ Slave DB๋ฅผ ์ฌ์ฉํฉ๋๋ค.
/api/member/master์ ์์ฒญ์ ๋ณด๋ด๋ฉด
Creating new JDBC Driver Connection to [jdbc:mysql://campshop-db.concrp...
์ด๋ ๊ฒ Master DB๋ฅผ ์ฌ์ฉํฉ๋๋ค.
์ด๋ฐ์์ผ๋ก ๋์ฉ๋ ํธ๋ํฝ์ผ๋ก ์ธํด ์๊ธฐ๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ฌธ์ ๋ฅผ ํจ์จ์ ์ผ๋ก ๋ถ์ฐ์ํค๋ฉฐ ํด๊ฒฐํ ์ ์๋ค.
REFERENCE
'Dev > Database' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[mybatis] Association๊ณผ Collection (0) | 2022.02.22 |
---|---|
[DB]DB Partitioning [DB ํํฐ์ ๋] (0) | 2022.02.15 |
[MYSQL]ํจ์จ์ ์ธ ํธ๋ํฝ ๋ถ์ฐ์ ์ํ Master/Slave ๋์ ๋ผ์ฐํ (0) | 2021.10.24 |
[DB]ํธ๋์ญ์ (transaction) (0) | 2021.09.14 |
[DB]์ด์ํ์(Anomly), ํจ์ ์ข ์์ฑ (Functional Dependency) (0) | 2021.09.13 |