根據用戶注冊,系統自動創建私有數據庫,用戶登錄,動態添加數據源到Spring數據路由,Session超時刪除數據源
好處:當數據量大的時候,類似水平切割效果,效率會高一些
壞處:數據源切換,Spring 事務處理比較繁瑣,數據連接處理不好會有很大消耗,如果涉及后台系統管理數據,也比較繁瑣.
使用Spring數據源路由,現在好像沒有直接添加數據源的方法,無奈之下只能用反射.
用戶登錄成功時,在Spring Security UserDetailService.loadUserByUsername 里面添加用戶數據源
/** * 加入用戶數據源 */ routingDataSource.addDataSource(userid);
/** * 根據用戶創建數據源 */ public void addDataSource(String userid) { if (StringUtils.isBlank(userid)) return; DbInfo dbInfo = getDbInfoService().getDbInfoByUserId(userid); try { Field targetDataSources = AbstractRoutingDataSource.class.getDeclaredField("targetDataSources"); Field resolvedDataSources = AbstractRoutingDataSource.class.getDeclaredField("resolvedDataSources"); targetDataSources.setAccessible(true); resolvedDataSources.setAccessible(true); Map<Object, Object> dataSources = (Map<Object, Object>) targetDataSources.get(this); if (dataSources.get(userInfo.getId().toString()) != null) return; Map<Object, DataSource> dataSources2 = (Map<Object, DataSource>) resolvedDataSources.get(this); DruidDataSource dds = new DruidDataSource(); dds.setUrl("jdbc:mysql://" + dbInfo.getDbaddr() + ":" + dbInfo.getDbport() + "/" + dbInfo.getDbname() + "?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&transformedBitIsBoolean=true&useSSL=true"); dds.setUsername(dbInfo.getUsername()); dds.setPassword(dbInfo.getPwd()); dataSources.put(userid, dds); dataSources2.put(userid, dds); } catch (NoSuchFieldException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } }
加入了數據源,當然需要刪除,可以在Session監聽器里面,銷毀Session的時候刪除
/** * 根據用戶刪除數據源 */ public void removeDataSource(String userid) { if (StringUtils.isBlank(userid)) return; try { Field targetDataSources = AbstractRoutingDataSource.class.getDeclaredField("targetDataSources"); Field resolvedDataSources = AbstractRoutingDataSource.class.getDeclaredField("resolvedDataSources"); targetDataSources.setAccessible(true); resolvedDataSources.setAccessible(true); Map<Object, Object> dataSources = (Map<Object, Object>) targetDataSources.get(this); if (dataSources.get(userInfo.getUsrno()) != null) { Map<Object, DataSource> dataSources2 = (Map<Object, DataSource>) resolvedDataSources.get(this); dataSources.remove(userid); dataSources2.remove(userid); } } catch (NoSuchFieldException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } }
注解加Aop 切換數據源
注解
/** * Created by 為 . * 根據當前用戶切換數據源 */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface SwitchDataSource { }
Spring AOP,新版本的SpringAOP 可以很好切入監聽器,因為監聽器可以被Spring容器管理了,變相加強了SpringAop,這樣就不需要使用原生Aspectj了
/** * Created by 為 on 2017-4-27. */ @Component @Aspect @Order(0)//配置Spring注解事務時,在事務之前切換數據源 public class SwitchDataSourceAspectj { //定義切點 @Pointcut("@annotation(com.lzw.common.annotation.SwitchDataSource)") public void switchDataSource(){} @Around("switchDataSource()") public Object arounduserDataSource(ProceedingJoinPoint joinPoint){ DataSourceContextHolder.user(); try { return joinPoint.proceed(); } catch (Throwable throwable) { throwable.printStackTrace(); }finally { DataSourceContextHolder.write(); } return null; } }
這樣可以在方法上添加注解切換數據源(注意事務與切換數據源的注解順序),不過如果在一個方法中需要多次切換到不同數據源查詢數據,會消耗很多連接數,為了更好控制數據庫連接數,需要使用Spring事務
編程式Spring事務
注入TransactionManager
@Resource private PlatformTransactionManager platformTransactionManager;
開始事務處理,每個用戶單獨數據庫,訪問量不大,所以沒有配置連接池,每次重新獲取連接性能比較低,開啟事務是為了數據庫連接重用
//為了節省連接數,盡可能在一次切換里獲取需要的數據 DataSourceContextHolder.user(); //TransactionTemplate 必須每次new出來,不能使用Spring單例注入,設置的數據會一直存在. TransactionTemplate transactionTemplate = new TransactionTemplate(platformTransactionManager); transactionTemplate.setPropagationBehavior(Propagation.REQUIRES_NEW.value()); transactionTemplate.execute(new TransactionCallbackWithoutResult() { @Override public void doInTransactionWithoutResult(TransactionStatus status) { //數據庫操作代碼 } }); DataSourceContextHolder.write();