使用springboot aop來實現(xiàn)讀寫分離和事物配置
什么事讀寫分離
讀寫分離,基本的原理是讓主數(shù)據(jù)庫處理事務(wù)性增、改、刪操作(INSERT、UPDATE、DELETE),而從數(shù)據(jù)庫處理SELECT查詢操作。數(shù)據(jù)庫復(fù)制被用來把事務(wù)性操作導(dǎo)致的變更同步到集群中的從數(shù)據(jù)庫。
為什么要實現(xiàn)讀寫分離
增加冗余
增加了機器的處理能力
對于讀操作為主的應(yīng)用,使用讀寫分離是最好的場景,因為可以確保寫的服務(wù)器壓力更小,而讀又可以接受點時間上的延遲。
實現(xiàn)
本文介紹利用spring aop來動態(tài)切換數(shù)據(jù)源來實現(xiàn)讀寫分離。
先建一個maven項目,導(dǎo)入springBoot依賴。
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.2.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!--mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>${druid.version}</version>
</dependency>
<!-- mysql connector-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql-connector.version}</version>
</dependency>
<!--test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
然后在配置文件application.yml中自定義數(shù)據(jù)源配置項
server: port: 8080 logging: level: org.springframework: INFO com.qiang: DEBUG spring: output: ansi: enabled: always datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/db_area?characterEncoding=utf-8&serverTimezone=Asia/Shanghai username: root password: root db: readsize: 2 read0: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/db_area?characterEncoding=utf-8&serverTimezone=Asia/Shanghai username: root password: root read1: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/db_area?characterEncoding=utf-8&serverTimezone=Asia/Shanghai username: root password: root aop: auto: true proxy-target-class: true
配置Druid
package com.qiang.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import javax.sql.DataSource;
/**
* @author gengqiang
* @date 2018/5/3
*/
@Configuration
public class DruidConfig {
private Logger logger = LoggerFactory.getLogger(DruidConfig.class
/**
* 主據(jù)源
* @return
*/
@Primary
@Bean(name = "dataSource")
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource dataSource() {
return DataSourceBuilder.create().type(com.alibaba.druid.pool.DruidDataSource.class).build();
}
/**
* 從數(shù)據(jù)源1
* @return
*/
@Bean(name = "readDataSource0")
@ConfigurationProperties(prefix = "spring.db.read0")
public DataSource readDataSource0() {
return DataSourceBuilder.create().type(com.alibaba.druid.pool.DruidDataSource.class).build();
}
/**
* 從數(shù)據(jù)源2
* @return
*/
@Bean(name = "readDataSource1")
@ConfigurationProperties(prefix = "spring.db.read1")
public DataSource readDataSource1() {
return DataSourceBuilder.create().type(com.alibaba.druid.pool.DruidDataSource.class).build();
}
}
配置Mybaits
package com.qiang.config;
import com.qiang.config.db.DataSourceType;
import com.qiang.config.db.RoutingDataSource;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.mybatis.spring.boot.autoconfigure.SpringBootVFS;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.annotation.TransactionManagementConfigurer;
import javax.sql.DataSource;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* @author gengqiang
* @date 2018/5/3
*/
@Configuration
@EnableTransactionManagement(order = 2)
@MapperScan(basePackages = {"com.qiang.demo.mapper"})
public class MybatisConfig implements TransactionManagementConfigurer, ApplicationContextAware {
private static ApplicationContext context;
/**
* 寫庫數(shù)據(jù)源
*/
@Autowired
private DataSource dataSource;
/**
* 讀數(shù)據(jù)源數(shù)量
*/
@Value("${spring.db.readsize}")
private Integer readsize;
/**
* 數(shù)據(jù)源路由代理
*
* @return
*/
@Bean
public AbstractRoutingDataSource routingDataSouceProxy() {
RoutingDataSource proxy = new RoutingDataSource(readsize);
Map<Object, Object> targetDataSources = new HashMap<>(readsize + 1);
targetDataSources.put(DataSourceType.WRITE.getType(), dataSource);
for (int i = 0; i < readsize; i++) {
DataSource d = context.getBean("readDataSource" + i, DataSource.class);
targetDataSources.put(i, d);
}
proxy.setDefaultTargetDataSource(dataSource);
proxy.setTargetDataSources(targetDataSources);
return proxy;
}
@Bean
@ConditionalOnMissingBean
public SqlSessionFactoryBean sqlSessionFactory() throws IOException {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(routingDataSouceProxy());
bean.setVfs(SpringBootVFS.class);
bean.setTypeAliasesPackage("com.qiang");
Resource configResource = new ClassPathResource("/mybatis-config.xml");
bean.setConfigLocation(configResource);
ResourcePatternResolver mapperResource = new PathMatchingResourcePatternResolver();
bean.setMapperLocations(mapperResource.getResources("classpath*:mapper/**/*.xml"));
return bean;
}
@Override
public PlatformTransactionManager annotationDrivenTransactionManager() {
return new DataSourceTransactionManager(routingDataSouceProxy());
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
if (context == null) {
context = applicationContext;
}
}
}
其中實現(xiàn)數(shù)據(jù)源切換的功能就是自定義一個類擴展AbstractRoutingDataSource抽象類,就是代碼中的定義的RoutingDataSource,其實該相當(dāng)于數(shù)據(jù)源DataSourcer的路由中介,可以實現(xiàn)在項目運行時根據(jù)相應(yīng)key值切換到對應(yīng)的數(shù)據(jù)源DataSource上。
RoutingDataSource.class
package com.qiang.config.db;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 數(shù)據(jù)源路由
*
* @author gengqiang
*/
public class RoutingDataSource extends AbstractRoutingDataSource {
private AtomicInteger count = new AtomicInteger(0);
private int readsize;
public RoutingDataSource(int readsize) {
this.readsize = readsize;
}
@Override
protected Object determineCurrentLookupKey() {
String typeKey = DataSourceContextHolder.getJdbcType();
if (typeKey == null) {
logger.error("無法確定數(shù)據(jù)源");
}
if (typeKey.equals(DataSourceType.WRITE.getType())) {
return DataSourceType.WRITE.getType();
}
//讀庫進行負載均衡
int a = count.getAndAdd(1);
int lookupkey = a % readsize;
return lookupkey;
}
}
其中用到了2個輔助類
package com.qiang.config.db;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 全局數(shù)據(jù)源
*
* @author gengqiang
*/
public class DataSourceContextHolder {
private static Logger logger = LoggerFactory.getLogger(DataSourceContextHolder.class);
private final static ThreadLocal<String> local = new ThreadLocal<>();
public static ThreadLocal<String> getLocal() {
return local;
}
public static void read() {
logger.debug("切換至[讀]數(shù)據(jù)源");
local.set(DataSourceType.READ.getType());
}
public static void write() {
logger.debug("切換至[寫]數(shù)據(jù)源");
local.set(DataSourceType.WRITE.getType());
}
public static String getJdbcType() {
return local.get();
}
}
package com.qiang.config.db;
/**
* @author gengqiang
*/
public enum DataSourceType {
READ("read", "讀庫"), WRITE("write", "寫庫");
private String type;
private String name;
DataSourceType(String type, String name) {
this.type = type;
this.name = name;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
最后通過aop設(shè)置切面,攔截讀寫來動態(tài)的設(shè)置數(shù)據(jù)源
package com.qiang.config.aop;
import com.qiang.config.db.DataSourceContextHolder;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
/**
* 攔截數(shù)據(jù)庫讀寫
*
* @author gengqiang
*/
@Aspect
@Component
@Order(1)
public class DataSourceAspect {
Logger logger = LoggerFactory.getLogger(getClass());
@Before("execution(* com.qiang..*.*ServiceImpl.find*(..)) " +
"|| execution(* com.qiang..*.*ServiceImpl.count*(..))" +
"|| execution(* com.qiang..*.*ServiceImpl.sel*(..))" +
"|| execution(* com.qiang..*.*ServiceImpl.get*(..))"
)
public void setReadDataSourceType() {
logger.debug("攔截[read]方法");
DataSourceContextHolder.read();
}
@Before("execution(* com.qiang..*.*ServiceImpl.insert*(..)) " +
"|| execution(* com.qiang..*.*ServiceImpl.save*(..))" +
"|| execution(* com.qiang..*.*ServiceImpl.update*(..))" +
"|| execution(* com.qiang..*.*ServiceImpl.set*(..))" +
"|| execution(* com.qiang..*.*ServiceImpl.del*(..))")
public void setWriteDataSourceType() {
logger.debug("攔截[write]操作");
DataSourceContextHolder.write();
}
}
主要的代碼就寫好了,下面來測試一下是否讀寫分離。
寫一個測試類:
package com.qiang;
import com.qiang.demo.entity.Area;
import com.qiang.demo.service.AreaService;
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;
/**
* @author gengqiang
* @date 2018/5/4
*/
@RunWith(SpringRunner.class)
@SpringBootTest
public class TestApplication {
@Autowired
private AreaService areaService;
@Test
public void test() {
Area area = new Area();
area.setDistrictId("0000");
area.setName("test");
area.setParentId(0);
area.setLevel(1);
areaService.insert(area);
}
@Test
public void test2() {
areaService.selectByPrimaryKey(1);
}
}
其中第一個測試插入數(shù)據(jù),第二個測試查詢。
第一測試結(jié)果:

第二個測結(jié)果:

從結(jié)果看出來第一個走的寫數(shù)據(jù)源,就是主數(shù)據(jù)源,第二個的走讀數(shù)據(jù)源,就是從數(shù)據(jù)源。
然后我們在測試一下事物,看遇到異常是否會滾。
測試:
@Test
public void contextLoads() throws Exception {
try {
areaService.insertBack();
} catch (Exception e) {
// e.printStackTrace();
}
System.out.println(areaService.count(new Area()));
}
其中service:
@Override
@Transactional(rollbackFor = Exception.class)
public void insertBack() {
Area area = new Area();
area.setDistrictId("0000");
area.setName("test");
area.setParentId(0);
area.setLevel(1);
mapper.insert(area);
throw new RuntimeException();
}
方法上加@Transactional,聲明一個事物。

看一下運行結(jié)果,雖然運行插入的時候,sql是運行了,但最后查詢的時候數(shù)量為0,說明會滾了。
配置事物

第一步需要加一個注解@EnableTransactionManagement,后面的參數(shù)是為了區(qū)分aop和事物執(zhí)行的順序。
然后在需要會滾的方法上加一個注解@Transactional。
以上這篇使用springboot aop來實現(xiàn)讀寫分離和事物配置就是小編分享給大家的全部內(nèi)容了,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
SpringBoot?@Transactional事務(wù)不生效排查方式
這篇文章主要介紹了SpringBoot?@Transactional事務(wù)不生效排查方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-01-01
基于Mybatis Plus實現(xiàn)代碼生成器CodeGenerator
這篇文章主要介紹了基于Mybatis Plus實現(xiàn)代碼生成器CodeGenerator,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-08-08
java開發(fā)中基于JDBC連接數(shù)據(jù)庫實例總結(jié)
這篇文章主要介紹了java開發(fā)中基于JDBC連接數(shù)據(jù)庫的方法,以實例形式較為詳細的總結(jié)分析了Java使用JDBC的具體步驟與注意事項,并附帶了一個完整實例加以說明,需要的朋友可以參考下2015-11-11

