作者:狂乱的贵公子 

# 1、引言

读写分离要做的事情就是对于一条SQL该选择哪个数据库去执行,至于谁来做选择数据库这件事儿,无非两个,要么中间件帮我们做,要么程序自己做。因此,一般来讲,读写分离有两种实现方式。第一种是依靠中间件(比如:MyCat),也就是说应用程序连接到中间件,中间件帮我们做SQL分离;第二种是应用程序自己去做分离。这里我们选择程序自己来做,主要是利用Spring提供的路由数据源,以及AOP。

然而,应用程序层面去做读写分离最大的弱点(不足之处)在于无法动态增加数据库节点,因为数据源配置都是写在配置中的,新增数据库意味着新加一个数据源,必然改配置,并重启应用。当然,好处就是相对简单。




mysql和plsql并发_数据源


# 2、AbstractRoutingDataSource

基于特定的查找key路由到特定的数据源。它内部维护了一组目标数据源,并且做了路由key与目标数据源之间的映射,提供基于key查找数据源的方法。


mysql和plsql并发_plsql能连mysql吗_02


# 3、工程结构


mysql和plsql并发_mysql和plsql并发_03


# 4、实践

关于配置请参考:

1)maven依赖

<?xml version="1.0" encoding="UTF-8"?>4.0.0com.cjs.example cjs-datasource-demo 0.0.1-SNAPSHOTjarcjs-datasource-demoorg.springframework.boot spring-boot-starter-parent 2.0.5.RELEASEUTF-8UTF-81.8org.springframework.boot spring-boot-starter-aop org.springframework.boot spring-boot-starter-jdbc org.springframework.boot spring-boot-starter-web org.mybatis.spring.boot mybatis-spring-boot-starter 1.3.2org.apache.commons commons-lang3 3.8mysql mysql-connector-java runtimeorg.springframework.boot spring-boot-starter-test testorg.springframework.boot spring-boot-maven-plugin

2)数据源配置

application.yml


mysql和plsql并发_mysql和plsql并发_04


多数据源配置


mysql和plsql并发_spring_05


这里,我们配置了4个数据源,1个master,2两个slave,1个路由数据源。前3个数据源都是为了生成第4个数据源,而且后续我们只用这最后一个路由数据源。

MyBatis配置


mysql和plsql并发_数据源_06


由于Spring容器中现在有4个数据源,所以我们需要为事务管理器和MyBatis手动指定一个明确的数据源。

3)设置路由key / 查找数据源

目标数据源就是那前3个这个我们是知道的,但是使用的时候是如果查找数据源的呢?

首先,我们定义一个枚举来代表这三个数据源。

package com.cjs.example.enums;public enum DBTypeEnum { MASTER, SLAVE1, SLAVE2;}

接下来,通过ThreadLocal将数据源设置到每个线程上下文中。

package com.cjs.example.bean;import com.cjs.example.enums.DBTypeEnum;import java.util.concurrent.atomic.AtomicInteger;public class DBContextHolder { private static final ThreadLocal contextHolder = new ThreadLocal<>(); private static final AtomicInteger counter = new AtomicInteger(-1); public static void set(DBTypeEnum dbType) { contextHolder.set(dbType); } public static DBTypeEnum get() { return contextHolder.get(); } public static void master() { set(DBTypeEnum.MASTER); System.out.println("切换到master"); } public static void slave() { // 轮询 int index = counter.getAndIncrement() % 2; if (counter.get() > 9999) { counter.set(-1); } if (index == 0) { set(DBTypeEnum.SLAVE1); System.out.println("切换到slave1"); }else { set(DBTypeEnum.SLAVE2); System.out.println("切换到slave2"); } }}

获取路由key。

package com.cjs.example.bean;import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;import org.springframework.lang.Nullable;public class MyRoutingDataSource extends AbstractRoutingDataSource { @Nullable @Override protected Object determineCurrentLookupKey() { return DBContextHolder.get(); }}

设置路由key

默认情况下,所有的查询都走从库,插入/修改/删除走主库。我们通过方法名来区分操作类型(CRUD)。


mysql和plsql并发_mysql和plsql并发_07


有一般情况就有特殊情况,特殊情况是某些情况下我们需要强制读主库,针对这种情况,我们定义一个主键,用该注解标注的就读主库。

package com.cjs.example.annotation;public @interface Master {}

例如,假设我们有一张表member。

package com.cjs.example.service.impl;import com.cjs.example.annotation.Master;import com.cjs.example.entity.Member;import com.cjs.example.entity.MemberExample;import com.cjs.example.mapper.MemberMapper;import com.cjs.example.service.MemberService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;import java.util.List;@Servicepublic class MemberServiceImpl implements MemberService { @Autowired private MemberMapper memberMapper; @Transactional @Override public int insert(Member member) { return memberMapper.insert(member); } @Master @Override public int save(Member member) { return memberMapper.insert(member); } @Override public List selectAll() { return memberMapper.selectByExample(new MemberExample()); } @Master @Override public String getToken(String appId) { // 有些读操作必须读主数据库 // 比如,获取微信access_token,因为高峰时期主从同步可能延迟 // 这种情况下就必须强制从主数据读 return null; }}

# 5、测试

package com.cjs.example;import com.cjs.example.entity.Member;import com.cjs.example.service.MemberService;import org.junit.Test;import org.junit.runner.RunWith;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.test.context.junit4.SpringRunner;@RunWith(SpringRunner.class)@SpringBootTestpublic class CjsDatasourceDemoApplicationTests { @Autowired private MemberService memberService; @Test public void testWrite() { Member member = new Member(); member.setName("zhangsan"); memberService.insert(member); } @Test public void testRead() { for (int i = 0; i < 4; i++) { memberService.selectAll(); } } @Test public void testSave() { Member member = new Member(); member.setName("wangwu"); memberService.save(member); } @Test public void testReadFromMaster() { memberService.getToken("1234"); }}

查看控制台


mysql和plsql并发_spring_08


mysql和plsql并发_数据源_09



你看到这里,我相信你对数据库的读写分离有了大概的了解,说白了,读写分离就是拿机子和带宽来换性能。这里,我再抛给各位读者大大几个问题去思考。

1、读写分离从哪些方面提高了性能?一般使用于那种场景?

2、主库与从库之间是怎么进行同步的?

3、Spring AOP 还能应用于哪些场景?