数据库读写分离 - 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,一经查实,立即删除!

相关文章

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分钟处 该题比较有代表性 停车库 耐火等级允许最大面积 民用建筑防火分区 防烟分区的划分    防火卷帘控制器的测试 防火阀 装在通风,空调系统中 只有连在风机主管…

如何在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…

火狐和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…

如何将您的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;还是来自…

Blazor学习之旅 (6) 路由系统

【Blazor】| 总结/Edison Zhou大家好&#xff0c;我是Edison。许久没有更新Blazor学习系列了&#xff0c;今天续更。Blazor 的路由系统就和 ASP.NET MVC的路由系统一样&#xff0c;可以为我们提供灵活的选项&#xff0c;可用于确保用户请求到达可处理它们并返回用户想要的信息的…

kindle降级卡大树_从提示框:Kindle购物流程图,iOS降级和DIY焊接笔

kindle降级卡大树Once a week we round up some of the tips from the HTG tips box and share them with the greater readership; this week we’re looking at shopping for Kindles with a flowchart, downgrading iOS, and rolling your own DIY soldering pen. 每周一次&…

MS CRM 2011 Form与Web Resource在JScript中的相互调用

原创地址&#xff1a;http://www.cnblogs.com/jfzhu/archive/2013/02/14/2912580.html 转载请注明出处 在Form中可以添加一个html的web resource&#xff0c;在web resource里可以用JScript来使用REST Endpoint或者SOAD Endpoint。 你可以在Form中添加一个web resource&#xf…

从 .NET 6 更新到 .NET 7

Upgrade from .NET 6 to .NET 7Intro.NET 7 已经发布了一段时间了&#xff0c;将几个服务升级到了 .NET 7&#xff0c;升级还是比较顺利的&#xff0c;也有遇到几个问题&#xff0c;分享一下TargetFramework首先我们需要将项目文件中的目标框架&#xff08;TargetFramework&…

MySQL-05:pymysql与pycharm设置

PyMySQL介绍 PyMySQL 是在 Python3.x 版本中用于连接 MySQL 服务器的一个库&#xff0c;Python2中则使用mysqldb。 Django中也可以使用PyMySQL连接MySQL数据库。 PyMySQL安装 pip install pymysql 连接数据库 前置条件&#xff1a; 已有一个MySQL数据库&#xff0c;并且…

hdmi-cec_如何使用HDMI-CEC使用PlayStation 4或Pro自动打开电视

hdmi-cecCall it petty, but there’s just something about eliminating the “turn on the TV” portion of firing up your PlayStation 4 that makes the whole process seem faster. And the good news is that getting this action on your setup is as easy and ticking…

ABP Framework 7.0 RC 新增功能简介

imageABP Framework 在架构上有四大目标&#xff1a;模块化、DDD、多租户和微服务。从 7.0 更新的功能来看&#xff0c;其侧重点转向微服务场景的实现&#xff0c;比如&#xff1a;Dapr 集成、动态权限和功能、外部本地化、分布式实体缓存服务&#xff0c;都是对微服务和分布式…

(原創) 07/28/1982 少女A (中森明菜)

Abstract明菜的第二首單曲&#xff0c;也是她的成名曲&#xff0c;在台灣曾經被歌手嘟嘟翻唱過。 Introduction[hjp2400,300,true]http://oomusou.googlepages.com/shojo_a.flv[/hjp2] 明菜從『少女A』這首單曲才開始竄紅&#xff0c;走的也是可愛路線&#xff0c;招牌的『明菜…

ftp服务

1.ftp工作原理FTP是一个客户机/服务系统。用户通过一个支持FTP协议的客户机程序&#xff0c;连接到在远程主机上的FTP服务器程序。用户通过客户机程序向服务器程序发出命令&#xff0c;服务器程序执行用户所发出的命令&#xff0c;并将执行的结果返回到客户机。2.安装ftp服务yu…