mybatis 多租户saas_MybatisPlus 多租户架构(SaaS)实现

1. 引言

读写分离要做的事情就是对于一条SQL该选择哪个数据库去执行,至于谁来做选择数据库这件事儿,无非两个,要么中间件帮我们做,要么程序自己做。因此,一般来讲,读写分离有两种实现方式。第一种是依靠中间件(比如:MyCat),也就是说应用程序连接到中间件,中间件帮我们做SQL分离;第二种是应用程序自己去做分离。这里我们选择程序自己来做,主要是利用Spring提供的路由数据源,以及AOP

然而,应用程序层面去做读写分离最大的弱点(不足之处)在于无法动态增加数据库节点,因为数据源配置都是写在配置中的,新增数据库意味着新加一个数据源,必然改配置,并重启应用。当然,好处就是相对简单。

2. AbstractRoutingDataSource

基于特定的查找key路由到特定的数据源。它内部维护了一组目标数据源,并且做了路由key与目标数据源之间的映射,提供基于key查找数据源的方法。

3. 实践

3.1. maven依赖

xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

4.0.0

com.cjs.example

cjs-datasource-demo

0.0.1-SNAPSHOT

jar

cjs-datasource-demo

org.springframework.boot

spring-boot-starter-parent

2.0.5.RELEASE

UTF-8

UTF-8

1.8

org.springframework.boot

spring-boot-starter-aop

org.springframework.boot

spring-boot-starter-jdbc

org.springframework.boot

spring-boot-starter-web

org.mybatis.spring.boot

mybatis-spring-boot-starter

1.3.2

org.apache.commons

commons-lang3

3.8

mysql

mysql-connector-java

runtime

org.springframework.boot

spring-boot-starter-test

test

org.springframework.boot

spring-boot-maven-plugin

3.2. 数据源配置

application.yml

spring:

datasource:

master:

jdbc-url: jdbc:mysql://192.168.102.31:3306/test

username: root

password: 123456

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

slave1:

jdbc-url: jdbc:mysql://192.168.102.56:3306/test

username: pig # 只读账户

password: 123456

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

slave2:

jdbc-url: jdbc:mysql://192.168.102.36:3306/test

username: pig # 只读账户

password: 123456

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

多数据源配置

package com.cjs.example.config;

import com.cjs.example.bean.MyRoutingDataSource;

import com.cjs.example.enums.DBTypeEnum;

import org.springframework.beans.factory.annotation.Qualifier;

import org.springframework.boot.context.properties.ConfigurationProperties;

import org.springframework.boot.jdbc.DataSourceBuilder;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;

import java.util.HashMap;

import java.util.Map;

/**

* 关于数据源配置,参考SpringBoot官方文档第79章《Data Access》

* 79. Data Access

* 79.1 Configure a Custom DataSource

* 79.2 Configure Two DataSources

*/

@Configuration

public class DataSourceConfig {

@Bean

@ConfigurationProperties("spring.datasource.master")

public DataSource masterDataSource() {

return DataSourceBuilder.create().build();

}

@Bean

@ConfigurationProperties("spring.datasource.slave1")

public DataSource slave1DataSource() {

return DataSourceBuilder.create().build();

}

@Bean

@ConfigurationProperties("spring.datasource.slave2")

public DataSource slave2DataSource() {

return DataSourceBuilder.create().build();

}

@Bean

public DataSource myRoutingDataSource(@Qualifier("masterDataSource") DataSource masterDataSource,

@Qualifier("slave1DataSource") DataSource slave1DataSource,

@Qualifier("slave2DataSource") DataSource slave2DataSource) {

Map targetDataSources = new HashMap<>();

targetDataSources.put(DBTypeEnum.MASTER, masterDataSource);

targetDataSources.put(DBTypeEnum.SLAVE1, slave1DataSource);

targetDataSources.put(DBTypeEnum.SLAVE2, slave2DataSource);

MyRoutingDataSource myRoutingDataSource = new MyRoutingDataSource();

myRoutingDataSource.setDefaultTargetDataSource(masterDataSource);

myRoutingDataSource.setTargetDataSources(targetDataSources);

return myRoutingDataSource;

}

}

