高并发场景中,数据库都有哪些优化手段?不会还有人不知道吧

首先思考一个问题:在高并发的场景中,关于数据库都有哪些优化的手段?常用的有以下的实现方法:读写分离、加缓存、主从架构集群、分库分表等,在互联网应用中,大部分都是「读多写少」的场景,设置两个库,主库和读库,「主库的职能是负责写,从库主要是负责读,可以建立读库集群,通过读写职能在数据源上的隔离达到减少读写冲突、释压数据库负载、保护数据库的目的」。在实际的使用中,凡是涉及到写的部分直接切换到主库,读的部分直接切换到读库,这就是典型的读写分离技术。本篇博文将聚焦读写分离,探讨如何实现它。

在高并发下,需要对应用进行读写分离,配置多数据源,即写操作走主库,读操作则走从库,主从数据库负责各自的读和写,缓解了锁的争用,提高了读取性能。

实现读写分离有多种方式,如使用中间件MyCat、Sharding-JDBC等,这里我们使用Aop的方式在代码层面实现读写分离。

实现原理

实现读写分离,首先要对Mysql做主从复制,即搭建一个主数据库,以及一个或多个从数据库。

具体实现主从复制,可参照前一篇博客《基于docker配置MySQL主从复制》

使用Aop的方式,当调用业务层方法前,判断请求是否是只读操作,动态切换数据源,如果是只读操作,则切换从数据库的数据源,否则使用主数据库的数据源。

一: 主从数据源的配置

我们需要配置主从数据库,主从数据库的配置一般都是写在配置文件里面。通过@ConfigurationProperties注解,可以将配置文件(一般命名为:application.Properties)里的属性映射到具体的类属性上,从而读取到写入的值注入到具体的代码配置中,按照习惯大于约定的原则,主库我们都是注为master,从库注为slave,本项目采用了阿里的druid数据库连接池,使用build建造者模式创建DataSource对象,DataSource就是代码层面抽象出来的数据源,接着需要配置sessionFactory、sqlTemplate、事务管理器等

