数据库读写分离 - MyBatis

2019独角兽企业重金招聘Python工程师标准>>> hot3.png

由于项目中数据量较大,访问量也较高,故在数据库的设计上,采用读写分离思想,达到性能要求!

 

简单科普一下实现读写分离的思路

  1. 配置及加载数据库信息,即加载数据源
  2. 继承类AbstractRoutingDataSource,并重写方法determineCurrentLookupKey(),在这个方法中决定使用哪个数据源,实现切换数据库。这点非常重要。
  3. 使用AOP切面,截获读写操作,修改数据库读写标志。

 

废话不多,上代码

  1. 配置及加载数据库信息
    (1)在yml文件中填写数据库配置信息,这里是mysql、1主2从:
    datasource:#从库数量readSize: 2type: com.alibaba.druid.pool.DruidDataSource # 使用druid数据源#主库write:url: jdbc:mysql://localhost:3306/lingguan_xiaowang?useUnicode=true&characterEncoding=utf-8&useSSL=falseusername: usernamepassword: passworddriver-class-name: com.mysql.jdbc.Driverfilters: statmaxActive: 20initialSize: 1maxWait: 60000minIdle: 1timeBetweenEvictionRunsMillis: 60000minEvictableIdleTimeMillis: 300000validationQueryTimeout: 900000validationQuery: SELECT SYSDATE() from dualtestWhileIdle: truetestOnBorrow: falsetestOnReturn: falsepoolPreparedStatements: truemaxOpenPreparedStatements: 20# 从库read1:url: jdbc:mysql://localhost:3306/slave1?useUnicode=true&characterEncoding=utf-8&useSSL=falseusername: usernamepassword: passworddriver-class-name: com.mysql.jdbc.Driverfilters: statmaxActive: 20initialSize: 1maxWait: 60000minIdle: 1timeBetweenEvictionRunsMillis: 60000minEvictableIdleTimeMillis: 300000validationQueryTimeout: 900000validationQuery: SELECT SYSDATE() from dualtestWhileIdle: truetestOnBorrow: falsetestOnReturn: falsepoolPreparedStatements: truemaxOpenPreparedStatements: 20read2:url: jdbc:mysql://localhost:3306/slave2?useUnicode=true&characterEncoding=utf-8&useSSL=falseusername: usernamepassword: passworddriver-class-name: com.mysql.jdbc.Driverfilters: statmaxActive: 20initialSize: 1maxWait: 60000minIdle: 1timeBetweenEvictionRunsMillis: 60000minEvictableIdleTimeMillis: 300000validationQueryTimeout: 900000validationQuery: SELECT SYSDATE() from dualtestWhileIdle: truetestOnBorrow: falsetestOnReturn: falsepoolPreparedStatements: truemaxOpenPreparedStatements: 20


    (2)通过读取yml配置文件,构造动态数据源DataSource
     

    
    import com.gome.store.util.mybatis.DataSourceType;
    import com.gome.store.util.mybatis.MyAbstractRoutingDataSource;
    import org.apache.ibatis.session.SqlSessionFactory;
    import org.mybatis.spring.SqlSessionFactoryBean;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.Primary;
    import org.springframework.core.env.Environment;
    import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
    import org.springframework.jdbc.datasource.DataSourceTransactionManager;
    import org.springframework.transaction.annotation.EnableTransactionManagement;import javax.sql.DataSource;
    import java.util.HashMap;
    import java.util.Map;/*** @Description 解析数据库的配置项* @Author wangjie<https://my.oschina.net/xiaowangqiongyou>* @Date 2017/7/31*/
    @Configuration
    @EnableTransactionManagement
    public class DataSourceConfiguration {protected Logger logger = LoggerFactory.getLogger(this.getClass());@Value("${datasource.type}")private Class<? extends DataSource> dataSourceType;@Autowiredprivate Environment env;@Value("${datasource.readSize}")private String dataSourceSize;@Bean(name = "writeDataSource")@Primary@ConfigurationProperties(prefix = "datasource.write")public DataSource writeDataSource() {logger.info("-------------------- writeDataSource init ---------------------");return DataSourceBuilder.create().type(dataSourceType).build();}/*** 有多少个从库就要配置多少个** @author wangjie* @date 2017/7/31* @params*/@Bean(name = "readDataSource1")@ConfigurationProperties(prefix = "datasource.read1")public DataSource readDataSourceOne() {logger.info("-------------------- readDataSourceOne init ---------------------");return DataSourceBuilder.create().type(dataSourceType).build();}@Bean(name = "readDataSource2")@ConfigurationProperties(prefix = "datasource.read2")public DataSource readDataSourceTwo() {logger.info("-------------------- readDataSourceTwo init ---------------------");return DataSourceBuilder.create().type(dataSourceType).build();}@Beanpublic MyAbstractRoutingDataSource dataSource(@Qualifier("writeDataSource") DataSource writeDataSource,@Qualifier("readDataSource1") DataSource readDataSourceOne,@Qualifier("readDataSource2") DataSource readDataSourceTwo) {int size = Integer.parseInt(dataSourceSize);Map<Object, Object> targetDataSources = new HashMap<>();// 写targetDataSources.put(DataSourceType.write.getType(), writeDataSource);// 读targetDataSources.put(DataSourceType.read.getType() + 0, readDataSourceOne);targetDataSources.put(DataSourceType.read.getType() + 1, readDataSourceTwo);MyAbstractRoutingDataSource dataSource = new MyAbstractRoutingDataSource(size);dataSource.setTargetDataSources(targetDataSources);// 该方法是AbstractRoutingDataSource的方法dataSource.setDefaultTargetDataSource(writeDataSource);// 默认的datasource设置为myTestDbDataSourcereturn dataSource;}@Beanpublic SqlSessionFactory sqlSessionFactory(MyAbstractRoutingDataSource ds) throws Exception {SqlSessionFactoryBean fb = new SqlSessionFactoryBean();fb.setDataSource(ds);// 指定数据源(这个必须有,否则报错)// 下边两句仅仅用于*.xml文件,如果整个持久层操作不需要使用到xml文件的话(只用注解就可以搞定),则不加fb.setTypeAliasesPackage(env.getProperty("mybatis.typeAliasesPackage"));// 指定基包fb.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(env.getProperty("mybatis.mapperLocations")));//return fb.getObject();}/*** 配置事务管理器*/@Beanpublic DataSourceTransactionManager transactionManager(MyAbstractRoutingDataSource dataSource) throws Exception {return new DataSourceTransactionManager(dataSource);}
    }
    

    注释:
            · @Primary:指定在同一个接口有多个实现类可以注入的时候,默认选择哪一个,而不是让@Autowire注解报错(一般用于多数据源的情况下)
            · @Qualifier:指定名称的注入,当一个接口有多个实现类的时候使用(在本例中,有两个DataSource类型的实例,需要指定名称注入)
            · 通过动态数据源构造SqlSessionFactory和事务管理器(如果不需要事务,后者可以去掉)
     

  2. 编写相应的控制数据源类型类
    (1)数据源类型
    
    /*** @Description* @Author wangjie<https://my.oschina.net/xiaowangqiongyou>* @Date 2017/7/31*/
    public enum DataSourceType {read("read", "从库"), write("write", "主库");private String type;private String name;DataSourceType(String type, String name) {this.type = type;this.name = name;}public String getType() {return type;}public String getName() {return name;}
    }
    


    (2)数据源标志类
    
    /*** @Description 本地线程全局变量,用来存放当前操作是读还是写* @Author wangjie<https://my.oschina.net/xiaowangqiongyou>* @Date 2017/7/31*/
    public class DataSourceContextHolder {private static final ThreadLocal<String> local = new ThreadLocal<>();public static ThreadLocal<String> getLocal() {return local;}/** 读可能是多个库* @author wangjie* @date 2017/7/31* @params*/public static void read() {local.set(DataSourceType.read.getType());}/** 写只有一个库* @author wangjie* @date 2017/7/31* @params*/public static void write() {local.set(DataSourceType.write.getType());}public static String getJdbcType() {return local.get();}/** 清除数据源类型* @author wangjie* @date 2017/7/31* @params*/public static void clearDataSourceType() {local.remove();}
    }
    

     
  3. 继承AbstractRoutingDataSource 
    
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
    import org.springframework.util.StringUtils;import java.util.concurrent.atomic.AtomicInteger;/*** @Description 多数据源切换* @Author wangjie<https://my.oschina.net/xiaowangqiongyou>* @Date 2017/7/31*/
    public class MyAbstractRoutingDataSource extends AbstractRoutingDataSource {protected Logger logger = LoggerFactory.getLogger(this.getClass());private final int dataSourceNumber;private AtomicInteger count = new AtomicInteger(0);public MyAbstractRoutingDataSource(int dataSourceNumber) {this.dataSourceNumber = dataSourceNumber;}@Overrideprotected Object determineCurrentLookupKey() {Object resultObject = null;String typeKey = DataSourceContextHolder.getJdbcType();// 只对主库开启事务,如果typeKey为空表示获取主库的datasourceif (StringUtils.isEmpty(typeKey)) {resultObject = DataSourceType.write.getType();} else {// 读简单负载均衡int number = count.getAndAdd(1);int lookupKey = number % dataSourceNumber;resultObject = DataSourceType.read.getType() + lookupKey;}logger.info("determineCurrentLookupKey:" + resultObject);return resultObject;}
    }
    

     
  4. AOP的实现
    
    import com.gome.store.util.mybatis.DataSourceContextHolder;
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;
    import org.aspectj.lang.reflect.MethodSignature;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.stereotype.Component;
    import org.springframework.transaction.annotation.Transactional;/*** @Description aop 拦截 进行切换数据源* 如果service层 增加了@Transactional ,导致数据源MyAbstractRoutingDataSource的determineCurrentLookupKey()方法会在执行DataSourceAop拦截之前就进行全局事务绑定* 从而导致获取 DataSourceContextHolder.getJdbcType(); 一直都是空值* @Author wangjie<https://my.oschina.net/xiaowangqiongyou>* @Date 2017/7/31*/
    @Aspect
    @Component
    public class DataSourceAop {private static Logger logger = LoggerFactory.getLogger(DataSourceAop.class);//    @Before("execution(* com.ggj.encrypt.modules.*.dao..*.find*(..)) or execution(* com.ggj.encrypt.modules.*.dao..*.get*(..)) or execution(* com.ggj.encrypt.modules.*.dao..*.select*(..))")@Before("execution(* com.gome.store.dao..*.query*(..)) or execution(* com.gome.store.dao..*.get*(..)) or execution(* com.gome.store.dao..*.select*(..))")public void setReadDataSourceType() {DataSourceContextHolder.read();logger.info("dataSource切换到:Read");}// 另一种方式
    //	@Around("@annotation(org.springframework.transaction.annotation.Transactional)")
    //    public void setWriteDataSourceType(ProceedingJoinPoint joinPoint) throws Throwable {
    //		Transactional datasource = ((MethodSignature)joinPoint.getSignature()).getMethod().getAnnotation(Transactional.class);
    //		if(datasource.readOnly()){
    //			DataSourceContextHolder.read();
    //            logger.info("dataSource切换到:Read");
    //		}else{
    //			DataSourceContextHolder.write();
    //            logger.info("dataSource切换到:write");
    //		}
    //		joinPoint.proceed();
    //	}
    }
    

     

  5. 测试

    @Override
    public User queryUsersByUsername(String userName) {return userDao.queryByName(userName);
    }@Transactional()
    @Override
    public void updateUserInfoById(User user) {userDao.updateBySelective(user);userDao.updateById(user.getId());
    }

     

寄语

        这里只是简单的实现了数据库的读写分离,很多细节问题没有具体实现,比如某个DB宕掉了时的处理,负载均衡只是简单的采用轮训方式,性能的统计,sql语句黑白名单等等问题。

 

转载于:https://my.oschina.net/xiaowangqiongyou/blog/1499887

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

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

相关文章

MySQL-03:数据表操作基本命令笔记

目录 数据表 1、创建表 2、删除表 3、清空表 4、修改表 5、基本数据类型 数据表 1、创建表 create table 表名(列名 类型 是否可以为空&#xff0c;列名 类型 是否可以为空 )ENGINEInnoDB DEFAULT CHARSETutf8 是否可空&#xff0c;null表示空&#xff0c;非字符串n…

java 怎么调试到第三方库的内部,在有源码的情况下

第一步&#xff0c; 把第三方库加到workspace : https://stackoverflow.com/questions/370814/how-to-set-a-breakpoint-in-eclipse-in-a-third-party-library The most sure-fire way to do this (and end up with something thats actually useful) is to download the sou…

t-mobile频段_T-Mobile再次被黑客入侵:超过200万个帐号和地址可能泄漏

t-mobile频段Attackers may have compromised three percent of T-Mobile’s 77 million customers on Monday, revealing personal information like addresses, phone numbers, and account numbers. 周一&#xff0c;攻击者可能泄露了T-Mobile 7700万客户中的3&#xff05;&…

第二篇 第三章防火防烟分区检查(一)

仓库面积可以增加3倍 就是乘以4 要一定条件 : 第二篇 第三章防火防烟分区检查&#xff08;一&#xff09; 21分钟处 该题比较有代表性 停车库 耐火等级允许最大面积 民用建筑防火分区 防烟分区的划分    防火卷帘控制器的测试 防火阀 装在通风,空调系统中 只有连在风机主管…

高斯数学

伟大的数学家高斯在9岁那年&#xff0c;用很短的时间完成了从1到100的累加。那原本是老师给学生们出的难题&#xff0c;希望他们能老老实实地待在教室里。高斯的方法很简单&#xff0c;他发现这是50个101的求和&#xff1a;100&#xff0b;1、99&#xff0b;2、98&#xff0b;3…

Ant Design Blazor 发布 0.13.0,正式支持.NET 7!

时隔3个月&#xff0c;Ant Design Blazor 发布新功能版本 0.13.0&#xff0c;并正式支持.NET 7!大家快去访问 antblazor.com 体验吧&#xff01;&#x1f525; 新增 .NET 7 目标框架支持。#2810 ElderJames&#x1f525; 重构 Mentions 组件&#xff0c;修复定位和隐藏问题。#2…

gitlab 分支操作笔记\新建远程分支\抓取远程分支\复制远程\删除分支

密码重新输入与保存 git config --global http.emptyAuth truegit config --global credential.helper store 1.不复制远程&#xff0c;直接新建远程分支。&#xff08;非正规操作&#xff09; git init //初始化 git remote add origin http://****/*****/taskboard.git…

如何在Xbox One或PlayStation 4上为Skyrim特别版安装Mods

The Elder Scrolls V: Skyrim Special Edition is now available on PlayStation 4 and Xbox One, and for the first time, “mods” are available to console gamers. Elder Scrolls V&#xff1a;Skyrim特别版现已在PlayStation 4和Xbox One上可用&#xff0c;并且首次向主…

微软宣布:PowerBI 已经与 Office 整合,一切更简单,变革又来了

很多人认为 Office 是 Office&#xff0c;PowerBI 是 PowerBI&#xff0c;怎么在 PPT 中显示 PowerBI 呢&#xff1f;这种问题以后将再不会存在。微软已经宣布&#xff0c;PowerBI 已经与 Office 深度整合&#xff0c;在未来的企业中&#xff0c;PowerBI 将与 Word&#xff0c;…

066:ORM查询条件详解-startswith和endswith:

ORM查询条件详解-startswith和endswith&#xff1a; startswith&#xff1a;判断某个字段的值是否是以某个值开始的。大小写敏感。示例代码如下&#xff1a; articles1 Article.objects.filter(title__startswith"fuck") 以上代码的意思是提取所有标题以 fuck 字符串…

前端工程师面试题汇总

HTML Doctype作用&#xff1f;严格模式与混杂模式如何区分&#xff1f;它们有何意义? HTML5 为什么只需要写 <!DOCTYPE HTML>&#xff1f; 行内元素有哪些&#xff1f;块级元素有哪些&#xff1f; 空(void)元素有那些&#xff1f; 页面导入样式时&#xff0c;使用lin…

MySQL-04:数据内容操作-增删改查-基本命令笔记

1、增 insert into 表 (列名,列名...) values (值,值,值...) insert into 表 (列名,列名...) values (值,值,值...),(值,值,值...) insert into 表 (列名,列名...) select (列名,列名...) from 表 2、删 delete from 表 delete from 表 where id&#xff1d;1 and name&…

火狐和chrome_Firefox,Chrome和Edge都将支持WebAuthn的硬件两因素身份验证

火狐和chromeLogging into Gmail or Facebook could soon mean plugging in a USB device, potentially making phishing a thing of the past. 登录Gmail或Facebook可能很快就意味着要插入USB设备&#xff0c;这可能使网络钓鱼成为过去。 That’s thanks to WebAuthn, a new o…

Could not delete .........May be locked by another process.

问题 原因&#xff1a;默认的设置是文件修改后立即发布&#xff0c;这样的设置是在你每个保存文件时都会触发&#xff0c;如果tomcat已经在运行&#xff0c;这样频繁的操作也会造成文件锁死 解决&#xff1a; Tomcat 右键clean 转载于:https://www.cnblogs.com/feiZhou/p/93…

flask的基础1

1.python 现阶段三大主流web框架Django Tornado Flask的对比 1.Django 主要特点是大而全,集成了很多组件,例如: Models Admin Form 等等, 不管你用得到用不到,反正它全都有,属于全能型框架 2.Tornado 主要特点是原生异步非阻塞,在IO密集型应用和多任务处理上占据绝对性的优势,属…

python实现批量压缩文件夹

前段时间碰到一个需要把目录下文件夹压缩的项目&#xff0c;但是度娘里没找到&#xff0c;只好自己写脚本了。 #coding:utf-8 import os filePath raw_input("请输入路径&#xff1a;") if filePath "":os._exit() #需要退出ds list(os.walk(filePath…

单元测试01:nunit 安装与代码测试

1.nunit 下载与安装 a.下载 下载地址&#xff1a; http://nunit.org/download/ b.添加到系统环境变量 解压下载包后&#xff0c;添加两个路径到环境变量&#xff0c;如&#xff1a; D:\nunitD:\nunit\nunit-console 2.建立测试项目 a.建立class project b.project 里re…

如何将您的Google Authenticator凭证移至新的Android手机或平板电脑

Most of the app data on your Android is probably synced online will automatically sync to a new phone or tablet. However, your Google Authenticator credentials won’t — they aren’t synchronized for obvious security reasons. Android上的大多数应用程序数据可…

关于经纬度的两个计算[Teaksxgluxv]

一、子午线周长(公里) 40008.548 赤道周长(公里) 40075.704 纬度40008.548 / 360(度) 111.135 公里/度40008.548 / (360*60)(分) 1.85 公里/分40008.548 / (360*60*60)(秒) 30.87 米/秒 经度首先算相应经度位置的纬度圈长度40075.704 * cos(经度)然后方法相同&#xff0c;除…

转载通过 Docker 实现传统应用程序的现代化

长期以来&#xff0c;IT 组织将其预算的 80% 用于简单地维护现有应用程序&#xff0c;而只花费 20% 用于创新。 在过去的 10 年里&#xff0c;这一比例并没有太大改观。而同时又必须面对创新的压力。无论是直接来自客户的需求&#xff0c;要求提供新的功能&#xff0c;还是来自…