javax.sql.DataSource
java
javax.sql.XADataSource
mysql
org.springframework.jdbc.datasource.embedded,EnbeddedDataSource
spring
數據庫鏈接池sql
Apache Commons DBCP數據庫
Tomcat DBCPapp
實際生產中極可能會出現.ide
Spring 中的事務經過PlatformTransactionManagere
管理, 使用 AOP 實現, 具體實現類是 TransactionInterceptor
.spring-boot
事務傳播(propagation)與保護點(savepoint): 兩者密切相關.微服務
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.url=jdbc:mysql://localhost:3306/product?serverTimezone=Hongkong&useUnicode=true&characterEncoding=utf8&useSSL=false spring.datasource.username=xlx spring.datasource.password=xlx
注意serverTimezone
的配置, 亞洲通常配置爲 Hongkong
.ui
DataSource
@Repository public class ProductRepository { @Autowired private DataSource dataSource; ... }
spring.ds1.name=master spring.ds1.driver-class-name=com.mysql.cj.jdbc.Driver spring.ds1.url=jdbc:mysql://localhost:3306/product?serverTimezone=Hongkong&useUnicode=true&characterEncoding=utf8&useSSL=false spring.ds1.username=xlx spring.ds1.password=xlx spring.ds2.name=slave spring.ds2.driver-class-name=com.mysql.cj.jdbc.Driver spring.ds2.url=jdbc:mysql://localhost:3306/product?serverTimezone=Hongkong&useUnicode=true&characterEncoding=utf8&useSSL=false spring.ds2.username=xlx spring.ds2.password=xlx
AbstractRoutingDataSource
抽象類並實現方法determineCurrentLookupKey()
這個類就是須要使用的數據源類型.
public class DynamicDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { return DbTypeContextHolder.get(); } }
查看 AbstractRoutingDataSource
的源碼能夠發現, 其定義了一個 Map<Object, Object> targetDataSources;
用來保存多數據源, 而上面重寫的方法返回的就是須要使用的數據源的key.
public enum DBType { MASTER, SLAVE }
DbTypeContextHolder
其中使用一個 ThreadLocal
來保存key, 三個方法都必不可少. 分別用來獲取, 設置, 清除.@Slf4j public class DbTypeContextHolder { private final static ThreadLocal<DBType> holder = new ThreadLocal<DBType>(); private final static ThreadLocal<Boolean> alwaysWrite = new ThreadLocal<Boolean>(); public static DBType get() { if (alwaysWrite.get() != null && alwaysWrite.get()) { log.info("將強制使用Write"); return DBType.MASTER; } log.info("獲取到: "+holder.get().name()); return holder.get(); } public static void set(DBType dbType) { log.info("設置爲: "+dbType.name()); holder.set(dbType); } public static void clean() { log.info("清除..."); holder.remove(); } public static void setAlwaysWrite() { log.info("設置強制Write"); alwaysWrite.set(true); } public static void cleanAlwaysWrite() { log.info("清除強制Write"); clean(); alwaysWrite.remove(); } }
MultiDataSourceConfiguration
@Configuration public class MultiDataSourceConfiguration { @Autowired Environment environment; @Bean public DataSource getDynamicDataSource(){ DynamicDataSource dynamicDataSource = new DynamicDataSource(); Map<Object,Object> map = new HashMap<>(); for (int i = 1; i < 3 ; i++) { String name = environment.getProperty("spring.ds"+String.valueOf(i)+".name"); if (name==null) break; DataSource dataSource = DataSourceBuilder.create() .driverClassName(environment.getProperty("spring.ds"+String.valueOf(i)+".driver-class-name")) .username(environment.getProperty("spring.ds"+String.valueOf(i)+".username")) .url(environment.getProperty("spring.ds"+String.valueOf(i)+".url")) .password(environment.getProperty("spring.ds"+String.valueOf(i)+".password")) .build(); if (name.trim().toUpperCase().equals("MASTER")){ dynamicDataSource.setDefaultTargetDataSource(dataSource); map.put(DBType.MASTER,dataSource); }else{ map.put(DBType.SLAVE,dataSource); } } dynamicDataSource.setTargetDataSources(map); return dynamicDataSource; } }
public Boolean save(Product product) throws SQLException { System.out.println("save product : "+product); DbTypeContextHolder.set(DBType.MASTER); System.out.println(dataSource.getConnection()); DbTypeContextHolder.clean(); DbTypeContextHolder.set(DBType.SLAVE); System.out.println(dataSource.getConnection()); DbTypeContextHolder.clean(); return true; }
結果以下,說明是兩個不一樣的數據源:
HikariProxyConnection@1667860231 wrapping com.mysql.cj.jdbc.ConnectionImpl@78794b01 HikariProxyConnection@556437990 wrapping com.mysql.cj.jdbc.ConnectionImpl@314c0916
能夠定義切面來自動切換數據源. 好比普通的讀寫分離.
讀寫分離是經過定義在倉儲層方法上的規則來實現, 同時也定義了在服務層必須使用寫庫來讀取數據的方式.
除此以外, 若是使用 @Transactional
, 由於其也是經過AOP方式實現, 因此與自動切換的AOP存在順序關係, 爲了能在 @Transactional
事務前設置好數據源, 須要增長 @Order(0)
.
@Aspect @Component @Order(0) public class DataSourceAspect { // 必須使用寫庫的 @Pointcut("execution(* com.xlx.product.repository.repo..*.save*(..)) || execution(* com.xlx.product.repository.repo..*.insert*(..)) || execution(* com.xlx.product.repository.repo..*.update*(..))") public void write(){} @Before("write()") public void beforeWrite(JoinPoint joinPoint){ System.out.println("beforeWrite()"+joinPoint.getSignature().getName()); DbTypeContextHolder.set(DBType.MASTER); } @After("write()") public void afterWrite(JoinPoint joinPoint){ DbTypeContextHolder.clean(); } // 必須使用讀庫的 @Pointcut("execution(* com.xlx.product.repository.repo..*.get*(..))|| execution(* com.xlx.product.repository.repo..*.select*(..))||execution(* com.xlx.product.repository.repo..*.count*(..))") public void read(){} @Before("read()") public void beforeRead(JoinPoint joinPoint){ DbTypeContextHolder.set(DBType.SLAVE); } @After("read()") public void afterRead(JoinPoint joinPoint){ DbTypeContextHolder.clean(); } // 服務層有註解的方法特殊處理 @Pointcut("@annotation(org.springframework.transaction.annotation.Transactional)" + "||@annotation(com.xlx.product.repository.annotation.DBTypeAnnotation)") public void readWrite(){} @Before("readWrite()") public void before(JoinPoint joinPoint){ MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature(); if(methodSignature.getMethod().isAnnotationPresent(Transactional.class)) DbTypeContextHolder.set(DBType.MASTER); if(methodSignature.getMethod().isAnnotationPresent(DBTypeAnnotation.class)) DbTypeContextHolder.setAlwaysWrite(); } @After("readWrite()") public void after(JoinPoint joinPoint){ DbTypeContextHolder.clean(); DbTypeContextHolder.cleanAlwaysWrite(); } }
/** * 放置在方法上的註解, 用來指定使用的數據源類型, 默認使用主數據源 */ @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface DBTypeAnnotation { @AliasFor("values") DBType dbType() default DBType.MASTER; @AliasFor("dbType") DBType values() default DBType.MASTER; }
@Service @Slf4j public class ProductService { @Autowired ProductRepository repository; public String findAndSave(){ log.info("findAndSave() 方法開始....."); String rs = testFunc(); log.info("findAndSave() 方法結束....."); return rs; } @DBTypeAnnotation public String findAndSaveWithWrite(){ log.info("findAndSaveWithWrite() 方法開始....."); String rs = testFunc(); log.info("findAndSaveWithWrite() 方法結束....."); return rs; } private String testFunc() { List<Product> productList = repository.selectAll(); repository.saveProduct(productList.get(0)); productList = repository.selectAll(); repository.saveProduct(productList.get(0)); return "ok"; } }
@RestController public class ProductController { @Autowired ProductService productService; @GetMapping("/product/findAndSave") public String findAndSave()throws SQLException { return productService.findAndSave(); } @GetMapping("/product/findAndSaveWithWrite") public String findAndSaveWithWrite()throws SQLException { return productService.findAndSaveWithWrite(); } }