本文源码: GitHub·点这里 || GitEE·点这里
成都创新互联公司坚持“要么做到,要么别承诺”的工作理念,服务领域包括:成都网站制作、成都网站建设、企业官网、英文网站、手机端网站、网站推广等服务,满足客户于互联网时代的渌口网站设计、移动媒体设计的需求,帮助企业找到有效的互联网解决方案。努力成为您成熟可靠的网络建设合作伙伴!
在相对复杂的应用服务中,配置多个数据源是常见现象,例如常见的:配置主从数据库用来写数据,再配置一个从库读数据,这种读写分离模式可以缓解数据库压力,提高系统的并发能力和稳定性,执行效率。
在处理这种常见问题,要学会查询服务基础框架的API,说直白点就是查询Spring框架的API(工作几年,还没用过Spring之外的框架搭建环境),这种常用的业务模式,基本上Spring都提供了API支持。
核心API:AbstractRoutingDataSource
底层维护Map容器,用来保存数据源集合,提供一个抽象方法,实现自定义的路由策略。
@Nullable
private Map
补刀一句
:为何框架的原理很难通过一篇文章看明白?因为使用的不多,基本意识没有形成,熟悉框架原理的基本要求:对框架的各种功能都熟悉,经常使用,自然而然的就明白了,盐大晒的久,咸鱼才够味。
配置两个数据源
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driverClassName: com.MySQL.jdbc.Driver
master:
url: jdbc:mysql://localhost:3306/data_master
username: root
password: 123456
slave:
url: jdbc:mysql://localhost:3306/data_slave
username: root
password: 123456
从实际开发角度,这两个数据源需要配置主从复制流程,再基于安全角度,写库可以只给写权限,读库只给读权限。
Map容器加载
@Configuration
public class DruidConfig {
// 忽略参数加载,源码中有
@Bean
@Primary
public DataSource primaryDataSource() {
Map
这里的Map容器管理两个key,masterDataSource和slaveDataSource代表两个不同的库,使用不同的key即加载对应的库。
使用ThreadLocal管理当前会会话中线程参数,存取使用极其方便。
public class RouteContext implements AutoCloseable {
private static final ThreadLocal threadLocal = new ThreadLocal<>();
public static void setRouteKey (String key){
threadLocal.set(key);
}
public static String getRouteKey() {
String key = threadLocal.get();
return key == null ? "masterDataSource" : key;
}
@Override
public void close() {
threadLocal.remove();
}
}
获取ThreadLocal中,当前数据源的key,适配相关联的数据源。
public class RouteDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return RouteContext.getRouteKey();
}
}
基于AOP的切面思想,不同的方法类型,去设置对应路由Key,这样就可以在业务逻辑执行之前,切换到不同的数据源。
Aspect
@Component
@Order(1)
public class ReadWriteAop {
private static Logger LOGGER = LoggerFactory.getLogger(ReadWriteAop.class) ;
@Before("execution(* com.master.slave.controller.*.*(..))")
public void setReadDataSourceType() {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String method = request.getRequestURI() ;
boolean rwFlag = readOrWrite(method) ;
if (rwFlag){
RouteContext.setRouteKey("slaveDataSource");
} else {
RouteContext.setRouteKey("masterDataSource");
}
LOGGER.info("请求方法:"+method+";执行库:"+RouteContext.getRouteKey());
}
private String[] readArr = new String[]{"select","count","query","get","find"} ;
private boolean readOrWrite (String method){
for (String readVar:readArr) {
if (method.contains(readVar)){
return true ;
}
}
return false ;
}
}
常见的读取方法:select、count、query、get、find等等,方法的命名要遵循自定义的路由规则。
控制层API
import com.master.slave.entity.User;
import com.master.slave.service.UserService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
public class UserController {
@Resource
private UserService userService ;
@GetMapping("/selectById")
public User selectById (@RequestParam("id") Integer id) {
return userService.selectById(id) ;
}
@GetMapping("/insert")
public String insert () {
User user = new User("张三","write") ;
userService.insert(user) ;
return "success" ;
}
}
服务实现
@Service
public class UserService {
@Resource
private UserMapper userMapper ;
public User selectById (Integer id) {
return userMapper.selectById(id) ;
}
public void insert (User user){
userMapper.insert(user);
}
}
这样数据源基于不同的类型方法就会一直的动态切换。
GitHub·地址
https://github.com/cicadasmile/data-manage-parent
GitEE·地址
https://gitee.com/cicadasmile/data-manage-parent