spring配置主库从库_Spring主从数据库的配置和动态数据源切换原理

原文:https://www.liaoxuefeng.com/article/00151054582348974482c20f7d8431ead5bc32b30354705000

在大型应用程序中,配置主从数据库并使用读写分离是常见的设计模式。在Spring应用程序中,要实现读写分离,最好不要对现有代码进行改动,而是在底层透明地支持。

Spring内置了一个AbstractRoutingDataSource,它可以把多个数据源配置成一个Map,然后,根据不同的key返回不同的数据源。因为AbstractRoutingDataSource也是一个DataSource接口,因此,应用程序可以先设置好key, 访问数据库的代码就可以从AbstractRoutingDataSource拿到对应的一个真实的数据源,从而访问指定的数据库。它的结构看起来像这样:

┌───────────────────────────┐

│ controller │

│ set routing-key = "xxx" │

└───────────────────────────┘

┌───────────────────────────┐

│ logic code │

└───────────────────────────┘

┌───────────────────────────┐

│ routing datasource │

└───────────────────────────┘

┌─────────┴─────────┐

│ │

▼ ▼

┌─────────────┐ ┌─────────────┐

│ read-write │ │ read-only │

│ datasource │ │ datasource │

└─────────────┘ └─────────────┘

│ │

▼ ▼

┌─────────────┐ ┌─────────────┐

│ │ │ │

│ Master DB │ │ Slave DB │

│ │ │ │

└─────────────┘ └─────────────┘

第一步:配置多数据源

首先,我们在SpringBoot中配置两个数据源,其中第二个数据源是ro-datasource:

spring:

datasource:

jdbc-url: jdbc:mysql://localhost/test

username: rw

password: rw_password

driver-class-name: com.mysql.jdbc.Driver

hikari:

pool-name: HikariCP

auto-commit: false

...

ro-datasource:

jdbc-url: jdbc:mysql://localhost/test

username: ro

password: ro_password

driver-class-name: com.mysql.jdbc.Driver

hikari:

pool-name: HikariCP

auto-commit: false

...

在开发环境下,没有必要配置主从数据库。只需要给数据库设置两个用户,一个rw具有读写权限,一个ro只有SELECT权限,这样就模拟了生产环境下对主从数据库的读写分离。

在SpringBoot的配置代码中,我们初始化两个数据源:

@SpringBootApplication

public class MySpringBootApplication {

/**

* Master data source.

*/

@Bean("masterDataSource")

@ConfigurationProperties(prefix = "spring.datasource")

DataSource masterDataSource() {

logger.info("create master datasource...");

return DataSourceBuilder.create().build();

}

/**

* Slave (read only) data source.

*/

@Bean("slaveDataSource")

@ConfigurationProperties(prefix = "spring.ro-datasource")

DataSource slaveDataSource() {

logger.info("create slave datasource...");

return DataSourceBuilder.create().build();

}

...

}

第二步:编写RoutingDataSource

然后,我们用Spring内置的RoutingDataSource,把两个真实的数据源代理为一个动态数据源:

public class RoutingDataSource extends AbstractRoutingDataSource {

@Override

protected Object determineCurrentLookupKey() {

return "masterDataSource";

}

}

对这个RoutingDataSource,需要在SpringBoot中配置好并设置为主数据源:

@SpringBootApplication

public class MySpringBootApplication {

@Bean

@Primary

DataSource primaryDataSource(

@Autowired @Qualifier("masterDataSource") DataSource masterDataSource,

@Autowired @Qualifier("slaveDataSource") DataSource slaveDataSource

) {

logger.info("create routing datasource...");

Map map = new HashMap<>();

map.put("masterDataSource", masterDataSource);

map.put("slaveDataSource", slaveDataSource);

RoutingDataSource routing = new RoutingDataSource();

routing.setTargetDataSources(map);

routing.setDefaultTargetDataSource(masterDataSource);

return routing;

}

...

}

现在,RoutingDataSource配置好了,但是,路由的选择是写死的,即永远返回"masterDataSource",