这里,我们配置了4个数据源,1个master,2两个slave,1个路由数据源。前3个数据源都是为了生成第4个数据源,而且后续我们只用这最后一个路由数据源。

MyBatis配置

package com.cjs.example.config;

import org.apache.ibatis.session.SqlSessionFactory;

import org.mybatis.spring.SqlSessionFactoryBean;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.core.io.support.PathMatchingResourcePatternResolver;

import org.springframework.jdbc.datasource.DataSourceTransactionManager;

import org.springframework.transaction.PlatformTransactionManager;

import org.springframework.transaction.annotation.EnableTransactionManagement;

import javax.annotation.Resource;

import javax.sql.DataSource;

@EnableTransactionManagement

@Configuration

public class MyBatisConfig {

@Resource(name = "myRoutingDataSource")

private DataSource myRoutingDataSource;

@Bean

public SqlSessionFactory sqlSessionFactory() throws Exception {

SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();

sqlSessionFactoryBean.setDataSource(myRoutingDataSource);

sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml"));

return sqlSessionFactoryBean.getObject();

}

@Bean

public PlatformTransactionManager platformTransactionManager() {

return new DataSourceTransactionManager(myRoutingDataSource);

}

}

由于Spring容器中现在有4个数据源,所以我们需要为事务管理器和MyBatis手动指定一个明确的数据源。

3.3 设置路由key / 查找数据源

目标数据源就是那前3个这个我们是知道的,但是使用的时候是如果查找数据源的呢?

首先,我们定义一个枚举来代表这三个数据源

package com.cjs.example.enums;

public enum DBTypeEnum {

MASTER, SLAVE1, SLAVE2;

}

接下来,通过ThreadLocal将数据源设置到每个线程上下文中

package com.cjs.example.bean;

import com.cjs.example.enums.DBTypeEnum;

import java.util.concurrent.atomic.AtomicInteger;

public class DBContextHolder {

private static final ThreadLocal contextHolder = new ThreadLocal<>();

private static final AtomicInteger counter = new AtomicInteger(-1);

public static void set(DBTypeEnum dbType) {

contextHolder.set(dbType);

}

public static DBTypeEnum get() {

return contextHolder.get();

}

public static void master() {

set(DBTypeEnum.MASTER);

System.out.println("切换到master");

}

public static void slave() {

// 轮询

int index = counter.getAndIncrement() % 2;

if (counter.get() > 9999) {

counter.set(-1);

}

if (index == 0) {

set(DBTypeEnum.SLAVE1);

System.out.println("切换到slave1");

}else {

set(DBTypeEnum.SLAVE2);

System.out.println("切换到slave2");

}

}

}

获取路由key

package com.cjs.example.bean;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

import org.springframework.lang.Nullable;

public class MyRoutingDataSource extends AbstractRoutingDataSource {

@Nullable

@Override

protected Object determineCurrentLookupKey() {

return DBContextHolder.get();

}

}

设置路由key

默认情况下,所有的查询都走从库,插入/修改/删除走主库。我们通过方法名来区分操作类型(CRUD)

package com.cjs.example.aop;

import com.cjs.example.bean.DBContextHolder;

import org.apache.commons.lang3.StringUtils;

import org.aspectj.lang.JoinPoint;

import org.aspectj.lang.annotation.Aspect;

import org.aspectj.lang.annotation.Before;

import org.aspectj.lang.annotation.Pointcut;

import org.springframework.stereotype.Component;

@Aspect

@Component