/** * 主从配置 * *@authorwyq *@date2020年07月24日01:24:42 */@Configuration@MapperScan(basePackages ="com.wyq.mysqlreadwriteseparate.mapper", sqlSessionTemplateRef ="sqlTemplate")publicclassDataSourceConfig{/**    * 主库    */@Bean@ConfigurationProperties(prefix ="spring.datasource.master")publicDataSource master() {returnDruidDataSourceBuilder.create().build();    }/**    * 从库    */@Bean@ConfigurationProperties(prefix ="spring.datasource.slave")publicDataSource slaver() {returnDruidDataSourceBuilder.create().build();    }/**    * 实例化数据源路由    */@BeanpublicDataSourceRouter dynamicDB(@Qualifier("master")DataSource masterDataSource,@Autowired(required = false)@Qualifier("slaver")DataSource slaveDataSource) {        DataSourceRouter dynamicDataSource = new DataSourceRouter();        Map targetDataSources = new HashMap<>();        targetDataSources.put(DataSourceEnum.MASTER.getDataSourceName(), masterDataSource);if(slaveDataSource !=null) {            targetDataSources.put(DataSourceEnum.SLAVE.getDataSourceName(), slaveDataSource);        }        dynamicDataSource.setTargetDataSources(targetDataSources);        dynamicDataSource.setDefaultTargetDataSource(masterDataSource);returndynamicDataSource;    }/**    * 配置sessionFactory    *@paramdynamicDataSource    *@return*@throwsException    */@BeanpublicSqlSessionFactory sessionFactory(@Qualifier("dynamicDB")DataSource dynamicDataSource) throws Exception {        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();        bean.setMapperLocations(                new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/*Mapper.xml"));        bean.setDataSource(dynamicDataSource);returnbean.getObject();    }/**    * 创建sqlTemplate    *@paramsqlSessionFactory    *@return*/@BeanpublicSqlSessionTemplate sqlTemplate(@Qualifier("sessionFactory")SqlSessionFactory sqlSessionFactory) {returnnew SqlSessionTemplate(sqlSessionFactory);    }/**    * 事务配置    *    *@paramdynamicDataSource    *@return*/@Bean(name ="dataSourceTx")publicDataSourceTransactionManager dataSourceTransactionManager(@Qualifier("dynamicDB")DataSource dynamicDataSource) {        DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();        dataSourceTransactionManager.setDataSource(dynamicDataSource);returndataSourceTransactionManager;    }}

二: 数据源路由的配置

路由在主从分离是非常重要的,基本是读写切换的核心。Spring提供了AbstractRoutingDataSource 根据用户定义的规则选择当前的数据源,作用就是在执行查询之前,设置使用的数据源,实现动态路由的数据源,在每次数据库查询操作前执行它的抽象方法 determineCurrentLookupKey() 决定使用哪个数据源,为了能有一个全局的数据源管理器,此时我们需要引入DataSourceContextHolder这个数据库上下文管理器,可以理解为全局的变量,随时可取(见下面详细介绍),它的主要作用就是保存当前的数据源;

publicclassDataSourceRouterextendsAbstractRoutingDataSource{/**    * 最终的determineCurrentLookupKey返回的是从DataSourceContextHolder中拿到的,因此在动态切换数据源的时候注解    * 应该给DataSourceContextHolder设值    *    *@return*/@OverrideprotectedObjectdetermineCurrentLookupKey(){returnDataSourceContextHolder.get();    }}

三:数据源上下文环境

数据源上下文保存器,便于程序中可以随时取到当前的数据源,它主要利用ThreadLocal封装,因为ThreadLocal是线程隔离的,天然具有线程安全的优势。这里暴露了set和get、clear方法,set方法用于赋值当前的数据源名,get方法用于获取当前的数据源名称,clear方法用于清除ThreadLocal中的内容,因为ThreadLocal的key是weakReference是有内存泄漏风险的,通过remove方法防止内存泄漏;

/** * 利用ThreadLocal封装的保存数据源上线的上下文context */publicclassDataSourceContextHolder{privatestaticfinal ThreadLocal context =newThreadLocal<>();/**    * 赋值    *    * @param datasourceType    */publicstaticvoidset(String datasourceType){        context.set(datasourceType);    }/**    * 获取值    * @return    */publicstaticStringget(){returncontext.get();    }publicstaticvoidclear(){        context.remove();    }}

四:切换注解和Aop配置

首先我们来定义一个@DataSourceSwitcher注解,拥有两个属性①当前的数据源②是否清除当前的数据源,并且只能放在方法上,(不可以放在类上,也没必要放在类上,因为我们在进行数据源切换的时候肯定是方法操作),该注解的主要作用就是进行数据源的切换,在dao层进行操作数据库的时候,可以在方法上注明表示的是当前使用哪个数据源;

@DataSourceSwitcher注解的定义:

@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.METHOD)@Documentedpublic@interfaceDataSourceSwitcher {/**    * 默认数据源    * @return    */DataSourceEnumvalue()defaultDataSourceEnum.MASTER;/**    * 清除    * @return    */booleanclear()defaulttrue; }

DataSourceAop配置

为了赋予@DataSourceSwitcher注解能够切换数据源的能力,我们需要使用AOP,然后使用@Aroud注解找到方法上有@DataSourceSwitcher.class的方法,然后取注解上配置的数据源的值,设置到DataSourceContextHolder中,就实现了将当前方法上配置的数据源注入到全局作用域当中;

@Slf4j@Aspect@Order(value = 1)@ComponentpublicclassDataSourceContextAop{@Around("@annotation(com.wyq.mysqlreadwriteseparate.annotation.DataSourceSwitcher)")publicObject setDynamicDataSource(ProceedingJoinPoint pjp) throws Throwable {        boolean clear =false;try{            Method method =this.getMethod(pjp);            DataSourceSwitcher dataSourceSwitcher = method.getAnnotation(DataSourceSwitcher.class);clear=dataSourceSwitcher.clear();            DataSourceContextHolder.set(dataSourceSwitcher.value().getDataSourceName());            log.info("数据源切换至:{}", dataSourceSwitcher.value().getDataSourceName());returnpjp.proceed();        }finally{if(clear) {                DataSourceContextHolder.clear();            }        }    }privateMethod getMethod(JoinPoint pjp) {        MethodSignature signature = (MethodSignature) pjp.getSignature();returnsignature.getMethod();    } }

五:用法以及测试

在配置好了读写分离之后,就可以在代码中使用了,一般而言我们使用在service层或者dao层,在需要查询的方法上添加@DataSourceSwitcher(DataSourceEnum.SLAVE),它表示该方法下所有的操作都走的是读库;在需要update或者insert的时候使用@DataSourceSwitcher(DataSourceEnum.MASTER)表示接下来将会走写库。其实还有一种更为自动的写法,可以根据方法的前缀来配置AOP自动切换数据源,比如update、insert、fresh等前缀的方法名一律自动设置为写库,select、get、query等前缀的方法名一律配置为读库,这是一种更为自动的配置写法。缺点就是方法名需要按照aop配置的严格来定义,否则就会失效

@ServicepublicclassOrderService{@ResourceprivateOrderMapper orderMapper;/**    * 读操作    *    *@paramorderId    *@return*/@DataSourceSwitcher(DataSourceEnum.SLAVE)publicList getOrder(String orderId) {returnorderMapper.listOrders(orderId);    }/**    * 写操作    *    *@paramorderId    *@return*/@DataSourceSwitcher(DataSourceEnum.MASTER)publicList insertOrder(LongorderId) {        Order order = new Order();        order.setOrderId(orderId);returnorderMapper.saveOrder(order);    }}

1、MySQL主从复制

但我们仔细观察我们会发现,当我们的项目都是用的单体数据库时,那么就可能会存在如下问题:

读和写所有压力都由一台数据库承担, 压力大

数据库服务器磁盘损坏则 数据丢失 ,单点故障

为了解决上述提到的两个问题,我们可以准备两 (多) 台MySQL,一台主( Master )服务器,一台从( Slave )服务器,主库的 数据变更 (写、更新、删除这些操作) ,需要 同步 到从库中 (主从复制) 。而用户在访问我们项目时,如果是 写操作 (insert、update、delete),则直接操作 主库 ;如果是 读操作 (select) ,则直接操作从库,这种结构就是 读写分离 啦。

在这种读写分离的结构中,从库是可以有多个的

介绍

MySQL主从复制是一个 异步 的复制过程,底层是基于Mysql数据库自带的 二进制日志 功能。就是一台或多台MySQL数据库(slave,即 从库 )从另一台MySQL数据库(master,即 主库 )进行日志的复制,然后再解析日志并应用到自身,最终实现 从库 的数据和 主库 的数据保持一致。MySQL主从复制是 MySQL数据库自带功能,无需借助第三方工具。

二进制日志:

二进制日志(BINLOG)记录了所有的 DDL(数据定义语言)语句和 DML(数据操纵语言)语句,但是不包括数据查询语句。此日志对于灾难时的数据恢复起着极其重要的作用,MySQL的主从复制, 就是通过该binlog实现的。默认MySQL是未开启该日志的。

MySQL复制过程分成三步:

MySQL master 将数据变更写入二进制日志( binary log )

slave将master的binary log拷贝到它的中继日志( relay log )

slave重做中继日志中的事件,将数据变更反映它自己的数据

1.2、主从库搭建

在环境搭建之前,我们需要准备好两台服务器,如果生活富裕使用的是两台云服务器的时候记得要开放安全组,即防火墙;如果是比狗子我生活好点但也是用的虚拟机的话,记得别分这么多内存启动蓝屏了(别问怎么知道的)

这里就不给大家展示数据库的安装和防火墙的操作了,这个我感觉网上好多资源都能够满足遇到的问题,在搭建主从库的时候有在网上见到过说MySQL版本要一致的,我也没太留意直接就在之前的MySQL上操作了,大家可以自己去验证一下。

1.2.1、主库配置

服务器:192.168.150.100(别试了黑不了的,这是虚拟机的ip)

1、修改Mysql数据库的配置文件 vim /etc/my.cnf

在打开的文件中加入下面两行,其中的server-id不一定是100,确保唯一即可log-bin=mysql-bin#[必须]启用二进制日志server-id=100#[必须]服务器唯一ID复制代码

2、重启Mysql服务

这里有三个方法都能重启MySQL,最简单的无疑就是一关一开:

netstopmysql;netstartmysql;systemctl restart mysqldservice mysqld restart复制代码

3、创建数据同步的用户并授权

登录进去MySQL之后才能够执行下面的命令,因为这是SQL命令,Linux不认识这玩意是啥。

GRANTREPLICATIONSLAVEON*.*to'用户名'@'开放的地址'identifiedby'密码';eg:GRANTREPLICATIONSLAVEON*.*to'masterDb'@'%'identifiedby'Master@123456';记得刷一下权限FLUSHPRIVILEGES;复制代码

4、查看master同步状态

这个时候还 不用退出MySQL ,因为下面的命令还是SQL命令,执行下面的SQL,可以拿到我们后面需要的两个重要参数。

showmasterstatus;复制代码

执行完这一句SQL之后,==不要再操作主库!不要再操作主库!不要再操作主库!==重要的事情说三遍,因为再操作主库之后可能会导致红框中的 两个属性值会发生变化 ,后面如果发生了错误可能就和这里有那么两毛钱关系了。

1.2.2、从库配置

服务器:192.168.150.101(别试了黑不了的,这也是虚拟机的ip)

1、 修改Mysql数据库的配置文件 vim /etc/my.cnf

这里要注意server-id和主库以及其他从库都不能相同,否则后面将会配置不成功。

2、重启Mysql服务

这里有三个方法都能重启MySQL,最简单的无疑就是一关一开:

netstopmysql;netstartmysql;systemctl restart mysqldservice mysqld restart复制代码

3、设置主库地址及同步位置

登录进去MySQL之后才能够执行下面的命令,因为这是SQL命令

设置主库地址和同步位置changemastertomaster_host='192.168.150.100',master_user='masterDb',master_password='Master@123456',master_log_file='mysql-bin.000010',master_log_pos=68479;记得记得开启从库配置startslave;复制代码

参数说明:

master_host: 主库的 IP地址

master_user: 访问主库进行主从复制的 用户名 ( 上面在主库创建的 )

master_password: 访问主库进行主从复制的用户名对应的 密码

master_log_file: 从哪个 日志文件 开始同步 ( 即1.2.1中第4步获取的 File )

master_log_pos: 从指定日志文件的哪个 位置 开始同步 ( 即1.2.1中第4步获取的 Position )

4、查看从数据库的状态

这个时候还 不用退出MySQL ,因为下面的命令还是SQL命令,执行下面的SQL,可以看到从库的状态信息。通过状态信息中的 Slave_IO_running 和 Slave_SQL_running 可以看出主从同步是否就绪,如果这两个参数全为 Yes ,表示主从同步已经配置完成。

showslavestatus\G;复制代码

坑位介绍

1.3.1、UUID报错

这可能是由于linux 是复制出来的,MySQL中还有一个 server_uuid 是一样的,我们也需要修改。 vim /var/lib/mysql/auto.cnf

1.3.2、server_id报错

这应该就是各位大牛设置server_id的时候不小心设置相同的id了,修改过来就行,步骤在上面的配置中。

1.3.3、同步异常解决

这是狗子在操作过程中搞出来的一个错误……

出错的原因是在主库中删除了用户信息,但是在从库中同步的时候失败导致同步停止,下面记录自己的操作(是在进入MySQL的操作且是从库)。

MASTER_LOG_POS复制代码

STOPSLAVE;SETGLOBALSQL_SLAVE_SKIP_COUNTER=1;STARTSLAVE;SHOWSLAVESTATUS\G;复制代码

在数据库中操作时,一定要注意当前所在的数据库是哪个,作为一个良好的实践:在SQL语句前加 USE dbname 。

操作不规范,亲人两行泪……

项目中实现

2.1、ShardingJDBC

Sharding-JDBC定位为 轻量级Java框架 ,在Java的JDBC层提供的额外服务。 它使用客户端直连数据库,以 jar包 形式提供服务,无需额外部署和依赖,可理解为增强版的JDBC驱动, 完全兼容JDBC和各种ORM框架 。

使用Sharding-JDBC可以在程序中轻松的实现数据库 读写分离 。

Sharding-JDBC具有以下几个特点:

适用于任何基于JDBC的ORM框架,如:JPA, Hibernate, Mybatis, Spring JDBC Template或直接使用JDBC。

支持任何第三方的数据库连接池,如:DBCP, C3P0, BoneCP, Druid, HikariCP等。

支持任意实现JDBC规范的数据库。目前支持MySQL,Oracle,SQLServer,PostgreSQL以及任何遵循SQL92标准的数据库。

下面我们将用ShardingJDBC在项目中实现MySQL的读写分离。

2.2、依赖导入

在pom.xml文件中导入ShardingJDBC的依赖坐标

<!--sharding-jdbc-->org.apache.shardingspheresharding-jdbc-spring-boot-starter4.0.0-RC1复制代码

2.3、配置文件

在application.yml中增加数据源的配置

spring:shardingsphere:datasource:names:master,slave# 主数据源master:type:com.alibaba.druid.pool.DruidDataSourcedriver-class-name:com.mysql.cj.jdbc.Driverurl:jdbc:mysql://192.168.150.100:3306/db_test?useUnicode=true&characterEncoding=utf-8&useSSL=falseusername:rootpassword:123456# 从数据源slave:type:com.alibaba.druid.pool.DruidDataSourcedriver-class-name:com.mysql.cj.jdbc.Driverurl:jdbc:mysql://192.168.150.101:3306/db_test?useUnicode=true&characterEncoding=utf-8&useSSL=falseusername:rootpassword:123456masterslave:# 读写分离配置,设置负载均衡的模式为轮询load-balance-algorithm-type:round_robin# 最终的数据源名称name:dataSource# 主库数据源名称master-data-source-name:master# 从库数据源名称列表,多个逗号分隔slave-data-source-names:slaveprops:sql:show:true#开启SQL显示,默认false# 覆盖注册bean,后面创建数据源会覆盖前面创建的数据源main:allow-bean-definition-overriding:true复制代码

2.4、测试跑路

这时我们就可以对我们项目中的配置进行一个测试,下面分别调用一个更新接口和一个查询接口,通过查看日志中记录的数据源来判断是否能够按照我们预料中的跑。

更新操作(写操作)

查询操作(读操作)

搞定!!!程序正常按照我们预期的成功跑起来了,成功借助ShardingJDBC在我们项目中实现了数据库的读写分离

本文就结束啦,希望大家能从文章中得到帮助获得收获,也可以评论出你想看哪方面的技术。文章会持续更新,希望能帮助到大家,哪怕是让你灵光一现。有什么不对的或者我没有说到的欢迎评论探讨,我也会虚心学习!喜欢的朋友可以点点赞和关注,也可以分享出去让更多的人看见,一起努力一起进步!

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容