设计思路
一、核心问题
1、数据源信息的存储
2、数据源信息的动态同步、加载与缓存
3、数据源信息的切换
4、数据源对象的注入
二、问题分析
1、数据源信息的存储
(1)在分库分表的基础上,需要单独维护一套DBConnection的配置信息,然后组装成DataSource(以DruidDataSource为例)对象缓存起来。
(2)缓存的设计。通常情况下,动态数据源切换都会使用一个标识来选择对应的数据源对象。而且考虑到线程安全的问题,推荐使用currentHashMap。
2、数据源信息的同步与加载
(1)同步问题:在分布式的前提下,需要考虑到数据的一致性的问题。如果做的比较low的话,可以使用缓存,如果使用缓存的情况下,还需要注意缓存数据一致性的问题。LocalCache+ZooKeeper为例。
- 使用ZK,监听数据源变化,保持分布式系统数据的一致性。
(2)数据源的加载:在Spring容器的帮助下,这个操作变得比较随意。通过几个注解即可实现。主要描述流程思路。
- 通常情况下,要在访问DB前将数据源对象创建出来。
(3)缓存的问题
- 使用本地缓存的刷新问题:基于订阅-发布模型的监听(广播)机制。ZK、MQ、Redis都可以。如果想做的牛逼一点,可以考虑到冷热数据的问题。充分利用内存空间。可以借鉴LRU内存淘汰算法。
- Redis缓存:
- 使用这个方案的话,就不需考虑分布式系统数据一致性的问题。但是需要考虑缓存穿透和缓存数据一致性的问题。通常情况下,DB路由数据变更频率不大,凭借着Redis的QPS,应该是可以支撑起频繁的数据访问请求。如果,DB路由变更,势必要更新缓存(删Key)
- 缓存穿透的问题:并发量高的情况下,key被删除,势必会引起缓存穿透的问题。
(1)分布式锁策略:使用分布式锁限制访问DB的线程数,这样的话肯定要阻塞线程,导致超时。通常这种方案在业务上是不可取的。
(2)数据一致性策略:保证DB能承受住访问的压力(其实还好,谁家DB还扛不住几百的绝对并发?);引入数据版本机制,即在更新缓存时,判断当前数据的版本号与缓存数据的版本号是否一致。
3、数据源的切换
- 首先需要考虑的就是线程安全的问题。这里可以使用简单粗暴的手段,ThreadLocal
public class DataSourceContextHolder {
private static final ThreadLocal<String> contextHolder= new ThreadLocal<String>();
public static void setDataSourceType(String groupId){
contextHolder.set(groupId);
}
public static String getDataSourceType(){
log.info("Service get datasource currently is : [" + contextHolder.get() + "]");
return contextHolder.get();
}
public static void clearDataSourceType(){
contextHolder.remove();
}
}
4、数据源对象的注入
思路:从动态数据源工具中获取到DataSource标示,根据标示获取DataSrouce对象。在Spring容器中,这一点其实更好实现。原理就是在DB访问之前,切换数据源。