public class DataSourceAop {

@Pointcut("!@annotation(com.cjs.example.annotation.Master) " +

"&& (execution(* com.cjs.example.service..*.select*(..)) " +

"|| execution(* com.cjs.example.service..*.get*(..)))")

public void readPointcut() {

}

@Pointcut("@annotation(com.cjs.example.annotation.Master) " +

"|| execution(* com.cjs.example.service..*.insert*(..)) " +

"|| execution(* com.cjs.example.service..*.add*(..)) " +

"|| execution(* com.cjs.example.service..*.update*(..)) " +

"|| execution(* com.cjs.example.service..*.edit*(..)) " +

"|| execution(* com.cjs.example.service..*.delete*(..)) " +

"|| execution(* com.cjs.example.service..*.remove*(..))")

public void writePointcut() {

}

@Before("readPointcut()")

public void read() {

DBContextHolder.slave();

}

@Before("writePointcut()")

public void write() {

DBContextHolder.master();

}

/**

* 另一种写法:if...else... 判断哪些需要读从数据库,其余的走主数据库

*/

// @Before("execution(* com.cjs.example.service.impl.*.*(..))")

// public void before(JoinPoint jp) {

// String methodName = jp.getSignature().getName();

//

// if (StringUtils.startsWithAny(methodName, "get", "select", "find")) {

// DBContextHolder.slave();

// }else {

// DBContextHolder.master();

// }

// }

}

有一般情况就有特殊情况,特殊情况是某些情况下我们需要强制读主库,针对这种情况,我们定义一个主键,用该注解标注的就读主库

package com.cjs.example.annotation;

public @interface Master {

}

例如,假设我们有一张表member

package com.cjs.example.service.impl;

import com.cjs.example.annotation.Master;

import com.cjs.example.entity.Member;

import com.cjs.example.entity.MemberExample;

import com.cjs.example.mapper.MemberMapper;

import com.cjs.example.service.MemberService;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.stereotype.Service;

import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service

public class MemberServiceImpl implements MemberService {

@Autowired

private MemberMapper memberMapper;

@Transactional

@Override

public int insert(Member member) {

return memberMapper.insert(member);

}

@Master

@Override

public int save(Member member) {

return memberMapper.insert(member);

}

@Override

public List selectAll() {

return memberMapper.selectByExample(new MemberExample());

}

@Master

@Override

public String getToken(String appId) {

// 有些读操作必须读主数据库

// 比如,获取微信access_token,因为高峰时期主从同步可能延迟

// 这种情况下就必须强制从主数据读

return null;

}

}

4. 测试

package com.cjs.example;

import com.cjs.example.entity.Member;

import com.cjs.example.service.MemberService;

import org.junit.Test;

import org.junit.runner.RunWith;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.boot.test.context.SpringBootTest;

import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)

@SpringBootTest

public class CjsDatasourceDemoApplicationTests {

@Autowired

private MemberService memberService;

@Test

public void testWrite() {

Member member = new Member();

member.setName("zhangsan");

memberService.insert(member);

}

@Test

public void testRead() {

for (int i = 0; i < 4; i++) {

memberService.selectAll();

}

}

@Test

public void testSave() {

Member member = new Member();

member.setName("wangwu");

memberService.save(member);

}

@Test

public void testReadFromMaster() {

memberService.getToken("1234");

}

}

查看控制台

5. 工程结构

6. 参考

https://www.jianshu.com/p/f2f4256a2310

http://www.cnblogs.com/gl-developer/p/6170423.html

https://www.cnblogs.com/huangjuncong/p/8576935.html

https://blog.csdn.net/liu976180578/article/details/77684583

推荐

学习资料分享

12 套 微服务、Spring Boot、Spring Cloud 核心技术资料,这是部分资料目录:

Spring Security 认证与授权

Spring Boot 项目实战(中小型互联网公司后台服务架构与运维架构)

Spring Boot 项目实战(企业权限管理项目))

Spring Cloud 微服务架构项目实战(分布式事务解决方案)

...

公众号后台回复arch028获取资料::

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

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

相关文章

Floodlight 在 ChannelPipeline 图

