SaaS平台切换数据源实现原理
买泛域名SSL证书 送5斤装现摘猕猴桃一箱、同时提供技开源商城搭建免费技术支持。
泛域名ssl证书 239元1年送1个月、单域名39元1年,Sectigo(原Comodo证书)全球可信证书,强大的兼容性,高度安全性,如有问题7天内可退、可开发票
加微信VX 18718058521 备注SSL证书
【腾讯云】2核2G4M云服务器新老同享99元/年,续费同价
泛域名ssl证书 239元1年送1个月、单域名39元1年,Sectigo(原Comodo证书)全球可信证书,强大的兼容性,高度安全性,如有问题7天内可退、可开发票
加微信VX 18718058521 备注SSL证书
【腾讯云】2核2G4M云服务器新老同享99元/年,续费同价
前言 :SaaS平台的实现模式有3种 简单来说分别是 分字段 分表 分库
这里作者展示的是隔离性最高的高的分库 当然对应的服务器成本也更高
如图所示 总公司能够创建数据库并分账号
创建数据库这里展示一下核心代码
1.测试数据是否能正常连接
public boolean testConnect(String ipAndPort, String username, String password) { final String defaultDatabaseName = "test"; String url = new StringBuilder("jdbc:mysql://").append(ipAndPort).append("/").append(defaultDatabaseName).toString(); Connection conn = null; try { System.out.println("连接数据库"); //连接数据库 conn = DataSourceBuilder.create().url(url).driverClassName(JDBCDriver).username(username).password(password).build().getConnection(); } catch (SQLException se) { se.printStackTrace(); return false; } catch (Exception e) { e.printStackTrace(); return false; } finally { try { if (conn != null) conn.close(); } catch (SQLException se) { se.printStackTrace(); return false; } } return true;}
2开始创建数据库并插入默认数据
因为数据太多了这里省略N张表 直接看方法
public boolean createDatabases(Integer tenantId, String ipAndPort, String username, String password, String databaseName, String systemAccount, String systemPassword, String tenantName) { Connection conn = null; Statement stmt = null; PreparedStatement ps = null; try { String url = new StringBuilder("jdbc:mysql://").append(ipAndPort).append("/").toString(); System.out.println("连接数据库"); //连接数据库 DataSource dataSource = DataSourceBuilder.create().url(url).driverClassName(JDBCDriver).username(username).password(password).build(); conn = dataSource.getConnection(); //获取执行的SQL的对象 stmt = conn.createStatement(); String sql = new StringBuilder().append("CREATE DATABASE").append(" ").append(databaseName).toString(); stmt.executeUpdate(sql); System.out.println("数据库创建成功"); stmt.close(); conn.close(); url = new StringBuilder("jdbc:mysql://").append(ipAndPort).append("/").append(databaseName).toString(); //连接数据库 dataSource = DataSourceBuilder.create().url(url).driverClassName(JDBCDriver).username(username).password(password).build(); conn = dataSource.getConnection(); stmt = conn.createStatement(); String creatsql = "CREATE TABLE sys_user (" + "id int(11) NOT NULL AUTO_INCREMENT COMMENT '主键id'," + "user_name varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '用户名'," + "user_pwd varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '密码'," + "nick varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '昵称'," + "img_url varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '头像地址'," + "create_time datetime(0) NULL DEFAULT NULL COMMENT '创建时间'," + "PRIMARY KEY (id) USING BTREE," + "UNIQUE INDEX user_name(user_name) USING BTREE" + ") ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = DYNAMIC;"; stmt.executeLargeUpdate(creatsql); String creatsql1 = "CREATE TABLE tbl_sys_user_role_relation (" + "id bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键id'," + "user_id bigint(20) NULL DEFAULT NULL COMMENT '系统用户id'," + "role_id bigint(20) NULL DEFAULT NULL COMMENT '角色id'," + "PRIMARY KEY (id) USING BTREE" + ") ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '后台用户和角色关系表' ROW_FORMAT = DYNAMIC;"; stmt.executeLargeUpdate(creatsql1); ps = conn.prepareStatement(sql); ps.setInt(1, 32); ps.setInt(2, 30); ps.setString(3, "资源管理"); ps.setString(4, "pms:permission"); ps.setInt(5, 1); ps.setString(6, "permissionManager"); ps.setInt(7, 1); ps.setTimestamp(8, new java.sql.Timestamp(System.currentTimeMillis())); ps.setInt(9, 32); result = ps.executeUpdate();// 返回值代表收到影响的行数 sql = "INSERT INTO tbl_role(id, name, description, admin_count, create_time, status, sort) VALUES (1, '超级管理员', '拥有所有查看和操作功能', 0,'2020-02-02 00:00:01', 1, 0);"; ps = conn.prepareStatement(sql); result = ps.executeUpdate();// 返回值代表收到影响的行数 sql = "INSERT INTO tbl_sys_user_role_relation(id, user_id, role_id) VALUES (1, 1, 1);"; ps = conn.prepareStatement(sql); result = ps.executeUpdate();// 返回值代表收到影响的行数 sql = "INSERT INTO tbl_role_permission_relation(id, role_id, permission_id) VALUES (1, 1, 30);"; ps = conn.prepareStatement(sql); result = ps.executeUpdate();// 返回值代表收到影响的行数 sql = "INSERT INTO tbl_role_permission_relation(id, role_id, permission_id) VALUES (2, 1, 31);"; ps = conn.prepareStatement(sql); result = ps.executeUpdate();// 返回值代表收到影响的行数 sql = "INSERT INTO tbl_role_permission_relation(id, role_id, permission_id) VALUES (3, 1, 32);"; ps = conn.prepareStatement(sql); result = ps.executeUpdate();// 返回值代表收到影响的行数 System.out.println("插入成功" + username); // 添加到map中 DynamicDataSource dynamicDataSource = (DynamicDataSource) SpringContextUtils.getBean("dynamicDataSource"); Map<Object, Object> dataSourceMap = new HashMap<>(); HikariDataSource master = (HikariDataSource) SpringContextUtils.getBean("master"); HikariDataSource newDataSource = new HikariDataSource(); newDataSource.setDriverClassName(JDBCDriver); newDataSource.setJdbcUrl(url + "?useUnicode=true&characterEncoding=utf-8&autoReconnect=true&useSSL=false&serverTimezone=Asia/Shanghai"); newDataSource.setUsername(username); newDataSource.setPassword(password); newDataSource.setDataSourceProperties(master.getDataSourceProperties()); dataSourceMap.put(String.valueOf(tenantId), newDataSource); // 设置数据源 dynamicDataSource.setDataSources(dataSourceMap); /** * 必须执行此操作,才会重新初始化AbstractRoutingDataSource 中的 resolvedDataSources,也只有这样,动态切换才会起效 */ dynamicDataSource.afterPropertiesSet(); System.out.println("新增数据源添加到dataSourceMap成功!"); } catch (SQLException se) { se.printStackTrace(); return false; } catch (Exception e) { e.printStackTrace(); return false; } finally { try { if (stmt != null) stmt.close(); if (ps != null) ps.close(); if (conn != null) conn.close(); } catch (SQLException se) { se.printStackTrace(); return false; } } return true; }
重点来了 如何动态切换数据源
项目启动时将数据源放入在map中 交给spring管理 每个账号登入时都会得到一个租户ID 在调用接口的时候 将租户Id 传入Head 中 通过AOP 环绕增强@Around在切换数据源 如图二所示
aop 环绕增强的核心代码
// 监听 app接口@Around("execution(* com.spring.security.demo.controller.app.*.*(..))")public Object appAround(ProceedingJoinPoint jp) throws Throwable { ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); Object result=""; try { HttpServletRequest request = sra.getRequest(); String tenantId = request.getHeader("tenantId"); log.info("当前租户+tenantId"+tenantId); DynamicDataSourceContextHolder.setDataSourceKey(tenantId); result = jp.proceed(); }catch (Exception e){ e.printStackTrace(); result ="系统异常"; }finally { DynamicDataSourceContextHolder.clearDataSourceKey(); } return result;}