随着业务的规模不断扩大,数据量的成倍增长,查询效率越来越低,单个数据库已经不足以支撑大量并发请求,严重甚至导致服务器宕机,这个时候我们就需要做些优化了
主要从以下几个方面优化数据库:
- 业务拆分
- 读写分离
- 数据库分库与分表
业务拆分
对于电商项目,会包含用户、商品、订单、仓库等模块,项目初期我们可能是放在同一个数据库来存储的扩大,单库压力非常大,这个时候我们可以根据业务拆分为多个数据库,这样系统的吞吐量大大提升。,但是随着规模
如下图:
主从复制,读写分离
数据库写入效率要低于读取效率,一般系统中数据读取频率高于写入频率,单个数据库实例在写入的时候会影响读取性能,这是做读写分离的原因。
实现方式主要基于MySQL的主从复制,通过路由的方式使应用对数据库的写请求只在master上进行,读请求在slave上进行。
主从复制怎么配置
同步原理:Mysql服务器之间的主从同步是基于二进制日志机制,主服务器使用二进制日志来记录数据库的变动情况,从服务器通过读取和执行该日志文件来保持和主服务器的数据一致。
有很多种配置主从同步的方法,可以总结为如下的步骤:
- 在主服务器上,必须开启二进制日志机制和配置一个独立的ID
- 在每一个从服务器上,配置一个唯一的ID,创建一个用来专门复制主服务器数据的账号
- 在开始复制进程前,在主服务器上记录二进制文件的位置信息
- 如果在开始复制之前,数据库中已经有数据,就必须先创建一个数据快照(可以使用mysqldump导出数据库,或者直接复制数据文件)
- 配置从服务器要连接的主服务器的IP地址和登陆授权,二进制日志文件名和位置
具体步骤:https://blog.csdn.net/qq_38727626/article/details/82632649
读写分离代码实现
思路:之前在做项目的时候实现过mybatis数据源的动态切换。基于原来的方案,用aop来拦截dao层方法,根据方法名称就可以判断要执行的sql类型,动态切换主从数据源。
数据源动态切换可以看这里:https://blog.csdn.net/qq_43037478/article/details/109648880
aop实现
/**
* 数据源切换类
*/
@Aspect
@Component
public class DataSourceAspect {
private static final Logger logger = LoggerFactory.getLogger(DataSourceAspect.class);
private static final String[] queryStrs = {"query", "select", "get"};
/**
* 定义切入点,切入点为com.angla.demo.dao下的所有方法
*/
@Pointcut("execution(* com.xiateng.dao.*(..))")
public void executeSql(){}
@Before("executeSql()")
public void doBefore(JoinPoint joinPoint){
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
String mName = methodSignature.getMethod().getName();
logger.info("拦截sql方法:{}", mName);
// 默认设置pre为Master
DataSourceContext.setDataSource(StaticConst.DATA_SOURCE_MASTER);
for (String name : queryStrs) {
if (mName.startsWith(name)) {
logger.info("查询语句,设置数据源为slave");
DataSourceContext.setDataSource(StaticConst.DATA_SOURCE_SLAVE);
break;
}
}
logger.info("当前数据源:{}",DataSourceContext.getDataSource());
}
}
分库分表
1、分表
垂直拆分
概念:以 字段为依据,按照字段的活跃性,将 表中字段拆到不同的 表(主表和扩展表)中。
结果:
- 每个 表的 结构都不一样;
- 每个 表的 数据也不一样,一般来说,每个表的 字段至少有一列交集,一般是主键,用于关联数据;
- 所有 表的 并集是全量数据;
场景:系统绝对并发量并没有上来,表的记录并不多,但是字段多,并且热点数据和非热点数据在一起,单行数据所需的存储空间较大。以至于数据库缓存的数据行减少,查询时会去读磁盘数据产生大量的随机读IO,产生IO瓶颈。
分析:可以用列表页和详情页来帮助理解。垂直分表的拆分原则是将热点数据(可能会冗余经常一起查询的数据)放在一起作为主表,非热点数据放在一起作为扩展表。这样更多的热点数据就能被缓存下来,进而减少了随机读IO。拆了之后,要想获得全部数据就需要关联两个表来取数据。但记住,千万别用join,因为join不仅会增加CPU负担并且会讲两个表耦合在一起(必须在一个数据库实例上)。关联数据,应该在业务Service层做文章,分别获取主表和扩展表数据然后用关联字段关联得到全部数据。
水平拆分
概念:以 字段为依据 ,按照一定策略(hash、range等),将一个 表中的数据拆分到多个 表中
结果:
- 每个 表的 结构都一样;
- 每个 表的 数据都不一样,没有交集;
- 所有 表的 并集是全量数据;
场景:系统绝对并发量并没有上来,只是单表的数据量太多,影响了SQL效率,加重了CPU负担,以至于成为瓶颈。
分析:表的数据量少了,单次SQL执行效率高,自然减轻了CPU的负担。
2、分库
水平分库
概念:以 字段为依据 ,按照一定策略(hash、range等),将一个 库中的数据拆分到多个 库中。
结果:
- 每个 库的 结构都一样;
- 每个 库的 数据都不一样,没有交集;
- 所有 库的 并集是全量数据;
场景:系统绝对并发量上来了,分表难以根本上解决问题,并且还没有明显的业务归属来垂直分库。
分析:库多了,io和cpu的压力自然可以成倍缓解。
垂直分库
概念:以 表为依据,按照业务归属不同,将不同的 表拆分到不同的 库中 。
结果:
- 每个 库的 结构都不一样;
- 每个 库的 数据也不一样,没有交集;
- 所有 库的 并集是全量数据;
场景:系统绝对并发量上来了,并且可以抽象出单独的业务模块。
分析:到这一步,基本上就可以服务化了。例如,随着业务的发展一些公用的配置表、字典表等越来越多,这时可以将这些表拆到单独的库中,甚至可以服务化。再有,随着业务的发展孵化出了一套业务模式,这时可以将相关的表拆到单独的库中,甚至可以服务化。
分库分表工具
sharding-sphere:jar,前身是sharding-jdbc;
TDDL:jar,Taobao Distribute Data Layer;
Mycat:中间件。
分库分表步骤
根据容量(当前容量和增长量)评估分库或分表个数 -> 选key(均匀)-> 分表规则(hash或range等)-> 执行(一般双写)-> 扩容问题(尽量减少数据的移动)。