一、数据垂直分表原理
1、分表方式
**水平分表:**按行对数据进行拆分,根据某个分片算法(范围法、Hash法)将记录分别存放在不同表中,通过大表拆小表的方式缩小数据查询范围、提高处理效率,所有分表的表结构完全相同。
bash
Hash法举例:
将1张大表分成3张小表,可以根据数据的id % 3 = x,x的值只能为0、1、2,由此将数据分配到三张表中。
范围法:
按照时间分表:年、月等
**垂直分表:**将一张大表分成若干小表,小表表结构不同,通过主键进行关联,如将一张商品信息表分为基本信息表和详情信息表,通过商品id做关联。
2、垂直分表原理
MySQL Innodb存储引擎中,存储数据的基本单位为"行 (Row)",管理数据的基本单位位"页 (Page)",默认每一页都是固定的16K 大小,一页内允许存储某一张表的若干行数据,页内部数据 存储是紧密的,检索效率非常高。
MySQL中用于保存页的单位被称为"区 (Extent)",区由连续的页组成,默认情况下一个区有1MB的存储空间,也就是一个区默认最多可以有64个连续的页。
bash
若干数据行 -> 页(大小固定16K)*64 -> 区(大小固定1MB)
从Innodb1.0开始,因为引入了压缩页 这些新特性,从存储空间上页实际占用的空间会更少 ,但也带来新问题,因为压缩解压缩都要占用CPU与IO资源 ,导致跨页检索效率低于页内检索效率 ,要尽可能保证每一页尽可能多存储一些行,这样检索效率会更高,这是设计优化的理论支撑。
垂直分表通过将重要字段单独剥离出一张小表(必须作为驱动表,连接查询时一定要让小表驱动大表),让每一页能够容纳更多的行,进而缩小数据扫描的范围,达到提高执行效率的目的。
3、垂直分表依据
垂直分表是对于多字段大表的优化措施,通常要满足以下两个条件:
- 单表数据量未来可能千万
- 字段超过20个,且包含了超长的Varchar、CLOB、BLOB等字段
谨记小表驱动大表的规则,小表应保留重要字段(数据查询、排序时需要的字段,高频访问的小字段),大表应保留低频访问字段和大字段。
二、多级缓存架构
在分布式架构下缓存在每一层都有自己的设计。如图从上到下包含四层,分别为:客户端、应用层、服务层以及数据层。

1、客户端缓存
以客户端为浏览器举例,在浏览器层主要对HTML中的图片、CSS、JS、字体这些静态资源进行缓存,将这些资源缓存到本地,并设置过期时间Expires。
2、应用层缓存
浏览器是客户端,只负责读取Expires响应头,不负责存储Expires。Expires存储在应用层,也就是在CDN与Nginx中进行设置。
2.1 CDN(内容分发网络)
全称是Content Delivery Network,即内容分发网络,是互联网静态资源分发的主要技术手段。
CDN服务器缓存较远地区的静态文件,使得所在地发起的访问可以就近获取静态资源。
CDN技术的核心是"智能DNS",智能DNS会根据用于的IP地址自动确定就近CDN节点,如下图。