现在问题来了:如何存储动态选择的key以及在哪设置key?

在Servlet的线程模型中,使用ThreadLocal存储key最合适,因此,我们编写一个RoutingDataSourceContext,来设置并动态存储key:

public class RoutingDataSourceContext implements AutoCloseable {

// holds data source key in thread local:

static final ThreadLocal threadLocalDataSourceKey = new ThreadLocal<>();

public static String getDataSourceRoutingKey() {

String key = threadLocalDataSourceKey.get();

return key == null ? "masterDataSource" : key;

}

public RoutingDataSourceContext(String key) {

threadLocalDataSourceKey.set(key);

}

public void close() {

threadLocalDataSourceKey.remove();

}

}

然后,修改RoutingDataSource,获取key的代码如下:

public class RoutingDataSource extends AbstractRoutingDataSource {

protected Object determineCurrentLookupKey() {

return RoutingDataSourceContext.getDataSourceRoutingKey();

}

}

这样,在某个地方,例如一个Controller的方法内部,就可以动态设置DataSource的Key:

@Controller

public class MyController {

@Get("/")

public String index() {

String key = "slaveDataSource";

try (RoutingDataSourceContext ctx = new RoutingDataSourceContext(key)) {

// TODO:

return "html... www.liaoxuefeng.com";

}

}

}

到此为止,我们已经成功实现了数据库的动态路由访问。

这个方法是可行的,但是,需要读从数据库的地方,就需要加上一大段try (RoutingDataSourceContext ctx = ...) {}代码,使用起来十分不便。有没有方法可以简化呢?

有!

我们仔细想想,Spring提供的声明式事务管理,就只需要一个@Transactional()注解,放在某个Java方法上,这个方法就自动具有了事务。

我们也可以编写一个类似的@RoutingWith("slaveDataSource")注解,放到某个Controller的方法上,这个方法内部就自动选择了对应的数据源。代码看起来应该像这样:

@Controller

public class MyController {

@Get("/")

@RoutingWith("slaveDataSource")

public String index() {

return "html... www.liaoxuefeng.com";

}

}

这样,完全不修改应用程序的逻辑,只在必要的地方加上注解,自动实现动态数据源切换,这个方法是最简单的。

想要在应用程序中少写代码,我们就得多做一点底层工作:必须使用类似Spring实现声明式事务的机制,即用AOP实现动态数据源切换。

实现这个功能也非常简单,编写一个RoutingAspect,利用AspectJ实现一个Around拦截:

@Aspect

@Component

public class RoutingAspect {

@Around("@annotation(routingWith)")

public Object routingWithDataSource(ProceedingJoinPoint joinPoint, RoutingWith routingWith) throws Throwable {

String key = routingWith.value();

try (RoutingDataSourceContext ctx = new RoutingDataSourceContext(key)) {

return joinPoint.proceed();

}

}

}

注意方法的第二个参数RoutingWith是Spring传入的注解实例,我们根据注解的value()获取配置的key。编译前需要添加一个Maven依赖:

org.springframework.boot

spring-boot-starter-aop

到此为止,我们就实现了用注解动态选择数据源的功能。最后一步重构是用字符串常量替换散落在各处的"masterDataSource"和"slaveDataSource"。

使用限制

受Servlet线程模型的局限,动态数据源不能在一个请求内设定后再修改,也就是@RoutingWith不能嵌套。此外,@RoutingWith和@Transactional混用时,要设定AOP的优先级。

本文代码需要SpringBoot支持,JDK 1.8编译并打开-parameters编译参数。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/571412.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

oracle rac实例切换,RAC+单实例DG的切换

RAC单实例DG的切换RAC单实例DG的搭建过程之前切换不成功&#xff0c;和参数设置有关。注意的参数是sid* 之类的&#xff0c;刚搭建好的环境racdbdg是单实例的&#xff0c;是备库&#xff0c;rac节点是主库。搭建完毕&#xff0c;切换了一次&#xff0c;刚好主库是单实例的racdb…

tablueau地图标记圆形_R语言在线地图神器:Leaflet for R包(三) 符号标记

