多库使用
About 2650 wordsAbout 9 min
1984-01-24
一个系统随着规模增加,会从单一数据库变成多数据库,包含如下变化
1)业务库变成主从库.
2)业务拆分多个库,比如拆分成客户库,订单库,商品库。
3) 多租户,包括常见的单表多租户,以及其他多租户方案,多表多租户,多schema多租户,多库多租户,多异构库多租户
4)数据通过消息,数据库日志等方式同步到大数据系统,涉及到交易数据使用传统数据库,涉及到查询的使用大数据系统支持sql查询,
5)以上几种混合形式
6)业务拆分多个库,可以使用ShardingSpher等中间件查询
BeetlSQL3 能支持如上所有场景,其中 3 多租户场景将在《多租户》中专门说明. 本章节重点是1,2,4,5,6
本章例子来源于源码 S6MoreDatabase或者单元MoreDatabaseTest,或者SpringBoot下提供所有的例子
如果你用的是Spring Boot ,可以参考 https://gitee.com/xiandafu/springboot3-beetl-beetlsql-example ,包含了多库,以及多租户例子
业务主从库
SQLManager的数据源,如果只提供一个,则认为读写均操作此数据源,如果提供多个,则默认第一个为写库,其他为读库。用户在开发代码的时候,无需关心操作的是哪个数据库,因为调用SQLMnager 的 select相关api的时候,总是去读取从库,add/update/delete 的时候,总是读取主库(如下是主从实现原理,大部分情况下无需关心如何实现)
sqlManager.insert(User.class,user) // 操作主库,如果只配置了一个数据源,则无所谓主从
sqlManager.unique(id,User.class) //读取从库
主从库的逻辑是由ConnectionSource来决定的,如下DefaultConnectionSource 的逻辑
@Override
public Connection getConn(ExecuteContext ctx,boolean isUpdate){
if(this.slaves==null||this.slaves.length==0) return this.getWriteConn(ctx);
if(isUpdate) return this.getWriteConn(ctx);
else return this.getReadConn(ctx);
}
如果你有一主多从数据源,可以通过ConnectionSourceHelper 构造一个支持主从的ConnectionSource(不同的框架,BeetlSQL提供了不通的构造方式,ConnectionSourceHelper可以作为一个示例使用)
//为了简单起见,主从库都走同一个数据库
DataSource master = SampleHelper.datasource();
DataSource slave1 = SampleHelper.datasource();
DataSource slave2 = SampleHelper.datasource();
//一主二从
ConnectionSource source = ConnectionSourceHelper.getMasterSlave(master,new DataSource[]{slave1,slave2});
SQLManagerBuilder builder = new SQLManagerBuilder(source);
builder.setNc(new UnderlinedNameConversion());
builder.setInters(new Interceptor[]{new DebugInterceptor()});
builder.setDbStyle(new MySqlStyle());
SQLManager sqlManager = builder.build();
如果你是Spring或者SpringBoot框架,则可以简单指定多个数据源,以SpringBoot为例子
beetlsql = sqlManager1
beetlsql.sqlManager1.ds=ds1,salve1,salve2
beetlsql.sqlManager1.basePackage=org.beetl.sql.springboot.simple
这里ds1和slave1,slave2 是数据源的名称。
如果是Spring,配置如下:
<bean id="sqlManagerFactoryBean" class="org.beetl.sql.ext.spring.SqlManagerFactoryBean">
<property name="cs" >
<bean class="org.beetl.sql.ext.spring.SpringConnectionSource">
<property name="masterSource" ref="dataSource-master"></property>
<property name="slaveSource">
<list>
<ref bean="dataSource-slave-1"></ref>
<ref bean="dataSource-slave-2"></ref>
</list>
</property>
</bean>
</property>
<!-- 其他配置 -->
</bean>
对于不同的ConnectionSource 完成逻辑不一样,对于spring,jfinal这样的Web框架**,如果sqlManager在事务环境里,总是操作主数据库**,无论是否是读写操作,考虑到主从复制的延迟,这样有利于保持数据一致性。如果是只读事务环境 则才会操作从数据库。如果没有事务环境,则根据sql是查询还是更新来决定。
如下是SpringConnectionSource 提供的主从逻辑
@Override
public Connection getConn(ExecuteContext ctx, boolean isUpdate){
if (this.slaves == null || this.slaves.length == 0)
return this.getWriteConn(ctx);
//如果是更新语句,也得走master
if (isUpdate){
return this.getWriteConn(ctx);
}
//在事物里都用master,除了readonly事物
boolean inTrans = TransactionSynchronizationManager.isActualTransactionActive();
if (inTrans) {
boolean isReadOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly();
if (!isReadOnly) {
return this.getWriteConn(ctx);
}
}
return this.getReadConn(ctx);
}
注意,对于使用者来说,无需关心上面说的细节,仅供要定制主从逻辑的架构师
1) 开发者可以自定义ConnectionSource来完成主从逻辑,
2)如果想强制某个操作从库,可以使用sqlManager.forceDataSource方法.
主从库延迟问题
由于主从延迟,在实际上主从架构有些问题,如下
- 用户写主库,由于延迟,用户读从库,数据确没有读到
- 如果用户只读,但前后俩次被路由到俩个从库,则数据可能不一致
先说解决第一个问题办法:
应该保证如果是一个事务内的写-读操作, 那读写都应该是主库,ConnectionSource.getConn 需要实现这个逻辑,BeetlSQL默认已经实现此逻辑,参考SpringConnectionSource 源码,根据事务属性来判断是访问主库,还是从库
如果写主库后,业务页面发起的请求,是否要查主库,这有俩个解决办法,第一个办法比较通用
通用解决办法
- 通用解决办法: ConnectionSource.getConn方法,如果 参数update为true,应该给web cookie 设置一个超时时间,所以后续的所有读操作,检测这个cookie 是否存在,如果存在,说明此会话刚写完不久,所有操作都无条件路由到主库。BeetlSQL没有内置实现设置web cookie功能,用户可以自己扩展ConnectionSource实现。 这个超时时间一句主从复制具体延时来评估,比如设置10秒。 也可用session设定一个上次更新主库时间
- 根据业务场景来决定,比如用户修改自己的profile,那凡是操作自己数据,读写必须是主库。如果是读别人数据,如查看别的用户profile,则从从库。
第二个解决办法:一般情况下是一主多从,或者从库也有自己的从库,因此需要设计路由策略,保证用户始终只访问一个从库。可以根据当前用户id或者当前IP地址等信息,hash后路由到一个确定从库,ConnectionSource目前实现是随机从库的,需要用户自己去实现路由库的策略
BeetlSQL这就要能按照如上方式,重写ConnectionSource的路由策略,即可支持,开发者需要结合自己的框架自行实现
此方案解决了同一个人用户能看到一致的数据,无法解决俩个用户,被路由到不同的从库,看到不同的数据的情况,这只分布式带来不可避免问题。
多个业务库
如果你的系统会访问不同的数据库,比如订单数据库,用户数据库,支付数据库,那你可以使用BeetlSQL提供的多个业务库解决办法
- 多个SQLManager,每个SQLManager对应了一个数据库,比如oderSqlManager,customerSqlManager
- 使用一个SQLManager,多库使用Spring的动态数据源。在操作数据库前,指定数据库
- 同方案1,将多个SQLManager合并成一个动态的SQLManager代理Proxy,在操作数据库前,指定使用哪个SQLManager
需要注意,当业务出现对多个数据库进行读写时候,Spring等Web框架不能保证分布式事物,因此BeetlSQL也不能
多个SQLManager和多个数据源
如果系统有拆分成多个业务系统,则需要为系统配置多个SQLManager,每个SQLManager关联一个业务系统的数据库,或者主从库
如下是Spring Boot 配置orderSqlManager和productSqlManager的片段
#系统有俩个sqlManager
beetlsql = orderSqlManager,productSqlManager
#订单是一主俩从
beetlsql.orderSqlManager.ds=orderDs1,orderDs2-slave,orderDs3-slave
#商品
beetlsql.productSqlManager.ds=productDs
这样,你可以在业务系统注入SQLManager
@Quilifier("orderSqlManager")
@Autowired
SQLManager orderSQLManager;
@Quilifier("productSqlManager")
@Autowired
SQLManager productSQLManager;
需要注意是,多个业务库之间操作,会导致事务不一致,这种情况在使用Spring等Web框架时候无法避免。尽量在事前检查操作不会导致SQL出问题,事后记录日志以方便修复数据一致性
一个SQLManager和使用动态数据源
如果你使用Spring,可以使用动态数据源,因此项目自有一个SQLManager,此SQLManager 使用动态数据源。这样的好处是如果你的业务操作在请求范围内都是访问一个库,那么事务将得到Spring保证(访问多个数据库的事物是无法保证的)
使用动态数据源同使用普通数据源一样
#系统有俩个sqlManager
beetlsql = mySqlManager
#订单是一主俩从
beetlsql.mySqlManager.ds=dynamicDataSource
这里的dynamicDataSource指向的一个动态数据源,可以在Spring里配置,比如俩个数据源ds1,ds2
@Bean(name = "dynamicDataSource")
public DynamicRoutingDataSource routing(@Qualifier("ds1") DataSource ds1,@Qualifier("ds2") DataSource ds2) {
DynamicRoutingDataSource ds = new DynamicRoutingDataSource();
ds.setDefaultTargetDataSource(ds1);
Map<Object,Object> map = new HashMap<>();
map.put("ds1",ds1);
map.put("ds2",ds2);
ds.setTargetDataSources(map);
return ds;
}
DynamicRoutingDataSource是开发者自己实现org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource
,可以参考 https://gitee.com/xiandafu/springboot3-beetl-beetlsql-example/tree/master/tenant/database-tenant
开发者在实现业务逻辑时候,需要指定使用哪个数据源,下面代码截取database-tenant
,每个租户使用一个数据库
@Autowired
MyCommonMapper myCommonMapper;
@Autowired
SQLManager sqlManager;
@Autowired
Routing routing;
public List<OrderLog> logs(String tenantId){
routing.db(tenantId);
return myCommonMapper.all();
}
合并SQLManager
不推荐,如果是Spring框架,建议使用动态数据源
在系统中使用ThreadLocalSQLManager,可以把多个SQLManager合并成一个,比如上个例子的订单和商品俩个SQLManager,可以做合并。使用ThreadLocalSQLManager作为代理类
如下一个Spring Service类,尽管使用了一个sqlManager和mapper,但会根据service方法的注解决定使用哪个sqlManager,如果service方法未指定,则使用默认的sqlManager
@Service
public class ThreadLocalService {
@Qualifier("proxySqlManager")
SQLManager sqlManager;
@Autowired
ThreadLocalService self;
@Autowired
UserInfoMapper mapper;
@Use("sqlManager2")
public long test2(){
return mapper.allCount();
}
@Use("sqlManager1")
public long test1(){
return mapper.allCount();
}
public long testDefault(){
return mapper.allCount();
}
}
Spring Boot 需要配置各个SQLManager,以及代理SQLManager
beetlsql.sqlManagers = proxySqlManager
# 设定proxySqlManager为threadlocal,且第一个是默认
beetlsql.proxySqlManager.threadlocal=sqlManager1,sqlManager2
beetlsql.proxySqlManager.basePackage=org.beetl.sql.springboot.threadlocal
# 配置sqlManager1,配置sqlManager2
beetlsql.sqlManager1.ds=ds1
beetlsql.sqlManager2.ds=ds2
如上配置了3个SQLManager,sqlManager1和sqlManager2常规配置,beetlsql.proxySqlManager.threadlocal
代理了sqlManager1,sqlManager2,并指定beetlsql.sqlManagers为proxySqlManager
代码例子参考Spring Boot
使用ShardingSphere
Apache ShardingSphere 定位为轻量级 Java 框架,在 Java 的 JDBC 层提供的额外服务。 它使用客户端直连数据库,以 jar 包形式提供服务,无需额外部署和依赖,可理解为增强版的 JDBC 驱动,完全兼容 JDBC 和各种 ORM 框架。
- 适用于任何基于 JDBC 的 ORM 框架,如:JPA, Hibernate, Mybatis, Spring JDBC Template 或直接使用 JDBC;
- 支持任何第三方的数据库连接池,如:DBCP, C3P0, BoneCP, HikariCP 等;
- 支持任意实现 JDBC 规范的数据库,目前支持 MySQL,PostgreSQL,Oracle,SQLServer 以及任何可使用 JDBC 访问的数据库。
因此,它对于任何ORM框架都是透明的,因此BeetSQL可以无缝集成。