数据库八股
双写一致性问题
一个数据同时存在于持久性存储(数据库)及缓存(redis)中,任何一方的更新都必须保证另一方的同步更新,以保持双方数据的一致性,如:
- 写数据库后忘记更新缓存:后续请求可能仍从缓存中读到旧数据;
- 先删除缓存以保证数据新鲜度,但是后续更新数据库失败了
- 并发情况下,多个读写请求交替进行,可能导致多个请求操作同一份数据,比如删缓更库时,新读请求读到旧数据并把它放到缓存里
- 主从复制存在时延,若在此期间缓存失效,可能从未同步的从库读到旧数据
双写策略
旁路缓存
即读请求到来先在缓存里面找,如果没有命中则去数据库查找,找到后加载到缓存并返回数据;
写请求到来时先更新数据库,再删除缓存;
为什么是删除缓存不是更新缓存?
并发情况下,两个写请求到来可能出现a更新数据库->b更新数据库->b更新缓存->a更新缓存的情况,导致数据库缓存数据不一致
为什么先操作数据库后操作缓存?
若先删除缓存,a请求删除缓存并更新数据库,b请求到来直接从未更新完的数据库读到旧数据并加载到缓存里,导致出现脏数据
读写穿透
读穿透类似旁路缓存策略,只是未命中缓存的话,由缓存系统去隐式地自动地查询数据库并加载到缓存而非由应用程序显示地访问数据库
写穿透则是更新缓存,同时更新数据库,只有当数据库更新完成,这个写操作才算完成
异步缓存写入
更新缓存后并非立即更新数据库,而是通过缓存系统记录下此次更新,放到一个队列中(日志文件或者内存队列),后台独立存在一个线程定期或者当队列达到一定数量去从队列中取更新操作,批量写入数据库
因为更新数据库操作并不及时,所以很忌讳缓存或系统故障导致丢数据,适用容忍一定延迟的数据一致性场景
用一定一致性换性能和扩展
延时双删
更新数据库时,先删除一遍缓存,确保后续请求会从数据库读新的,但更新数据库和删除缓存可能存在时间窗口,期间可能读到旧数据,所以设定一定时间后再删除一遍缓存
删除缓存重试
删除缓存失效执行时,设定重试策略以确保正确删除,如指数退避、固定间隔重试等策略多次尝试
监听并读取binlog异步删除缓存
更新数据库时,将对应操作记录在类似binlog的事务日志里面
用专门的异步服务或监听器订阅binlog变化,若有数据更新
根据binlog中操作信息定位到受影响的缓存项
将需要更新的数据发送到消息队列,由消费者处理队列中的事件,以一个异步的方式删除或更新缓存
将更新缓存和主业务解耦,避免主线程阻塞
总结
读的时候旁路缓存;
写的时候,先写数据库,异步处理缓存的更新,即监听并读取binlog事件,异步删除缓存.
一主多从的情况下,还要等所有从库binlog事件都被处理后才删除缓存
一条SQL查询语句的过程
- 连接器:连接器负责和客户端连接,登陆校验,获取权限,维持和管理连接
- 查看缓存:会先去看之前有没有执行过相同的语句,如果有的话,语句和相对应的结果会以键值对的方式被存在内存里面,直接返回,如果没有就到分析器流程
- 分析器:主要进行语法分析语义分析,比如知道语句中字符串的含义
- 优化器,负责索引的选择,如果是多表查询的话还负责多表间的加载顺序,生成执行计划
- 执行器(执行引擎)通过分析器知道了做什么,通过优化器知道了怎么做,先作一次权限校验,接下来按照执行计划去存储引擎获取数据,接着处理数据,可能设计排序连接过滤等,最后返回结果
事务隔离级别
- 读未提交:允许一个事务读取另一个事务尚未提交的数据修改.存在脏读、不可重复读、幻读现象
- 读已提交:一个事务只能读取已提交的数据操作
- 可重复读:事务执行期间,多次读取同一数据结果相同,即一个事务执行时,其他事务所作修改对他而言是不可见的
- 序列化:最高级别的,确保事务间并发执行与串行相同
事务四大特性ACID
原子性:事务的所有操作不应该出现部分成功的情况
一致性:事务执行前后,数据库从一个一致性状态到另一个一致性状态
隔离性:多个事务并发执行时,一个事务不会影响其他事务执行
持久性:一旦事务被提交,那么对数据库的操作就是永久的,即便系统故障或崩溃
数据库的存储引擎
存储引擎主要负责执行查询、数据存储
- MyISAM:早期默认的存储引擎,支持全文索引、表级锁;适合快速读取和数据量不大的场景
采用非聚集索引,B+数叶子节点存储指向数据文件的指针
- InnoDB:目前主流,支持事务ACID、行级锁、外键、崩溃恢复(事务日志实现,确保一致性),适合需要事务和高并发的场景
采用聚集索引,b+树叶子节点存储数据
- Memory:将表数据存在内存里,查询速度飞快,适用于临时数据的存储(一旦server崩溃或重启容易丢失数据)
数据库三大范式
1NF:字段不可再分;
2NF:有主键且非主键必须依赖主键
3NF:不能是传递依赖,必须每列都直接和主键有联系
MySQL索引类型
索引:以文件形式存在硬盘,包含索引字段的值,和对应行数据所在的物理空间
- 普通索引:最基本,仅仅用来加速查询
- 唯一索引:具有唯一约束,索引列值必须唯一,可为空
- 主键索引:唯一且非空,一张表只有一个
- 全文索引:用于全文搜索
- 复合索引:多列的组合,遵循最左前缀原则,专门用于组合搜索,比索引合并要快
- 索引合并:使用多个单列索引组合 搜索
- 覆盖索引:是一种数据查询方式,并不是类型.指通过索引值就可以找到查询列,不需要通过主键值回表查询
- 空间索引:用域地理数据
为什么不对每一列创建一个索引?
索引的创建和维护有成本; 数据发生变化也需要动态维护索引 索引占据物理空间
MySQL为什么使用B+树作索引
为什么不是Hash
- hash需要将数据全部加载到内存中,数据量大可能无法一次性装入内存,B+树基于 按照节点分段加载
- 单一查找特别快,但是业务场景经常涉及多条查询,B+树有序且叶子节点链表相连,查询更快
为什么不是B树
- B树所有节点都存数据,导致每个节点(文件页)能存的索引数大大减小,B+树每一页能存的索引数提升,数高也就更加矮胖,磁盘IO次数也就得到空指
聚簇索引和非聚簇索引
聚簇索引:正文内容本身就是按照一定规则排序的目录.形同字典,正文内容本身就是按照拼音排序的目录,无需借鉴其他目录就可以找到要查找的数据
叶节点就是数据节点
非聚簇索引:目录存粹是目录,正文存粹是正文.比如按偏旁去找字,检字表中连续的的字在正文并不连续,要先找检字表,在去对应页码.
叶节点仍然是索引节点,但包含一个指向数据文件的指针
前者可以直接找到查询数据,后者需要通过非聚簇索引找到记录对应的主键值(页码),再使用主键值通过聚簇索引找到数据,所以有回表查询成本
大并发服务器基本框架
基本服务器框架都是c/s的,请求和响应的流程是:客户端与应用服务器建立连接,应用服务器与数据库服务器进行交互
! 当大量并发到来的时候,服务器会进行大量数据库操作,但数据库的最大连接数量是有限的,未连接的需等待前面已连接的请求处理后在访问数据库
可以在两者间增设中间层DAL,设计缓冲队列和连接池设计.缓冲队列用来存储等待的请求,当连接池有空闲连接就从队列中取一个请求处理.但仅仅减轻服务器压力,未提升处理速度(受限于数据库并发数)
增设一层缓存,将常用数据加载到缓存,命中缓存
**!**缓存足够多时,需要进行缓存换页
缓存可以部署在和应用服务器同一台机器上,也可以单独服务器,推荐单独服务器,不然不同应用服务器间很难访问彼此的缓存
**!**如果有大量并发请求到来,虽有多台服务器+缓存服务器+DAL中间层,数据库服务器仍会出现瓶颈,比如大量写操作阻塞了很多读请求.
将数据库实现读写分离.由于读一般比写多,执行负载均衡replication,中间层将写操作投递到主库中,读操作从从库中读取.当主库被修改,通过replication机制同步给从库
同样,应用服务器也实现负载均衡,架设多台服务器,不同请求派发给不同的服务器.由单独的任务服务器派发.这种方式时任务服务器主动分发,一个用服务器被动接受,在请求类型相近的情况下,比如,都是读请求,服务器a2个读请求,服务器b3个读请求,,没什么问题,但如果请求处理的复杂程度不同.
缓存穿透,缓存雪崩,缓存击穿
缓存穿透
客户端请求的数据在缓存和数据库当中都不存在,大量这样的高并发请求直接命中服务器,导致服务器增压甚至宕机.可能是因为原先的数据存在,但因为某种原因被删了,但前端或前置应用程序仍然保有;或者是用户的恶意攻击行为,利用不存在的key,或者恶意尝试导致产生大量不存在的业务数据请求
解决办法:
- **接口校验:**类似用户权限的拦截,对于id=-3827这种无效访问直接拦截,不允许请求到达数据库甚至缓存上
- **缓存空值:**数据库虽然没有id=1002的用户数据,但在缓存存空值,后续请求到来直接返回空给客户端.但是key的过期时间不能过长防止占用太多redis资源;如果是用户恶意攻击行为,会存特别多的空值到redis,占内存
- **实时监控:**一旦发现redis命中率下降,配合运维人员对访问的对象和访问数据进行分析,进行黑名单的设置限制服务.
- **布隆过滤器:**将目前所有可访问的资源,通过简单的映射关系放到布隆过滤器里面,当一个请求到来时,先进行布隆判断,有则可能有,无则一定无.
布隆原理:使用bitmap和多个哈希函数,当向其中加入数据x时,将hash1(x),hash2(x),hash3(x)等位置的值置为1.查询时,如果相应位置为一,认为存在过滤器中.性能高但也有不足:可能误判:但误判率可以通过哈希函数数量调节;不容易进行数据删除:两个不同数据可能在某一哈希函数上有相同的值
缓存雪崩
大量key集体过期或者缓存服务故障
解决办法:
- 随机化过期时间,使得过期时间分散开
- 设置多级缓存:如nginx缓存+redis+其他,不同层用不同缓存
- 构建缓存的高可用集群:redis集群,提高可用性,防止单一redis故障
- 使用锁或者队列:如果查不到,就加排它锁,其他请求只能等待,影响并发量
- 缓存标记:热点数据不考虑失效,后台异步更新缓存
缓存击穿
热点key过期,同时大量用户访问该key
解决办法:
- 互斥锁:只有一个请求能获取到该锁,然后到数据库中将数据查询并返回redis,之后的请求就可以从redis中获得响应
- 逻辑过期:在value值内部设置一个比过期时间更短的时间标识,异步线程发现该值快过期了,马上延长内置的该值,并重新从数据库加载并写入
- 提前设置热点数据
- 实时监控调整过期时间
缓存预热:系统上线后将相关缓存数据直接加载到缓存系统,避免用户先去数据库再写入缓存
缓存降级:在访问量飙升/服务出现问题/非核心服务影响到核心流程性能时,为保证”核心服务可用,即使是有损的”,对一些关键数据进行自动降级/人工降级
redis常见数据结构及使用场景
- String:不光存字符串,也可以是数字.常规应用:计数:粉丝数,关注数,也可缓存序列化的用户信息
- Hash:主要用于对象存储(不需要序列化和反序列化),如用户信息,商品信息;
- List:比如关注列表,消息列表;还可以通过Lrange命令实现从某元素开始读多少个元素,即基于list实现高性能分页查询;
- Set:类似List作列表,但可以自动去重;很方便求并集交集差集,如判断是否在一个集合里:比如共同关注,共同好友;
- Sorted Set:和set相比增加了一个权重的参数.,使得集合中元素能按照权重有序化:适用于实时排行信息:比如直播间在线用户列表,礼物排行榜,积分榜等
使用redis有哪些好处
- 访问快.数据存在内存中
- 支持事务原子性:redis中的操作都是原子的
- 数据类型丰富
- 特性丰富:可以用来缓存,消息
Memcached与Redis的区别
- 存储方式:Memcache将数据全存在内存里面,断电会丢失,且内存限制数据大小;Redis有部分在硬盘上,可以进行持久化;速度:R更快;
- 数据支持类型:M只有String一种类型;R更加丰富;
- Value值大小:M只有1mb,R最大可达一个G;
- m是多线程,非阻塞IO复用;Redis是单线程io多路复用
- R支持数据的备份,即主从模式的数据备份,
- 底层模型不同:底层通信方式及通信协议不同.Redis自己构建了VM机制,因为一般的系统调用系统函数会浪费一定的时间去移动和请求
为什么redis是单线程的
- 全部操作基于内存,CPU不是redis的瓶颈,而是机器内存大小或者网络带宽.单线程易实现;
- 可以减少不必要的线程切换和竞态条件
Redis高并发原因
- 纯内存数据库,读取快;
- 单线程非阻塞io多路复用,无线程切换开销,单线程来轮询描述符,将数据库开关读写转化为事件
- 不同数据结构对数据存储进行优化,每种数据结构都有至少两种内部编码实现
- 采用自己实现的事件分离器,效率更高**[这个事件分离器是单线程的,所以说是单线程模型].**
IO多路复用程序负责监听多个套接字,并向事件分离器派送那些产生了事件的套接字.由分离器交给对应的事件处理器处理
Redis过期策略:定期+惰性
定期删除
每隔一段时间(100ms)检查,随机抽查key看有没有过期,过期则删除
为什么随机: 如果redis存了特别多的key,不随机而是遍历的话,CPU负载大
为什么不是定时: 定时需要一个定时器监视key,发现过期就删除,虽然很及时释放,但十分消耗CPU资源,大并发下,CPU要用来处理请求而不是删除过期key
惰性删除
定期删除会导致很多key还没被删除,在获取某个key的时候,会先检查是否过期,过期则删除.
数据淘汰机制
定期+惰性不足以将全部过期key删除,它们会逐步累积直到使内存达到最大内存限制
Redis配置文件有一行配置:maxmemory-policy volatile-lru用以配置淘汰策略.主要有六种方案:
- (默认):新写入的会报错
- (键空间)最近最少使用移除,LRU(Least Recently Used
- (键空间)随机移除
- (设置了过期时间的键空间)LRU
- (设置了过期时间的键空间)随机移除
- (设置了过期时间的键空间)优先挑选快要过期的
如何保证Redis中的数据全都是热点数据—数据淘汰机制
Redis持久化
把内存中的数据同步到磁盘文件来保证数据持久化.当Redis重启后,就可以通过加载硬盘文件到内存的方式实现恢复数据的目的
实现:单独fork一个子进程,将父进程数据库数据复制到子进程当中,由子进程写入到临时文件,持久化的过程结束了,再用这个临时文件替换上次的快照文件.子进程退出,内存释放
RDB快照持久化
通过创建快照来获取内存里面数据在某个时间点上的副本.Redis创建快照后,可以留在本地以便重启服务器时使用,可以复制备份交给其他服务器,创建具有相同数据的服务器副本
- 无法做到实时持久化,是按一定时间周期策略,所以可能丢失数据;
- 存储量较大时,效率较低,IO性能较低
- fork创建子进程,内存产生额外消耗
AOF持久化
每执行一条会更改redis中数据的命令,就把该命令写入硬盘中的AOF文件.redis重启后,通过重新执行该文件中的命令达到重建数据库的效果
- 实时性更强,每隔一秒进行一次,最多丢一秒数据
- 写入性能更高,redis会减速以适应硬盘的最大写入速度
- 适合灾难性误删除的紧急恢复
redis4.0对持久化机制的优化
AOF重写的时候直接把RDB的内容写到AOF开头.做到快速加载同时避免丢失过多数据
缺点是AOF里面RDB部分是压缩格式,可读性差
什么是AOF重写
AOF重写可以产生一个新的AOF文件,两者所保存的数据库状态相同,但重写文件体积更小
也并非字面意义上的重写.redis维护一个AOF重写缓冲区,在子进程创建新AOF期间,即重写期间,将所有新的写命令放入原有AOF缓冲区和AOF重写缓冲区
子进程完成创建新AOF工作之后,redis服务器将重写缓冲区内容追加到新AOF文件末尾,使得新旧两个AOF文件所保存数据库状态一致.最后用新AOF替换旧AOF,以此完成重写
AOF缓冲区:继续接收客户端命令
AOF重写缓冲区:保存重写期间的新命令
Redis集群
哨兵着眼于可用性,监听集群中的服务器,并在主服务器挂了之后自动从从服务器中选举出新的主服务器提供服务
集群着眼于扩展性,单个redis内存不足时,使用集群进行分片存储
如何解决Redis并发竞争key的问题
分布式锁,这里可以谈谈项目中怎么用的
主从复制
启动一个从节点,会发送一条命令给主节点
如果是初次连接,会触发一次全量复制.主节点启动后台线程生成一份RDB,并将新收到的写命令缓存在内存中.
从节点收到RDB后,先写入磁盘,再从磁盘加载到内存.
主节点把内存中缓存的新写命令发给从节点,从节点也会同步这些数据
主从如果是重新连接,主节点仅仅会复制给从节点部分缺少的数据