我们知道&#xff0c;在Netty架构&#xff0c;一个ServerBootstrap用于生成server端的Channel的时候都须要提供一个ChannelPipelineFactory类型的參数&#xff0c;用于服务于建立连接的Channel&#xff0c;流水线处理来自某个client的请求。所以这里的 OpenflowPipelineFactory…

html超文本链接本页面,从HTML语言到网上家园 第三章 超文本链接(1)-网页设计,HTML/CSS...

超文本链接是 html 语言最大的特点之一&#xff0c;使用超文本链接可以极大的增加文件访问的灵活度&#xff0c;人们可以通过点击页面中的链接指针查看所需的内容&#xff0c;进退自如&#xff0c;灵活方便&#xff0c;这更加符合人的跳跃、交叉的思维方式。凡是浏览过网页的人…

PS景观彩色平面图技巧

1、关于水系&#xff0c;园林学习网 PS景观彩色平面图 水要有阴影&#xff0c;不过是内投影。可以用图层特效来做&#xff0c;也可以用高斯模糊。 要有光感&#xff0c;可以用退晕&#xff0c;也可以用滤镜打光。 2、草地 草地在红线内外一定要区分开色象和明度饱和度&#xff…

牛顿如果穿越到现在,能看懂相对论和量子力学吗?

全世界只有3.14 % 的人关注了爆炸吧知识今天要讲给大家讲一个从朋友BOSS那里听来的故事&#xff0c;而故事的主人公就是赫赫有名的牛顿大神。话说那一天&#xff0c;BOSS在牛顿的苹果树下思考人生。突然牛顿就从苹果树下的棺材里爬了出来&#xff0c;棺材板怎么压都压不住。于是…

【啊哈!算法】之二、插入排序

作者&#xff1a;jofranks 原创作品&#xff0c;转载请标明出处&#xff01;版权所有&#xff0c;侵权必究! 来源&#xff1a;http://blog.csdn.net/jofranks 插入排序包括&#xff1a;直接插入排序&#xff0c;折半插入排序&#xff0c;希尔排序~&#xff01; OK&#xff0c;下…

02Prism WPF 入门实战 - 建项

1.概要Prism介绍Github: https://github.com/PrismLibrary/Prism开发文档&#xff1a;https://prismlibrary.com/docs/Prism是一个框架&#xff0c;用于在WPF、Xamarin Forms、Uno Platform和WinUI中构建松散耦合、可维护和可测试的XAML应用程序。设计目标 为了实现下列目的&a…

rowspan 动态变化_使用colspan和rowspan动态删除html表中的多个列

好的&#xff0c;您的代码中的一个问题是&#xff0c;您删除了当前正在使用for进行迭代的单元格。我改变了你的第一个循环来完成所有反向&#xff1a;for (var i (rows[0].cells.length -1); i > 0; i--)&#xff0c;从后到前...所以没有索引在删除时发生变化。第二个问题是…

[转]Linux中如何自动启动服务

linux自动启动服务很简单&#xff0c;最简单的是把启动命令放到/etc/rc.d/rc.local文件里。这样就可以每次启动的时候自动启动服务了。例如对于 apache&#xff0c;编译好apache后会在安装目录的bin下生成apachectl文件&#xff0c;这是个启动脚本&#xff0c;我们只需要把这个…

一个C实现的线程池(产品暂未运用)

https://github.com/Pithikos/C-Thread-Pool

html首页 slider图片切换效果,jQuery插件Slider Revolution实现响应动画滑动图片切换效果...

jQuery插件Slider Revolution实现响应动画滑动图片切换效果2018-12-31编程之家https://www.jb51.cc这是一款非常强大的内容切换插件&#xff0c;它基于jQuery&#xff0c;它充分响应&#xff0c;支持移动设备&#xff0c;支持手机触摸&#xff0c;键盘翻页&#xff1b;它内置幻…

Asp.Net+Jquery.Ajax详解5-$.getScript

