读写分离: 为保证数据库数据的一致性,我们要求所有对于数据库的更新操作都是针对主数据库的,但是读操作是可以针对从数据库来进行。大多数站点的数据库读操作比写操作更加密集,而且查询条件相对复杂,数据库的大部分性能消耗在查询操作上了。
主从复制数据是异步完成的,这就导致主从数据库中的数据有一定的延迟,在读写分离的设计中必须要考虑这一点。以博客为例,用户登录后发表了一篇文章,他需要马上看到自己的文章,但是对于其它用户来讲可以允许延迟一段时间(1分钟/5分钟/30分钟),不会造成什么问题。这时对于当前用户就需要读主数据库,对于其他访问量更大的外部用户就可以读从数据库。
适当放弃一致性:在一些实时性要求不高的场合,我们适当放弃一致性要求。这样就可以充分利用多种手段来提高系统吞吐量,例如页面缓存、分布式数据缓存、数据库读写分离、查询数据搜索索引化。
可以通过程序控制,将强一致性要求的功能(比如存钱、取钱)的读写操作均指向主数据库,或者将写操作采用“双写”的方式实现;而弱一致性(最终一致性)要求的功能(比如更新微博(写)、金融查询账户(读))实现读写分离。
理解Sharding jdbc原理
Sharding jdbc是基于JDBC协议实现的,当我们获得dataSource时,这个dataSource是Sharding jdbc自己定义的一个SpringShardingDataSource类型的数据源,该数据源在返回getConnection()及prepareStatement()时,分别返回ShardingConnection和ShardingPreparedStatement的实例对象。
实现步骤
- sql解析。Sharding jdbc使用阿里的Druid库解析sql。在这个过程中,Sharding jdbc实现了一个自己的sql解析内容缓存容器SqlBuilder。当语法分析中解析到一个表名的时候,在SqlBuilder中缓存一个sql相关的逻辑表名的token。并且,Sharding jdbc会将sql按照语义解析为多个segment。例如,”select id, name, price, publish, intro from book where id = ?”将解析为,”select id, name, price, publish, intro | from | book | where | id = ?”。
- 分库分表路由。根据解析上下文匹配用户对这句 SQL 所涉及的库和表配置的分片策略,并根据分片策略生成路由后的 SQL。路由后的 SQL 有一条或多条,每一条都对应着各自的真实物理分片。
- sql改写。在SqlBuilder中,查找sql中解析的segment,将和逻辑表名一致的segment替换成实际表名。(segment中可以标注该地方是不是表名)
- sql多线程执行,通过多线程执行器异步执行路由和改写之后得到的 SQL 语句。
结果归并,将多个执行结果集归并以便于通过统一的 JDBC 接口输出。
动态数据源方案
当一个客户端请求过来,会调用impl包下的service实现类,aop通过扫描实现类中方法上的@DataSource注解,如果没有该注解,则采用默认的写数据源;如果有该注解,则获取注解中的value值,并且set进去ThreadLocal。接着去获取数据源,继承AbstractRoutingDataSource,并重写determineCurrentLookupKey()方法。
每次访问数据库,都会调用getConnection()方法,去获取数据库连接,该方法里面调用了determineTargetDataSource()方法,然后在determineTargetDataSource()方法里面调用了AbstractRoutingDataSource类里的抽象方法determineCurrentLookupKey()。这时候我们需要重写该抽象方法来通过ThreadLocal获取当前的数据库类型的标识(write或者read),从而决定采用哪种数据库。AbstractRoutingDataSource会根据当前的数据源类型,取出对应的数据源,从而执行SQL语句。public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return CustomerContextHolder.getCustomerType();
}
}
public class CustomerContextHolder {
public static final String DATA_SOURCE_A = "dataSource";
public static final String DATA_SOURCE_B = "dataSource2";
private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();
public static void setCustomerType(String customerType) {
contextHolder.set(customerType);
}
public static String getCustomerType() {
return contextHolder.get();
}
public static void clearCustomerType() {
contextHolder.remove();
}
}
参考资料
- 分库分表之 Sharding-JDBC 中间件,看这篇真的够了!
- Mysql 主从复制配置和程序读写分离配置
- 如何保证主从复制数据一致性
- 如何解决主从同步的数据一致性问题?
- MySQL主从复制属于集群技术还是负载均衡技术?
- 详解利用Spring的AbstractRoutingDataSource解决多数据源的问题