如果还是直接写&#xff0c;会出现错误提示&#xff0c;说找不到经纬度的列像这种情况&#xff0c;就需要进行显式申明了&#xff1a;(顺便换个背景底图)leaflet(dh)%>%addProviderTiles("Esri.WorldStreetMap")%>%setView(116.4,39.9,zoom10)%>%addMarkers…

zabbix自动发现主机并加入组绑定模板

在被监控主机多的情况下&#xff0c;怎样将这些主机加入zabbix server进行监控呢&#xff1f;下面将介绍下zabbix自动发现功能 1、创建自动发现规则 创建“规则名称&#xff0c;配置ip范围及检查方式”&#xff0c;点击“增加”&#xff0c;完成自动发现规则的创建 2、加入组和…

oracle ora 08103,ORA-08103: 对象不再存在

该楼层疑似违规已被系统折叠 隐藏此楼查看此楼[2017-05-17 17:13:22] Function Called in lngGetDataTableWithParametersselect distinct to_char (b.chargeactive_dat, yyyymmdd) as orderdate,to_char(a.recipeno2_int) as recipeno2_chr, b.orderexectype_int as ordertype…

kotlin 用协程做网络请求_中国电信营业厅: 感受 Kotlin 的 quot;加速度quot;

"我们手上是一个很成熟的项目&#xff0c;所以毫无疑问需要保留 Java 代码&#xff0c;目前只会在新开发的页面中使用 Kotlin&#xff0c;并已经感受到了它带来的便利。随着功能的迭代&#xff0c;我们相信更多的功能会转而使用 Kotlin。"—— 付迎鑫&#xff0c;电信…

拖动验证码插件

拖动验证码插件 效果图&#xff1a; 在验证前&#xff0c;提交弹出验证失败&#xff0c;验证后弹出验证成功。 文件结构&#xff1a; css&#xff1a; #drag{ position: relative;background-color: #e8e8e8;width: 300px;height: 34px;line-height: 34px;text-align: center; …

matlab padarray函数零,matlab padarray函数

1padarray功能&#xff1a;填充图像或填充数组。用法&#xff1a;B padarray(A,padsize,padval,direction)A为输入图像&#xff0c;B为填充后的图像&#xff0c;padsize给出了给出了填充的行数和列数&#xff0c;通常用[r c]来表示。padval表示填充方法。它的具体值和描述如下…

如何将已有图片做成透明水印_如何用Photoshop在图片上添加透明水印?

我们经常会在网上看到一些漂亮的图片上有添加各种水印&#xff0c;添加水印可以让图片有一个标记&#xff0c;也可以防止别人盗用你的成果。那么图片上的这些水印是怎么添加的呢&#xff1f;今天&#xff0c;小编就给大家介绍用Photoshop软件来制作图片的透明水印的教程&#x…

[知了堂学习笔记]_网络基础知识_1.OSI参考模型(网络七层协议)

OSI参考模型是国际标准化组织ISO制定的模型&#xff0c;把计算机与计算机之间的通信分成七个互相连接的协议层&#xff0c;如图&#xff1a; 1.1各层功能 1、物理层 最底层是物理层&#xff0c;这一次负责传送比特流&#xff0c;它从第二层数据接收数据帧&#xff0c;并将帧的结…

微信小程序js数组初始化_微信小程序 数组(增,删,改,查)等操作实例详解...