目录&#xff08;已经更新的文章会有连接&#xff0c;从7月25日开始&#xff0c;每2到3天更新一篇&#xff09;&#xff1a; Asp.NetJquery.Ajax详解1-开篇&#xff08;2012.07.25发&#xff09; Asp.NetJquery.Ajax详解2-$.Load&#xff08;2012.07.26发&#xff09; Asp.NetJ…

大数据告诉你:学历真的能改变命运!!

全世界只有3.14 % 的人关注了爆炸吧知识央视新闻曾做过关于高考的调查&#xff0c;结果有七成网友支持高考取消数学&#xff0c;看到新闻后&#xff0c;有一位网友却一针见血地评论道&#xff1a;数学考试存在的意义就是把这七成网友筛选掉。的确&#xff0c;虽然买菜不需要专业…

.net core 中如何有效屏蔽重复提交

咨询区 Guilherme Ferreira&#xff1a;我通过 post 方式向我的一个webapi中提交数据&#xff0c;然后插入到数据库中&#xff0c;在 ui端&#xff0c;当用户点击某一个 button 之后&#xff0c;代码会将 button 禁用&#xff0c;但因为某些原因&#xff0c;点击按钮的速度比禁…

小米8ios图标包下载_小米互传PC端抢先下载,免流量、高速互传,支持多设备共享...

小米早在MIUI初期就已经在开始探索手机与电脑之间互传文件的问题&#xff0c;MIUI"无线数据线"功能一直备受喜欢。手机与电脑之间互传&#xff0c;90%的用户都选择使用WX或者QQ来实现&#xff0c;它们互传的通道是互联网&#xff0c;无网时不可使用。为解决这个问题&…

sql server2008中怎样用sql语句创建数据库和数据表

这是简单用代码实现创建数据库和数据表的sql语句&#xff0c;如下&#xff1a; --调用系统数据库-- use master go /***防止你要创建的数据库同名&#xff0c;先把它删除掉****/ if Exists(select * from dbo.sysdatabases where nameTestDB) begin drop database TestDB end g…

[C++][IO]读写二进制文件

1. 以二进制方式读写结构体 struct Student {string name;string sex;int age; }void write(string filePath, const struct Student* stu, int n) {FILE *fp;int i;if((fpfopen(filePath,"wb"))NULL){printf("cant open the file");return;}for(i0;i<n…

HTML怎么做类似QQ聊天气泡,h5实现QQ聊天气泡的实例介绍

这篇文章主要介绍了HTML5实现QQ聊天气泡效果&#xff0c;用 HTML/CSS 做了个类似QQ的聊天气泡&#xff0c;具有一定的参考价值&#xff0c;感兴趣的小伙伴们可以参考一下今天自己用 HTML/CSS 做了个类似QQ的聊天气泡&#xff0c;以下是效果图&#xff1a;以下说下关键地方的样式…

高等数学的用处之一

1 只能说计算的真精准2 龙虾&#xff1a;我都准备半天了&#xff0c;你俩到底上不上&#xff1f;3 猫(≧^.^≦)&#xff1a;我为这个宿舍付出太多了&#xff01;4 请举一个日常生活中利用高等数学来解决问题的案例。5 男生做什么会让女生不开心7 人家拍的泸沽湖的水性杨花和我拍…

wp7 blogs

http://blog.csdn.net/jazywoo123/article/month/2012/04/3转载于:https://www.cnblogs.com/songtzu/archive/2012/08/09/2630573.html

k8s 查看ip地址属于哪个pod_Kubernetes Pod 如何获取 IP 地址

【编者的话】在学习 Kubernetes 网络模型的过程中&#xff0c;了解各种网络组件的作用以及如何交互非常重要。本文就介绍了各种网络组件在 Kubernetes 集群中是如何交互的&#xff0c;以及如何帮助每个 Pod 都能获取 IP 地址。Kubernetes 网络模型的核心要求之一是每个 Pod 都拥…