数据库设计
数据库设计
数据库范式。等级越高、越严格。
第一范式:不可拆分
反例:尽量不要用json,一般是可以单独做成一张表的。先看设计是否合理,再看合适的存储组件
第二范式:必须有主键,非主属性,必须依赖于主属性
反例:好友关系列表。主键:关注人id,被关注人Id,关注人头像
第三范式:传递依赖
反例:员工表:员工id。部门id,部门名称。部门简介
反范式:提供效率,做一些不符合范式的表,来减少查询关联表的次数,提高效率
如果你的系统是重业务的系统,对性能和并发的要求不高,最好用第三方范式。
如果系统对某些查询较频繁,可以考虑冗余,反范式。
巨量数据的优化
数据量增加—》响应时间的增加。
常用方法:建立索引
表分区、分库分表、读写分离
表分区
表。每个表对应磁盘上的一个文件。ibd。data目录下
分流:对一个ibd文件的请求,分发到对多个ibd文件的请求。
将数据多个物理表进行存储,逻辑上,还是一张表
好处:
- 当查询条件 可 以判定 某个数据位于哪个分区,那么直接在分区中查询,不用扫描整个表。
- 业务代码不用改。
- 分区进行单独管。备份,恢复。
分区是可以通过navicat实现的
range。0-10,11-20。
partition by range
list: partition by list xxxxxxxxxxx values in (1,2)
hash:partion by hash
分区注意事项
尽量保证常用查询落到一个分区中
分区条件放到where上
坏处:如果出现跨区查询,适得其反
分库分表
目的:
1.业务拆分
2.适应不同的场景,读多写少,读少写多
3.数据隔离。核心业务和非核心业务拆分
怎么拆
水平分的话一般就是多个字段一般不能拆开,需要频繁一起查询
婚恋网站查询最多的就是年龄和性别,就可以垂直分
以系统能不能扛得住为主,先用简单方法,解决不了再复杂
分库分表的方法
范围分区:选取有序的列,按照一定的范围去划分
选取有序的列,按照一定的范围去划分。
就是分段大小的选取
分段小:导致子表数量多,增加维护难度
分段大:有可能单表依然存在性能问题
依据就是:分表后,表的各方面性能,能否满足系统要求
优点:可以平滑的扩充新表,只需增加子表的数据量,原有的数据不用动。
缺点:数据分布不均匀,指的是数据量的不均匀,因为它选取的有序的列,按照固定的范围划分。
hash分区:选取列,进行hash运算。%10=
优点:相反
缺点:相反
分库分表的问题
分布式ID问题
拆分维度问题
用户id查的快,订单id查的快,所以分的时候不要把用户id拿出去,具体看哪个字段用的多
创建一个索引表、映射表。订单id查得慢,就可以用订单id和用户id做一张关联表,这样就可以快了(过渡方案)
join问题
商品、订单、用户原来在一张表里
分库后通过sql的join就能查询,购买了化妆品的用户中的女用户。如果这几张表分布在不同的库里就尴尬了
单库join没问题,多库join失败
解决方式
- 在代码层面做join。
- es。canal->es。
目前:数据库越简单越好。 禁止3张表 做关联,禁用存储过程,禁用触发器。
事务
分库了,分布式事务的解决方案,XA的jar包可以查两个库做2pc的提交,但是不好用。用shardingsphere更好
成本问题:
非必要不分库,不要过度设计
XA,jar
哪怕1个亿数据,只要查询速度ok,也不用分库分表
读写分离
分流:将请求分流。防止阻塞。
数据库锁(分)对数据库并发有影响
X锁,写锁,只能我一人,读、写干不了
S锁:读锁
场景:读多写少,适合读写分离。读少写多反而用途不大
一主(写)多从(读)
只要有select分配到读库里读
路由问题
select S 锁 从库 ; create update delete add. X 锁 写库。 mybatis插件。
主从复制问题
- 写主库,如何高效同步到从库
从库用sql命令写,会失去读写分离的意义,因为回到了从前
主库:binlog,传给从库,写入从库的relayLog,解析relayLog。重现数据
- 会有时间差
(一)
编造:注册(主库),登录(查从库),此时提示用户不存在
注册后第一次查询指到主库去
如何看出是第一次注册:注册完,写个redis,key用户id1.删掉key
(二)
或者先去从库查,没有再去主库查
主业务用主库,非主业务用从库
读写分离、分库分表实现
读写分离
将不同的sql 分发到不同的机器上。
select-----某台机器
分库分表
where id = 1某台机器上
拦截、判断、分发
代码封装:dao抽象一层。夹层。tddl(Taobao Distributed Data Layer) 头都大了。
Shardingjdbc。需要改代码。
中间件封装:mycat。
缓存设计
缓存设计
导流:将原本复杂的操作请求(sql 大堆),引导到简单的请求上。前人栽树后人乘凉。
空间换时间的做法
什么数据用于做缓存?
读多写少
redis, memcached,localcache guava,客户端缓存,
user_info_xxxx : 姓名,年龄,xxx。getKey 内存操作
select * from user where id = xxx。 硬盘IO
缓存收益
要看成本收益,公司的人学的也需要成本
浏览器的缓存也是kv值。用kv值是最简单的
缓存命中的总时间:计算key的时间、查询key的时间、转换值的时间
一般不会拿Redis去做持久化存储的
耗时特别长的查询(复杂sql),读多写少才会划算,不然不值得做缓存
缓存键的设计
单向函数得当就会将碰撞的几率降低(哈希)
给一个输入很容易能算出结果,但是给结果很难算出输入(单向函数)
输入敏感(输入改一点,值就变了)
如何减少查询key的时间
主要取决于物理位置(放在内存还是硬盘)
转换值的时间
先序列化处理,再反序列化,就会比较耗时
或者是纯对象,比较高效,但是会有数据污染,读取缓存中的数据,都是拿的同一个引用,A对这个对象操作,B读这个时候数据就被污染了
实际中:前缀_业务关键信息_后缀
缓存的更新机制
涉及到删除的操作,用查和更新两个操作去想异常情况
只有更新缓存的操作,就只用去想服务器或者网络延迟这方面的原因
被动更新
调用方:
暂存方(缓存):
数据提供方:根据数据提供方的变化,缓存要进行更新。也就是双写一致
更新:要么我去取,要么你给我
被动:有效期到后,再次写入。
数据提供方可能已经被修改了
- 客户端数据,缓存中没有,从提供方获取,写入缓存(有一个过期时间)
- 在t内,所有的查询,都由缓存提供,所有的写直接写数据库
- 当缓存时间到点了,缓存时间才变没有,回到第一步,继续更新缓存
适合对实时性要求不高的场景,比如:商品关注的人数
主动更新
主动:被其他操作直接填充。
数据库:更新数据库
缓存:更新缓存,删除缓存
主动更新的四种方式
更新数据库,更新缓存
从线程安全的角度来考虑
数据不一致的风险比较高,所以一般不采用
数据库先改成1,再改成2
缓存这边,2先到了,先更新缓存为2,后面更新缓存为1
更新数据库,删除缓存
经常采用的方式
删除缓存是为了节省计算时间
业务要求:修改数据库,然后经过大量的计算,才能得出缓存的值。浪费了性能。如果缓存还没用,更浪费。
cache-aside模式
前提:缓存无数据,数据库无数据
A:查询,B:更新
异常流程(读的速度慢了,存在的情况很低)
就是说这个方案也有问题?这次是读的速度慢了?
读比写慢 概率很低,极低。(指的是A去查的速度,因为A写缓存的操作也算是读里面的,比B写的速度还慢)
A查缓存无数据,去读数据库是旧值
B更新数据库,新值
B删除缓存
A将旧值写入缓存(缓存没有,A要往里面写)
如果非要解决,可以再采用延时双删
删除缓存,更新数据库
第一次删除缓存,并没有来得及更新数据库,就进行了查缓存操作,此时没有缓存,就去查数据库,缓存更新为旧值
一般不采用,因为一般读比写快
通过延时双删来解决,过一段时间再把缓存删一遍,程序停止一会再把缓存删掉
会造成吞吐量下降,改成异步的,可以不用等前一个线程睡眠结束
延时双删其中有操作失败了怎么办
删除缓存。失败了不继续了就行
更新数据库。事务回滚
第二次删除缓存。重试删除。当你前面的操作无法回滚时,为了保证后续数据的一致性,(最便宜的做法)只能硬着头发往前走。回去代价太大了(此时事务已经结束,几乎已经无法利用事务回滚)。所以就是重试,用for循环去重试,如果用到重试的地方要借用中间件(消息队列重发消息)
或者用canal解决。binlog开启,会把操作的东西写在binlog日志里,用canal去订阅binlog日志,如果删除成功,没事,如果删除失败就放到消息队列里面,将二次删除key和业务代码解耦
**延时多久?**评估业务时间,读的基础上再过个200ms左右,也不能完全解决
更新缓存,更新数据库
更新了缓存,更新了数据库,但是数据库回滚了
或者是程序挂了,数据库没更新上去
数据不一致的风险比较高,所以一般不采用
先操作完缓存,再操作数据库。当用户来查的时候,缓存里总有数据
一般中小方案也不采用以下两种
一般都是必须要用缓存来作为主存储了
Read/Write Through
缓存改成功了,数据库写失败了。那么读就是读到库里没有的数据了
Write Behind
先往缓存写,缓存写成功直接返回
缓存和数据库同步的操作通过异步的方式进行。不用担心用户读到的数据是错的
数据库的操作非常耗时,当成异步也可以降低写操作的时间,提高系统吞吐量
缓存清理
如果所有的数据都放缓存,命中率100%
缓存比较贵,需要一套机制,来让有限的缓存空间发挥最大的作用
如何判断一个数据在未来被访问的次数
当清理一个数据的时候,它一直被访问,就认为它马上的未来也会被访问
最近刚被写入,也很可能被访问
清理机制
时效性清理机制
有一个生存ttl,当到了过期时间的时候就会被清理掉,为其他的数据腾空空间
set k v ex 10 s
set cookie 过期时间
设置过期时间10s怎么让他到了10s之后就过期呢,利用定时任务轮询,每一段时间查看一下
读一次,记录一次,时间。阈值。
自动清理机制:cookie和redis都有一个过期时间(本质就是轮询)
数目阈值式清理机制
判断缓存中的数量达到一定值,对缓存进行清理
阈值:根据自己的业务来定
采取什么策略去清理
Iru:规律:最近最少使用算法。LinkedHashMap 套。fifo,lru
经常访问的放前面,不常访问的放后面
map:存键值对
一个LinkedListHashMap既可以实现LRU也可以实现FIFO顺序:插入顺序(fifo),访问顺序(lru)
random:随机
fifo:先进先出
利用一个定长的队列来存储就可以了
实际中,在服务器上,缓存可以放到Map中,只有一台服务器上可以用。多个服务器共用需要通过消息中间件。Redis是通过一些配置不是这样实现的。
软引用清理
什么情况下会适时的释放空间,gc。
识别出要清理的缓存,然后清除。
gc root引用判断。
强:哪怕自己oom,不清理(不用)
软:当空间不足的时候,会被回收
弱:
虚:更容易被回收
用软引用,空间不足时进行缓存清理
把值放到SoftReference包装中
缓存清理总结
时效性清理+数目阈值
一个个拆开来回答,时效性清理能保证什么,数目阈值能保证什么
防止短时间内密集的查询导致缓存空间的急剧增大
lru+软引用:保证热数据,最大限度地提高缓存命中率
不建议仅仅使用软引用,缓存的存活与否就失去了控制,全交给了GC
目的:提高缓存命中率,节省空间,提示性能
缓存清理机制总结
缓存风险
不是用的组件越多越好
每增加一个环节就多一分风险,增加缓存也不例外
做架构能不用就不用
缓存穿透
缓存中没有,数据库也没有。
方案:在第一次调用的时候,数据提供方返回一个空值,将空值放到缓存中。
缓存雪崩
大量缓存突然失效,导致大量的请求,倾泻到数据库上,引起数据库压力骤增。
时效式清理:批量缓存,统一时间到期。缓存ttl=(固定时间,结合业务)+随机时间。
软引用清理:某个时间点,空间突然紧张,常用的缓存用强引用,不常用的用软引用。
缓存击穿
高频率的缓存,突然失效,大量请求倾泻到数据库上。
lru:
read write through or write behind.更新机制:无所谓。数据永远留在缓存当中。
缓存预热
read write through or write behind
预热:高频访问的数据,提前准备
打车的时候用的最高频率的功能就是预估价格
计价规则,提前加载到缓存中
系统启动前,加载缓存,不让缓存统一时间过期
电商系统:热门商品,提前加入缓存。网约车中,计价规则提前加入缓存
热门数据,加到缓存
缓存风险总结
遇到风险,分析原因,解决之。
原因:更新机制,清理机制。
缓存的位置
CPU一般是三级缓存L1,L2,L3
级别越小越靠近CPU,容量最小,速度最快
如何避免cpu浪费时间
多线程,排队,异步
cpu和其他协同工作慢了,那就只能从以下角度出发
- 减少我的等待时间---------缓存
- 我尽量多做事情--------多线程
目的:降本增效。
级联系统缓存位置
级联:一级一级的
要想系统性能好,缓存一定要趁早
缓存一般是系统内部做,不会专门做一个缓存系统中间做,要么在调用方,要么在被调用方
客户端缓存位置
目的:降低用户响应时间
打车系统:订单不是访问量最大的,有些人预估价格可能并不下单
秒杀系统:并不是下单请求量最高,而是商品详情页
降级:有些时候就不存储缓存了,没必要,过年的时候打车去北京西站滴滴崩了。
有些时候就不做个人详情的了,就给静态的大家都用的
保证优先级高的正常运行
百度也把凤巢让出来了,将自己的一部分服务器给抢红包功能
代码:storage
浏览器:cookie。如果非必要,不要用。会增大服务端的压力
静态缓存
用的html,没有走的服务器就生成的。apache
如果每个用户查出来的都一样。电商网站的物流信息,数据库存一份,缓存也存一份,因为所有人都要用
凡是与用户个人无关,具有较强通用性的数据都可以作为静态数据缓存
不适合缓存通用性很差的数据。
服务缓存
个性化的动态的不值得缓存,但是这些数据的生成都有一个过程
尽量找那些服务器里输出固定响应值的数据做缓存
一定固定的视频、图片、欢迎页一般缓存到客户端里,不放在服务端。服务端里的是工作人员可以操作到的。Apache是服务端,我们手机是装不了的
能减少I/O就减少I/O
数据库本身的缓存
数据库耗时比较久
冗余字段
订单表里有用户姓名,照理来说应该是存ID再去用户表找的,这样会浪费时间,干脆直接存姓名
查询缓存
建议不用,因为Mysql8以上抛弃他了
如果有相同的sql语句,会直接采用缓存结果,不会再去执行
缓存的sql语句和sql结果
更改表中的数据,那个缓存就会失效。对于频繁改变的表,不合适。只适合不频繁改变,且sql语句基本相同,版本也比较低就可以用。
清理碎片,flush query cache., reset query cache.
历史表
将数据放到历史表中,以后的操作比如说统计,可以延迟操作,而中间的数据存储,相当于一次缓存
十天前的一些东西可能不用了,把它挪走,相当于一个消息队列,先把这些仍在里面,以后要用就用。
新老数据一起查,就不用这么做了,只适合一些不怎么查的,只做统计用的数据
写缓存
读缓存是先读缓存,没有再去数据库拿
目的:削峰
防止请求洪峰,压垮系统
调用方发一堆处理不了了,减少数据处理方的压力
写缓存收益
原:数据处理方时间
后:写缓存时间+读缓存时间+传递时间+数据处理方时间
收益在于用户:减少了用户的响应时间
写缓存实践
利用redis,发布订阅
MQ也是
数据库(先写数据,剩下的和主业务无关的操作,后置)
目的:只要能减少用户的响应时间:就OK
适合场景:请求峰谷值变化明显、对实时性要求不高的场景。
请求不是异步处理,时效性比较差,要关注业务本身的要求
如果有相同的sql语句,会直接采用缓存结果,不会再去执行
缓存的sql语句和sql结果
更改表中的数据,那个缓存就会失效。对于频繁改变的表,不合适。只适合不频繁改变,且sql语句基本相同,版本也比较低就可以用。
清理碎片,flush query cache., reset query cache.
历史表
将数据放到历史表中,以后的操作比如说统计,可以延迟操作,而中间的数据存储,相当于一次缓存
十天前的一些东西可能不用了,把它挪走,相当于一个消息队列,先把这些仍在里面,以后要用就用。
新老数据一起查,就不用这么做了,只适合一些不怎么查的,只做统计用的数据
写缓存
读缓存是先读缓存,没有再去数据库拿
目的:削峰
防止请求洪峰,压垮系统
调用方发一堆处理不了了,减少数据处理方的压力
写缓存收益
原:数据处理方时间
后:写缓存时间+读缓存时间+传递时间+数据处理方时间
收益在于用户:减少了用户的响应时间
写缓存实践
利用redis,发布订阅
MQ也是
数据库(先写数据,剩下的和主业务无关的操作,后置)
目的:只要能减少用户的响应时间:就OK
适合场景:请求峰谷值变化明显、对实时性要求不高的场景。
请求不是异步处理,时效性比较差,要关注业务本身的要求