微信小程序 数组(增&#xff0c;删&#xff0c;改&#xff0c;查)等操作最近在做一个小程序的demo。由于不向后台请求数据&#xff0c;所以就涉及到对本地数据的操作&#xff0c;也遇到了一些坑&#xff0c;本文就以数组的增删改查为例&#xff0c;给新手分享一些经验。首先这是…

Linux版本配置环境变量,如何linux环境下配置环境变量过程图解

jdk下载地址&#xff1a;在linux环境下的root同级目录下配置software目录将下载好的jdk上传到software文件夹里面(我使用的操作软件是)到software这个目录下.输入命令:cd /software输入命令:ll就可以看到我们的jdk包解压文件解压命令 tar xzvf jdk-8u111-linux-x64.tar.gz解压之…

linux的mysql本地yum安装_linux下使用yum安装mysql

mysql yum repository提供linux平台上面的MySQL server, client和其它组件的rpm包。它支持以下平台&#xff1a;基于EL5-, EL6-, 和 EL7-的平台&#xff0c;比如对应版本的ed Hat Enterprise Linux, Oracle Linux, CentOSFedora 22, 23, 24步骤&#xff1a;一、添加mysql yum r…

A.PHP读取txt文本文件并分页显示的方法

PHP读取txt文本文件并分页显示的方法作者&#xff1a;TomRobert 字体&#xff1a;[增加 减小] 类型&#xff1a;转载 时间&#xff1a;2015-03-11 我要评论这篇文章主要介绍了PHP读取txt文本文件并分页显示的方法,涉及php操作文件的技巧,具有一定参考借鉴价值,需要的朋友可以参…

python阈值计算_opencv python 图像二值化/简单阈值化/大津阈值法

pip install matplotlib1简单的阈值化cv2.threshold第一个参数是源图像&#xff0c;它应该是灰度图像. 第二个参数是用于对像素值进行分类的阈值, 第三个参数是maxVal&#xff0c;它表示如果像素值大于(有时小于)阈值则要给出的值. OpenCV提供不同类型的阈值&#xff0c;它由函…

奔腾双核linux服务器,Dell推出双核心奔腾服务器

来自业内的消息&#xff0c;Dell近日推出了一台采用Intel双核心 Pentium D的服务器&#xff0c;这将给小型服务器带来更强的运算能力。Dell PowerEdge SC430 主要面向小型企业客户&#xff0c;价格在499美元起。相比PowerEdge SC420&#xff0c;Dell为 PowerEdge SC430 装配了两…

EasyPlayerPro Windows流媒体播放器(RTSP/RTMP/HTTP/HLS/File/TCP/RTP/UDP都能播)发布啦

EasyPlayerPro简介 EasyPlayerPro是一款全功能的流媒体播放器&#xff0c;支持RTSP、RTMP、HTTP、HLS、UDP、RTP、File等多种流媒体协议播放、支持本地文件播放&#xff0c;支持本地抓拍、本地录像、播放旋转、多屏播放等多种功能特性&#xff0c;核心基于ffmpeg&#xff0c;稳…

github 思维导图开元软件_Mymind教学系列--Github上的免费且强大思维导图工具-(一)...

How many 0‘s?Time Limit: 1000MSMemory Limit: 65536KTotal Submissions: 2844Accepted: 1506DescriptionA Benedict monk No.16 writes down the decimal representations of all natural numbers between and including m and n, m ≤ n. How many 0‘s will he write dow…

linux jlink软件安装,LINUX下安装JLINK

确认是否安装libusb$ dpkg -l | grep libusb如果有libusb相关信息&#xff0c;说明已经安装了$ dpkg -l | grep libusbii libusb-0.1-4 2:0.1.12-20 userspace USB programming libraryii libusb-1.0-0 2:1.0.9~rc3-2ubuntu1 userspace USB programming libraryii libusb-dev 2…

tcpdump记录

tcpdump -i eth0 -nn -A -X host 192.168.20.82 and port 9080 -i&#xff1a;interface 监听的网卡。-nn&#xff1a;表示以ip和port的方式显示来源主机和目的主机&#xff0c;而不是用主机名和服务。-A&#xff1a;以ascii的方式显示数据包&#xff0c;抓取web数据时很有用。…

abortonerror_如何在多项目目录顶层的Android Gradle Plugin中禁用lint abortOnError

我有一个顶级的Android Gradle项目.这个项目下面嵌套了多个子项目(有时它们是2级深),即&#xff1a;top level project|project1vendor libraries|lib1lib2lint正在中断一些图书馆项目的建设.我可以编辑每个图书馆项目的build.gradle来解决问题android {lintOptions {abortOnEr…