728x90

๐Ÿ“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 

  1. ์šฐ์„  ์ด์ „์— ๋งŒ๋“ค์—ˆ๋˜ ReplicationRoutingDataSource ํด๋ž˜์Šค์— Master DB์™€ Slave DB๋ฅผ ์ถ”๊ฐ€ํ•ด์ค€๋‹ค.
  2. replicationRoutingDataSource ์˜ replicationRoutingDataSourceNameList ์„ธํŒ…ํ•œ๋‹ค.(Slave Key ์ด๋ฆ„ ๋ฆฌ์ŠคํŠธ ์„ธํŒ…)
  3. ๋””ํดํŠธ๋Š” 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

728x90