在互联网应用中,因为CDN涉及多地域多节点组网,前期投入成本较高,更多的中小型软件公司通常会选择阿里云、腾讯云等大厂提供的CDN服务,通过按需付费的方式降低硬件成本。而这些服务商又会为CDN赋予额外的能力,比如阿里云、腾讯云CDN除了缓存文件之外,还提供了管理后台能为响应赋予额外的响应头。如下所示在阿里云CDN后台,就额外设置了Cache-Control响应头代表缓存有效期为1小时。这里我们额外提一下Expires与的Cache-Control的区别,Expires是指定具体某个时间点缓存到期,而Cache-Control则代表缓存的有效期是多长时间。Expires设置时间,Cache-Control设置时长,根据业务场景不同可以使用不同的响应头。
2.2 Nginx缓存
在互联网应用中,用户分布在全国各地,对资源的响应速度与带宽要求较高,因此部署CDN是十分有必要的。但在更多的企业应用中,其实大部分的企业用户都分布在指定的办公区域或者相对固定的场所,再加上并发用户相对较少,其实并不需要额外部署CDN这种重量级解决方案。在架构中只需要部署Nginx服务器,利用Nginx自带的静态资源缓存与压缩功能便可胜任大多数企业应用场景。
在Nginx中自带将后端应用中图片、CSS、JS等静态资源缓存功能,只需在Nginx的核心配置nginx.conf中增加下面的片段,便可对后端的静态资源进行缓存。
bash
# 设置缓存目录
# levels代表采用1:2也就是两级目录的形式保存缓存文件(静态资源css、js)
# keys_zone定义缓存的名称及内存的使用,名称为babytun-cache ,在内存中开始100m交换空间
# inactive=7d 如果某个缓存文件超过7天没有被访问,则删除
# max_size=20g;代表设置文件夹最大不能超过20g,超过后会自动将访问频度(命中率)最低的缓存文件删除
proxy_cache_path d:/nginx-cache levels=1:2 keys_zone=babytun-cache:100m inactive=7d max_size=20g;
#配置xmall后端服务器的权重负载均衡策略
upstream xmall {
server 192.168.31.181 weight=5 max_fails=1 fail_timeout=3s;
server 192.168.31.182 weight=2;
server 192.168.31.183 weight=1;
server 192.168.31.184 weight=2;
}
server {
#nginx通过80端口提供Web服务
listen 80;
# 开启静态资源缓存
# 利用正则表达式匹配URL,匹配成功的则执行内部逻辑
# ~* 代表URL匹配不区分大小写
location ~* \.(gif|jpg|css|png|js|woff|html)(.*){
# 配置代理转发规则
proxy_pass http://xmall;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_cache xmall-cache;
#如果静态资源响应状态码为200(成功) 302(暂时性重定向)时 缓存文件有效期1天
proxy_cache_valid 200 302 24h;
#301(永久性重定向)缓存保存5天
proxy_cache_valid 301 5d;
#其他情况
proxy_cache_valid any 5m;
#设置浏览器端缓存过期时间90天
expires 90d;
}
#使用xmall服务器池进行后端处理
location /{
proxy_pass http://xmall;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
增加上面配置后,每一次通过nginx访问xmall应用新的静态文件时,在Nginx服务的缓存目录便会生成缓存文件,在缓存有效期内该静态资源的请求便不再送到后端服务器,而直接由Nginx读取本地缓存并返回。
3、服务层缓存
对于后端应用与服务的缓存可以按部署方式分为进程内缓存 与分布式缓存服务。
3.1 进程内缓存
进程内缓存是在应用中开辟的一块内存空间,数据在运行时被载入这块内存,通过本地内存的低延迟、高吞吐的特性提高程序的访问速度。
进程内缓存在众多Java框架内都有广泛应用,例如Hibernate、Mybatis框架的一二级缓存、Spring MVC的页面缓存都是进程内缓存的经典应用场景。
Java中的代表性产品有EhCache、Caffeine。
3.2 分布式缓存
与进程内相对的,就是需要独立部署的分布式缓存服务。最常用的是基于Redis这种内存型NoSQL数据库,对整体架构中的应用数据进行集中缓存。在缓存架构设计时,一定要按照由近到远、由快到慢的顺序进行逐级访问。
以电商平台为例,如果没有本地缓存,所有商品、订单、物流的热点数据都保存在Redis服务器中,每完成一笔订单,都要额外增加若干次网络通信,网络通信本身就可能由于各种原因存在通信失败的问题。即便是你能保证网络100%可用,但Redis集群承担了来自所有外部应用的访问压力,一旦突发流量超过Redis的负载上限,整体架构便面临崩溃的风险。
因此在Java的应用端也要设计多级缓存 ,将进程内缓存与分布式缓存服务结合,有效分摊应用压力。在Java应用层面,只有EhCache的缓存不存在时,再去Redis分布式缓存获取,如果Redis也没有此数据再去数据库查询,数据查询成功后对Redis与EhCahce同时进行双写更新。这样Java应用下一次再查询相同数据时便直接从本地EhCache缓存提取,不再产生新的网络通信,应用查询性能得到显著提高。
但引入多级缓存后,又会遇到缓存数据一致性的挑战。可以在当前架构中引入MQ消息队列,利用RocketMQ的主动推送功能来向其他服务实例以及Redis缓存服务发起变更通知。

3.3 多级缓存应用场景
(1)缓存的数据是稳定的。例如邮政编码、地域区块、归档的历史数据这些信息适合通过多级缓存减小Redis与数据库的压力。
(2)瞬时可能会产生极高并发的场景。例如春运购票、双11零点秒杀、股市开盘交易等,瞬间的流量洪峰可能击穿Redis缓存,产生流量雪崩。这时利用预热的进程内缓存分摊流量,减少后端压力是非常有必要的。
(3)一定程度上允许数据不一致。例如某博客平台中修改了自我介绍这样的非关键信息,此时在应用集群中其他节点缓存不一致也并不会带来严重影响,对于这种情况我们采用T+1的方式在日终处理时保证缓存最终一致就可以了。
三、布隆过滤器的应用
主要用于解决缓存穿透:短时间内大量不存在的数据被查询,导致大量请求被发送至数据库,当请求数量超过数据库负载上限时,系统响应出现高延迟,甚至瘫痪。
原理
使用布隆过滤器之前要初始化数据,将数据加载布隆过滤器中。
- 布隆过滤器本质是一个N位的二进制数组,没有数据初始化时,数组的每一位都是0。
- 加载数据时,将所有数据依次进行若干次的Hash,Hash值会定位到二进制数组的不同位置,如果对应的位置是0则将其改为1,如果是1则不变。
- 查询时,将要查询的数据进行同等次数的Hash,根据其Hash值定位的位置,查看数组对应位置上的值,如果都是1则数据可能存在 ,如果有一个是0则数据一定不存在。
减少误判的措施
- 增加二进制数组位数
- 增加Hash次数
Java中的使用
XML
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-all</artifactId>
<version>3.16.0</version>
</dependency>
java
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
//构造Redisson
RedissonClient redisson = Redisson.create(config);
RBloomFilter<String> bloomFilter = redisson.getBloomFilter("bloom");
//初始化布隆过滤器:预计元素为1000000L,误判率为1%
bloomFilter.tryInit(1000000L,0.01);
bloomFilter.add("1"); //增加数据
//判断指定编号是否在布隆过滤器中
System.out.println(bloomFilter.contains("1")); //输出true
System.out.println(bloomFilter.contains("8888"));//输出false
布隆过滤器在项目中的使用流程
- 应用启动时,初始化布隆过滤器
- 用户发来请求时,判断布隆过滤器是否包含编号
- 包含编号,从Redis读取数据;Redis中不存在数据时,从数据库读取并存入Redis
- 不包含编号,返回数据不存在
数据被删了怎么办
布隆过滤器的某位二进制可能被多个编号Hash引用,因此无法直接处理删除数据的情况
- 方案1:定时异步重构布隆过滤器
- 方案2:计数Bloom Fliter(Hash对应的二进制位置,每次对应到都+1,删除数据后,对应位置-1)