1. データベースの読み取りと書き込みの分離とは何ですか?
データベースの読み取りと書き込みの分離とは、データベースをマスターとスレーブのライブラリに分割することであり、マスター ライブラリはデータの書き込みと、追加、削除、変更によるデータの保守に使用されます。スレーブ ライブラリは、同期メカニズム、つまりメイン ライブラリのフル ミラー バックアップを通じてメイン ライブラリからのデータを同期します。読み取りと書き込みの分離は通常、マスター ライブラリを複数のスレーブ ライブラリで構成することであり、これは一般的なデータベース アーキテクチャ設計です。
2. データベースの読み取りと書き込みを分離すると、どのような問題が解決されますか?
データベースの読み取りと書き込みの分離は、主にビジネスにおけるデータ読み取りパフォーマンスのボトルネックを解決するために行われます。ほとんどのシステムは一般に読み取りが多く書き込みは少ないため、現時点では読み取り操作がまずデータベース サービスのボトルネックとなり、間接的にデータベース書き込みの問題を引き起こします。一般に、インターネット システムは、当初は単一データベース、つまり読み取りと書き込みを共通のデータベースで行う設計アーキテクチャを採用しますが、ビジネスの拡大とシステム ユーザーの増加に伴い、単一データベースの読み取りと書き込みが行き詰まるようになります。そして事業運営さえも失敗してしまいます。現時点では、読み取りと書き込みを分離するという設計思想は、システムのニーズをある程度満たすことができます。読み取りと書き込みを分離したデータベース アーキテクチャを適切に使用すると、データベース読み取り操作のパフォーマンスのボトルネックが直線的に改善され、同時に書き込み操作に対する読み取りと書き込みのロックの競合の影響が解決され、書き込み操作のパフォーマンスが向上します。
注: 読み取りと書き込みの分離は、システムのビジネスおよびユーザー量のある程度の増加に伴うデータベースのパフォーマンス競合を一時的に解決するための設計上のアイデアにすぎず、この問題を完全に解決することはできません。システムのサイズが十分に大きい場合、読み取りと書き込みの分離ではシステムのパフォーマンスの問題に対処できなくなります。現時点では、これを解決するには、データベースのサブライブラリ、パーティション、テーブルのサブセグメント化など、他の設計アイデアが必要です。
3. 読み書き分離コードの実装(springboot+mybatis+mysql)
1. まず、myslql のマスター/スレーブライブラリの設定を行う必要がありますが、この設定については以前に書いたこちらの記事を参照してください。
2. コードの実装
1. idea で新しい springboot プロジェクトを作成します
2. 次の依存関係注入構成情報を pom.xml に追加します。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 使用TkMybatis可以无xml文件实现数据库操作,只需要继承tkMybatis的Mapper接口即可-->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
<version>1.1.4</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.9</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
3. src/main/resources ディレクトリに新しい application.yml ファイルを作成し、次の情報を構成します
server:
port: 8082
servlet:
context-path: /dxfl
spring:
datasource:
#读库数目
maxReadCount: 1
type-aliases-package: com.teamo.dxfl.mapper
mapper-locations: classpath:/mapper/*.xml
config-location: classpath:/mybatis-config.xml
write:
url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=true
username: root
password:
driver-class-name: com.mysql.jdbc.Driver
initialSize: 2 #初始化大小
maxWait: 6000 #获取连接时最大等待时间,单位毫秒。
min-idle: 5 # 数据库连接池的最小维持连接数
maxActive: 20 # 最大的连接数
initial-size: 5 # 初始化提供的连接数
max-wait-millis: 200 # 等待连接获取的最大超时时间
read1:
url: jdbc:mysql://localhost:3307/test?useUnicode=true&characterEncoding=utf-8&useSSL=true
username: root
password:
driver-class-name: com.mysql.jdbc.Driver
initialSize: 2 #初始化大小
maxWait: 6000 #获取连接时最大等待时间,单位毫秒。
min-idle: 5 # 数据库连接池的最小维持连接数
maxActive: 20 # 最大的连接数
initial-size: 5 # 初始化提供的连接数
max-wait-millis: 200 # 等待连接获取的最大超时时间
4. データ ソースの構成クラス (config ディレクトリの下)
DataSourceConfig.javaを書き込みます。
@Configuration
public class DataSourceConfig {
@Value("${spring.datasource.type-aliases-package}")
private String typeAliasesPackage;
@Value("${spring.datasource.mapper-locations}")
private String mapperLocation;
@Value("${spring.datasource.config-location}")
private String configLocation;
/**
* 写数据源
* @Primary 标志这个 Bean 如果在多个同类 Bean 候选时,该 Bean 优先被考虑。
* 多数据源配置的时候注意,必须要有一个主数据源,用 @Primary 标志该 Bean
*/
@Primary
@Bean
@ConfigurationProperties(prefix = "spring.datasource.write")
public DataSource writeDataSource() {
return new DruidDataSource();
}
/**
* 读数据源
*/
@Bean
@ConfigurationProperties(prefix = "spring.datasource.read1")
public DataSource readDataSource1() {
return new DruidDataSource();
}
/**
* 多数据源需要自己设置sqlSessionFactory
*/
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(routingDataSource());
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
// 实体类对应的位置
bean.setTypeAliasesPackage(typeAliasesPackage);
// mybatis的XML的配置
bean.setMapperLocations(resolver.getResources(mapperLocation));
bean.setConfigLocation(resolver.getResource(configLocation));
return bean.getObject();
}
/**
* 设置数据源路由,通过该类中的determineCurrentLookupKey决定使用哪个数据源
*/
@Bean
public AbstractRoutingDataSource routingDataSource() {
RoutingDataSourceConfig proxy = new RoutingDataSourceConfig();
DataSource writeDataSource = writeDataSource();
//设置数据源Map对象
Map<Object, Object> dataSource = new HashMap<Object, Object>(2);
dataSource.put(DataBaseTypeEnum.WRITE.getCode(), writeDataSource);
//如果配置了多个读数据源,就一次添加到datasource对象中
dataSource.put(DataBaseTypeEnum.READ.getCode()+"1", readDataSource1());
//写数据源设置为默认数据源
proxy.setDefaultTargetDataSource(writeDataSource);
proxy.setTargetDataSources(dataSource);
return proxy;
}
/**
* 设置事务,事务需要知道当前使用的是哪个数据源才能进行事务处理(配置mybatis时候用到)
*/
@Bean
public DataSourceTransactionManager dataSourceTransactionManager() {
return new DataSourceTransactionManager(routingDataSource());
}
}
5. データ ソース ルーティングの構成クラスを (config ディレクトリの下に) 作成します。このクラスは、AbstractRoutingDataSource を継承し、データ ソース選択のロジックを実装する detectCurrentLookupKey() メソッドを書き換えます。同時に、このクラスは ThreadLocal オブジェクトを保持します。このオブジェクトは、現在のスレッドで使用されるデータ ソースの種類が読み取りであるか書き込みであるかを格納するために使用されます。読み取りライブラリの負荷分散(読み取りライブラリの追加や複数設定)を実現するアルゴリズムを自分で書くこともでき、乱数を取得することでどの読み取りデータベースのデータソースを使用するかを簡単に選択できます。
DataSourceRouteConfig.java
public class RoutingDataSourceConfig extends AbstractRoutingDataSource {
//使用ThreadLocal对象保存当前线程是否处于读模式
private static ThreadLocal<String> DataBaseMap= new ThreadLocal<>();
@Value("${spring.datasource.maxReadCount}")
private int maxReadCount;
private final Logger log = LoggerFactory.getLogger(this.getClass());
@Override
protected Object determineCurrentLookupKey() {
String typeKey = getDataBaseType();
if (typeKey == DataBaseTypeEnum.WRITE.getCode()) {
log.info("使用了写库");
return typeKey;
}
//使用随机数决定使用哪个读库
int index = (int) Math.floor(Math.random() * (maxReadCount - 1 + 1)) + 1;;
log.info("使用了读库{}", index);
return DataBaseTypeEnum.READ.getCode() + index;
}
public static void setDataBaseType(String dataBaseType) {
DataBaseMap.set(dataBaseType);
}
public static String getDataBaseType() {
return DataBaseMap.get() == null ? DataBaseTypeEnum.WRITE.getCode() : DataBaseMap.get();
}
public static void clearDataBaseType() {
DataBaseMap.remove();
}
}
6. Annotation クラスを作成する必要があります。これは、Service クラスのどのメソッドが読み取りデータ ソースを使用するか、どのメソッドが書き込みデータ ソースを使用するかをマークするために特別に使用されます。(このメソッドに加えて、書き込みデータ ソースに一致するワイルドカード save*、update*、delete* メソッドなど、一致するメソッド名を aop クラスに設定することもできます。その他のメソッドは読み取りデータ ソースであり、ここでのみ考慮されます。 注釈アノテーション方法)。
ReadDB.java
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ReadDB {
//String value() default "";
}
7. aopクラスを書く トランザクションのオープンを要求する前に、Springのaop(アスペクト指向プログラミング)を使って、ServiceメソッドにReadDBアノテーションがあるかどうかを判断し、メソッドにReadDBアノテーションがある場合は、読み込むデータソースを選択する。このクラスは Ordered インターフェイスを実装し、getOrder() メソッドを書き換えます。このインターフェイスは、メソッド order を実行するクラスを呼び出すように Spring に通知します。getOrder() メソッドによって返される値が小さいほど、優先度が高くなります。ここで、データ ソースを選択する操作が実行される前に確実に実行されるように、 getOrder() メソッドによって返される値は、プロジェクト スタートアップ クラスの@EnableTransactionManagement(order= )に設定された値よりも小さい必要があることに注意してください。取引を開始しています。
DxflApplication.java
@SpringBootApplication
@EnableTransactionManagement(order = 5)
public class DxflApplication {
public static void main(String[] args) {
SpringApplication.run(DxflApplication.class, args);
}
}
ReadDBAspect.java
@Aspect
@Component
public class ReadDBAspect implements Ordered {
private static final Logger log= LoggerFactory.getLogger(ReadDBAspect.class);
@Around("@annotation(readDB)")
public Object setRead(ProceedingJoinPoint joinPoint, ReadDB readDB) throws Throwable {
try{
//设置读数据源
RoutingDataSourceConfig.setDataBaseType(DataBaseTypeEnum.READ.getCode());
return joinPoint.proceed();
}finally {
log.info("清除dataSource类型选中值:"+DataBaseTypeEnum.READ.getData());
RoutingDataSourceConfig.clearDataBaseType();
}
}
@Override
public int getOrder() {
return 0;
}
}
8. Service メソッドを記述し、データを読み取るメソッドに @ReadDB アノテーションを追加し、そのメソッドをデータベースを読み取るメソッドとしてマークします。
UserServiceImpl.java
@Service
public class UserviceServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
@ReadDB
public List<User> getAllUser() {
return userMapper.selectAll();
}
@Override
@Transactional(rollbackFor = RuntimeException.class)
public boolean save(User user){
if(userMapper.insert(user)>0){
return true;
}else{
return false;
}
//throw new RuntimeException("测试事务");
}
}
9. Service メソッドを呼び出して読み取りメソッドと書き込みメソッドのテストを実装するコントローラー クラスを作成します。
ユーザーコントローラー.java
@RestController
@RequestMapping("/user")
public class UserController{
@Autowired
private UserService userService;
@GetMapping("getAllUser")
public List<User> getAllUser() {
List resList = userService.getAllUser();
return resList;
}
@PostMapping("save")
public Result save(@RequestBody User user){
Result result = new Result();
if(null != user){
try{
userService.save(user);
result.setCode(ResultEnum.SUCCESS.getCode());
result.setMsg("保存用户成功!");
}catch(Exception e) {
e.printStackTrace();
result.setCode(ResultEnum.SUCCESS.getCode());
result.setMsg("保存用户失败,原因:"+ e.getMessage() +"!");
}
}else{
result.setCode(ResultEnum.SUCCESS.getCode());
result.setMsg("保存用户失败,原因:用户数据为空!");
}
return result;
}
}
10. テスト
サービスが正常に開始されたら、ライブラリの読み取りをテストします。ブラウザにアドレス http://localhost:8089/dxfl/user/getAllUser を入力します。表示は次のようになります。 バックグラウンド印刷を確認します
。ライブラリは
ライブラリのテストに使用されます。 ライブラリのテストと
ライブラリの作成にはフォームの送信が必要です。このときテスト ツールが必要です。ここでは ApiPost を使用します。テスト ツールを開き、テスト列にユーザーを保存するためのインターフェイスを入力し、送信方法として application/json を選択して、送信ボタンをクリックすると、成功した返信メッセージが応答とともに表示され、保存を求めるプロンプトが表示されます。ユーザーは書き込みテストが成功したことを意味します。
バックエンド ログの印刷情報は、書き込みライブラリが使用され、テーブル名のテスト書き込みライブラリが成功したことです。
読み取りと書き込みの分離が本当に成功しているかどうかを正常に検証するには、書き込みライブラリのテスト時に最初にスレーブ ライブラリ全体の機能をオフ (スレーブの停止) にしてから、マスター/スレーブ ライブラリの機能をオンにします (スレーブを開始) 指定された書き込みライブラリが書き込まれたことを確認した後。