面试-数据库

MySQL

特性:acid:原子性(undo log)、一致性(靠其他特性保证)、隔离性(锁机制、mvcc)、持久性(redo log)

隔离级别:未提交读、已提交读(解决脏读(读取的数据最终回滚))、可重复读(解决不可重复读(读取后被修改,前后读取不一致))、可串行化(解决幻读(同样的查询前后多了一条数据))。

三大范式:1NF:解决原子性问题。2NF:解决部分依赖问题。3NF;解决传递依赖问题。

执行顺序:FROM 子句组装来自不同数据源的数据;WHERE 子句基于指定的条件对记录行进行筛选;GROUP BY 子句将数据划分为多个分组;使用聚集函数进行计算;使用 HAVING 子句筛选分组;计算所有的表达式;SELECT 的字段;使用 ORDER BY 对结果集进行排序。

where 和having区别

  • where子句作用于表和视图,having作用于组。
  • where在数据分组前进行过滤,having在数据分组后进行过滤。
  • 另一方面,HAVING子句中不能使用除了分组字段和聚合函数之外的其他字段

Mysql的架构:

Server 层:连接器、查询缓存、分析器、优化器、执行器

存储引擎:InnoDB或者MyISAM

更新语句流程:

1.先查询到 id 为1的记录,有缓存会使用缓存。

2.拿到查询结果,将 name 更新为张三,然后调用引擎接口,写入更新数据,innodb 引擎将数据保存在内存中,同时记录redo log,此时redo log进入 准备状态。

3.执行器收到通知后记录binlog,然后调用引擎接口,提交redo log为提交状态。

4.更新完成。

inner join,left join的区别:完全匹配的部分、返回左表所有的行和左匹配

如何保证数据不丢失的?通过redo log 和 bin log

**如何解决SQL注入?

严格的参数校验**

PreparedStatement预编译

mybatis中#{}(参数占位符)防止SQL注入

${} :拼接替换符,不能防止SQL注入

MySQL优化:

  • 针对查询,我们可以通过使用索引、使用连接代替子查询的方式来提高查询速度。
  • 针对慢查询,我们可以通过分析慢查询日志,来发现引起慢查询的原因,从而有针对性的进行优化。
  • 针对插入,我们可以通过禁用索引、禁用检查等方式来提高插入速度,在插入之后再启用索引和检查。
  • 针对数据库结构,我们可以通过将字段很多的表拆分成多张表、增加中间表、增加冗余字段等方式进行优化。

表结构设计优化:

冗余的事不太容易发生变化的字段比如姓名性别等

不可避免需要join的时候,可以设计个中间表去存储所有数据

要设计一个游标字段(自增主键和时间戳很适合当游标字段)减少limit查询时间

如何分析sql的性能(EXPLAIN 命令)

type表示查询表连接类型
possible_keys:表示 MySQL 执行查询时可能用到的索引
key:表示 MySQL 实际使用到的索引
key_len:表示 MySQL 实际使用的索引的最大长度
rows:估算要找到所需的记录,需要读取的行数
extra:这列包含了 MySQL 解析查询的额外信息

定位慢查询:开启mysql慢查询日志,分析慢查询日志文件

导致MySQL慢查询有哪些原因?

  1. 索引使用不当或缺失: 索引未覆盖查询中使用的字段,或者索引被查询中的函数调用、范围查询或非等值条件所避开。索引选择性差,即索引不能有效地过滤掉大量行。
  2. 单表数据量太大
  3. 查询使用了临时表
  4. 表结构设计不佳: 过度规范化或反规范化可能导致额外的JOIN操作,增加查询复杂度。冗余数据或设计不良的外键关系。
  5. limit深度分页问题
  6. 复杂的查询语句: 包含大量的连接(JOINs)、子查询或者复杂的嵌套表达式,这些都会增加CPU和I/O负载。

大表查询慢常见优化措施?

创建适当的索引
优化查询语句:

利用缓存

提升硬件配置

读写分离

分库分表

慢sql的优化分析思路?

查看慢查询日志记录,分析慢SQL

2.explain查看分析SQL的执行计划

3.profile 分析执行耗时

4.Optimizer Trace分析详情

优化小技巧

  • 查询时只返回必要的列,用具体的字段列表代替 select * 语句,因为要尽量用聚集索引防止回表查询
  • SQL语句要避免造成索引失效的写法
  • 尽量用union all代替union UNION 会进行去重处理,这会增加排序和比较的计算成本
  • 避免在where子句中对字段进行表达式操作
  • Join优化 能用inner join 就不用left join right join,如必须使用 一定要以小表为驱动,( 内连接会对两个表进行优化,优先把小表放到外边,把大表放到里边。left join 或 right join,不会重新调整顺序)inner join 内连接,只保留两张表中完全匹配的结果集;left join会返回左表所有的行,即使在右表中没有匹配的记录;right join会返回右表所有的行,即使在左表中没有匹配的记录;
  • 使用varchar代替char.(因为可变常字段存储空间小,可节省空间)
  • 将多次插入换成批量Insert插入:例如,使用 INSERT INTO order (id, code, user_id) VALUES (123, '001', 100), (124, '002', 100), (125, '003', 101);
  • 避免在where子句中使用!=或<>操作符使用!=和<>很可能会让索引失效应尽量避免在where子句中使用!=或<>操作符,否则引擎将放弃使用索引而进行全表扫描
  • 多次插入改成批量插入默认新增SQL有事务控制,导致每条都需要事务开启和事务提交,而批量处理是一次事务开启和提交,效率提升明显,达到一定量级,效果显著,平时看不出来。

深度分页怎么优化?

覆盖索引和子查询来解决

索引

索引是一个单独的、存储在磁盘上的数据库结构,包含着对数据表里所有记录的引用指针。

InnoDB采用的B+树的数据结构来存储索引。

b+树的叶子节点和非叶子节点分别存储的什么?

聚簇索引

  • 叶子节点:存储的是整行数据。
  • 非叶子节点:存储的是主键值,这些值用于指导查询过程中的搜索方向。

非聚簇索引(二级索引)

  • 叶子节点:存储的是主键值,而不是整行数据。这是因为非聚簇索引的目的是加速查询,而不是直接提供数据。当通过非聚簇索引找到主键值后,MySQL会进行所谓的“回表”操作,即根据主键值去聚簇索引中查找相应的整行数据。
  • 非叶子节点:存储的是非主键索引列的值,这些值也用于指导查询过程中的搜索方向。

为什么选择b+树?

  1. IO次数较少B+树的高度更低,访问时所需的IO次数更少
  2. 查询性能稳定:每次查找操作的路径长度都相同,提供了稳定的查找效率。
  3. 范围查询高效

特点:

  1. 所有数据记录都是存储在叶子节点上
  2. 非叶子节点仅存储关键字信息(即索引)
  3. 叶子节点之间通过指针连接
  4. 平衡性
  5. 支持动态插入和删除
  6. 适合磁盘存储
  7. 支持左闭合区间搜索:B+树的搜索采用左闭合区间,这意味着搜索某个关键字时,会包含等于该关键字的最低值的记录,这对于支持自增ID等场景非常有利。

哈希索引:对于每一行数据,存储引擎会对索引列进行哈希计算得到哈希码,将哈希码的值作为哈希表的key值,将指向数据行的指针作为哈希表的value值

什么情况下需要建索引?

  1. 经常用于查询的字段
  2. 经常用于连接的字段建立索引,可以加快连接的速度
  3. 经常需要排序的字段建立索引,因为索引已经排好序,可以加快排序查询速度

什么情况下不建索引?

  1. where条件中用不到的字段不适合建立索引
  2. 表记录较少。比如只有几百条数据,没必要加索引。
  3. 需要经常增删改。需要评估是否适合加索引
  4. 参与列计算的列不适合建索引
  5. 区分度不高,数据重复且分布比较均匀的的字段

索引类型

按照底层存储方式角度划分:

  • 聚簇索引(聚集索引):索引结构和数据一起存放的索引,InnoDB 中的主键索引就属于聚簇索引。
  • 非聚簇索引(非聚集索引):索引结构和数据分开存放的索引,二级索引(辅助索引)就属于非聚簇索引。MySQL 的 MyISAM 引擎,不管主键还是非主键,使用的都是非聚簇索引。

按照应用维度划分:

  • 主键索引:加速查询 + 列值唯一(不可以有 NULL)+ 表中只有一个。
  • 普通索引:仅加速查询。
  • 唯一索引:加速查询 + 列值唯一(可以有 NULL)。
  • 覆盖索引:一个索引包含(或者说覆盖)所有需要查询的字段的值。
  • 联合索引:多列值组成一个索引,专门用于组合搜索,其效率大于索引合并。
  • 全文索引:对文本的内容进行分词,进行搜索。目前只有 CHARVARCHAR ,TEXT 列上可以创建全文索引。一般不会使用,效率较低,通常使用搜索引擎如 ElasticSearch 代替。

为什么联合索引比多个单列索引效率更高

  1. 减少I/O操作
  2. 减少排序和合并操作
  3. 更少的重复数据
  4. 更有效的范围查询

回表查询

回表的意思就是通过二级索引找到对应的主键值,然后再通过主键值找到聚集索引中所对应的整行数据,这个过程就是回表。

覆盖索引:如果一个索引包含(或者说覆盖)所有需要查询的字段的值,我们就称之为 覆盖索引,而无需回表查询。
最左前缀匹配原则,只要最左边的索引匹配,联合索引就会匹配

什么情况索引会失效

  • 以%开头的like查询如%abc,无法使用索引(使用倒置索引完成模糊查询使用索引)
  • 查询条件使用or连接,也会导致索引失效:例如,select * from student_info where student_name='Helen' or student_age > 15,这种情况下索引不会起作用。原因是数据库无法从OR条件的一个范围查询中获益,只能通过扫描表的方式来满足查询需求。
  • 在进行范围查询(如 BETWEEN、>、<、>=、<=)时会失效
  • 索引在使用的时候没有遵循最左匹配法则(联合索引的匹配从where的最左边的字段开始,只有匹配成功才能继续匹配到右边的下一个字段。)
  • 在添加索引的字段上进行了运算操作
  • 当操作符左右两边的数据类型不一致时,会发生隐式转换让索引失效。
  • 对索引列进行函数操作

index merge索引合并

MySQL中,当执行一个查询语句需要使用多个索引时,MySQL可以使用索引合并(Index Merge)来优化查询性能。具体来说,索引合并是将多个单列索引或多个联合索引合并使用,以满足查询语句的需要。交集合并(Intersection Merge)、并集合并(Union Merge)和排序并集合并(Sort-Union Merge)

倒排索引

倒排索引是一种数据库索引技术,用于存储从内容到文档的映射,以优化全文搜索的效率。倒排索引,是实现“单词-文档矩阵”的一种具体存储形式,它能快速地根据单词获取包含这个单词的文档列表

索引下推

核心思想是在服务层进行索引扫描时,将部分WHERE条件下推到存储引擎层,据这些过滤条件进行判断,只返回满足条件的记录,从而减少回表查询和服务层与存储引擎层之间的数据交互,提高查询效率。

大表如何添加索引

  1. 先创建一张跟原表A数据结构相同的新表B
  2. 在新表B添加需要加上的新索引。
  3. 把原表A数据导到新表B
  4. rename新表B为原表的表名A,原表A换别的表名;

MVCC

它最大的优点是读不加锁,因此读写不冲突,并发性能好。InnoDB实现MVCC,多个版本的数据可以共存,主要基 于以下技术及数据结构:

  1. 隐藏列:InnoDB中每行数据都有隐藏列,隐藏列中包含了本行数据的事务id、指向undo log的指 针等。

  2. 基于undo log的版本链:每行数据的隐藏列中包含了指向undo log的指针,而每条undo log也会 指向更早版本的undo log,从而形成一条版本链。

  3. ReadView:通过隐藏列和版本链,MySQL可以将数据恢复到指定版本。但是具体要恢复到哪个版 本,则需要根据ReadView来确定。所谓ReadView,是指事务(记做事务A)在某一时刻给整个事 务系统(trx_sys)打快照,之后再进行读操作时,会将读取到的数据中的事务id与trx_sys快照比 较,从而判断数据对该ReadView是否可见,即对事务A是否可见。

MVCC 是一种并发控制技术,它允许多个事务同时访问数据库中的同一份数据,而不会互相干扰。在 MySQL 中,每个事务都有一个唯一的事务ID,每个被修改的数据行都有一个隐藏的版本号。当一个事务尝试读取或修改数据时,它会检查当前事务ID与数据行的最新版本号是否匹配。如果匹配,说明该事务可以看到该版本的数据;如果不匹配,说明该事务需要等待其他事务完成并提交它们的更改。

MySQL 的 change buffer 是什么?

主要用于暂存那些未被加载进内存的非主键索引页上的更改操作。当进行插入、更新或删除等DML(数据操作语言)操作时,如果涉及到的非主键索引页不在内存中,这些操作会被缓存在Change Buffer中,而不是立即进行磁盘IO操作,从而减少了即时的磁盘访问,提升了操作效率。

redo log 的执行流程?

Redo Log(重做日志)是MySQL数据库中的一种日志文件,用于记录数据库中每个事务的修改操作。它的执行流程如下:

  1. 当一个事务开始时,InnoDB存储引擎会为这个事务分配一个连续的、且自增的log sequence number(LSN,日志序列号)。
  2. 在事务执行过程中,每当有数据被修改,InnoDB存储引擎就会将这个修改作为一个“redo log record”写入到Redo Log Buffer(重做日志缓冲区)中。这个记录包含了修改的类型(插入、更新或删除)、修改的表空间ID、修改的数据页号、修改的行信息等。
  3. 当事务提交时,InnoDB存储引擎会将Redo Log Buffer中的redo log record写入到Redo Log文件中。这个过程是异步进行的,也就是说,即使redo log record还没有被写入到磁盘上的Redo Log文件中,事务也可以正常提交。
  4. 当系统发生故障时,如服务器宕机或者数据库进程异常退出,InnoDB存储引擎可以通过Redo Log文件中的redo log record来恢复未完成的事务。具体来说,它会扫描所有的Redo Log文件,找到未完成的事务,然后根据redo log record重新执行这些事务的修改操作,从而保证数据的一致性。
  5. Redo Log文件的大小是有限的,当Redo Log Buffer中的redo log record被写入到Redo Log文件后,这些redo log record就会被标记为“已提交”。同时,InnoDB存储引擎会定期地将Redo Log文件中的“已提交”的redo log record进行清理,以释放磁盘空间。这个过程被称为“redo log compaction”

Sharding-JDBC

Sharding-JDBC定位为轻量级java框架,无需额外部署和依赖,Sharding-JDBC可以在程序中轻松的实现数据库读写分离

Mysql主从复制原理

1. 主服务器(master)把数据更改记录到二进制日志(binlog)中。
2. 从服务器(slave)把主服务器的二进制日志复制到自己的中继日志(relay log)中。
3. 从服务器重做中继日志中的日志,把更改应用到自己的数据库上,以达到数据的最终一致性。

解决主从复制延迟有几种常见的方法:

二次读取

关键业务读写操作全部指向主机,非关键业务采用读写分离

ShardingSphere 绝对可以说是当前分库分表的首选

分库分表后,数据怎么迁移呢?

  • 我们对老库的更新操作(增删改),同时也要写入新库(双写)。如果操作的数据不存在于新库的话,需要插入到新库中。 这样就能保证,咱们新库里的数据是最新的。
  • 在迁移过程,双写只会让被更新操作过的老库中的数据同步到新库,我们还需要自己写脚本将老库中的数据和新库的数据做比对。如果新库中没有,那咱们就把数据插入到新库。如果新库有,旧库没有,就把新库对应的数据删除(冗余数据清理)。
  • 重复上一步的操作,直到老库和新库的数据一致为止。

全局唯一标识(snowflake):利用“时间+机器标识+序列号”组合,通过位运算拼成一个不冲突的长整型。

库存扣减原子性保证:

先把库存缓存在 Redis,再借助 Lua 脚本 保证原子性:
悲观锁

TCC (Try-Confirm/Cancel)

  1. Try 阶段预扣库存(锁定),
  2. Confirm 阶段正式扣减,
  3. Cancel 阶段回滚预扣。

跨分片分页/排序

各分片局部 Top-K → 应用端 k 路归并 → 游标翻页

Redis

Redis数据类型

  • 字符串(string):普通字符串,Redis中最简单的数据类型,string的内部结构实现上类似Java的ArrayList
  • 哈希(hash):也叫散列,类似于Java中的HashMap结构
  • 列表(list):按照插入顺序排序,可以有重复元素,类似于Java中的LinkedList,底层是双向链表
  • 集合(set):无序集合,没有重复元素,类似于Java中的HashSet
  • 有序集合(sorted set/zset):集合中每个元素关联一个分数(score),根据分数升序排序,没有重复元素

Redis的有序集合(Zset)底层采用两种数据结构,分别是压缩列表(ziplist)(内存连续)和跳跃表(skiplist)(内存不连续)

当Zset的元素个数小于128个且每个元素的长度小于64字节时,采用ziplist编码。
跳表一种基于有序链表的数据结构,通过多级索引的方式实现高效的查找、插入和删除操作
Redis的Hash数据结构底层原理主要基于两种数据结构:ziplist和hashtable
3种redis特殊数据类型

Bitmap (位图)

HyperLogLog(基数统计)Redis 提供的 HyperLogLog 占用空间非常非常小,只需要 12k 的空间就能存储接近2^64个不同元素。
Geospatial (地理位置)Geospatial index(地理空间索引,简称 GEO) 主要用于存储地理位置信息,基于 Sorted Set 实现。
数值范围0-40亿的数如何排序(bitmap)?

  1. 初始化Bitmap:根据数值范围创建一个足够大的Bitmap。由于数值范围是0-40亿,Bitmap的大小需要能够覆盖这个范围,即至少需要40亿位。
  2. 标记数值:遍历待排序的数值列表,将每个数值在Bitmap中对应的位置标记为1。例如,如果数值是5,则在Bitmap的第6位(从0开始计数)标记为1。
  3. 按位输出:按照Bitmap的顺序,输出所有标记为1的位置对应的数值,即可得到排序后的结果。

String 还是 Hash 存储对象数据更好呢?

如果系统对性能和资源消耗非常敏感的话,String 就非常适合。

如果对象中某些字段需要经常变动或者经常需要单独查询对象中的个别字段信息,Hash 就非常适合。

使用 Redis 实现一个排行榜怎么做?Sorted Set

相关的一些 Redis 命令: ZRANGE (从小到大排序)、 ZREVRANGE (从大到小排序)、ZREVRANK (指定元素排名)。

Set 的应用场景是什么?

  • 存放的数据不能重复的场景:网站 UV 统计(数据量巨大的场景还是 HyperLogLog更适合一些)、文章点赞、动态点赞等等。
  • 需要获取多个数据源交集、并集和差集的场景:共同好友(交集)、共同粉丝(交集)、共同关注(交集)、好友推荐(差集)、音乐推荐(差集)、订阅号推荐(差集+交集) 等等。
  • 需要随机获取数据源中的元素的场景:抽奖系统、随机点名等等。

使用 Set 实现抽奖系统怎么做?

  • SADD key member1 member2 ...:向指定集合添加一个或多个元素。
  • SPOP key count:随机移除并获取指定集合中一个或多个元素,适合不允许重复中奖的场景。
  • SRANDMEMBER key count : 随机获取指定集合中指定数量的元素,适合允许重复中奖的场景。

使用 Bitmap 统计活跃用户怎么做?

可以使用日期(精确到天)作为 key,然后用户 ID 为 offset,如果当日活跃过就设置为 1。

使用 HyperLogLog 统计页面 UV 主要需要用到下面这两个命令:

  • PFADD key element1 element2 ...:添加一个或多个元素到 HyperLogLog 中。
  • PFCOUNT key1 key2:获取一个或者多个 HyperLogLog 的唯一计数。

Redis 怎么实现延时队列

使用sortedset,拿时间戳作为score,消息内容作为key,调用zadd来生产消息,消费者用zrangebyscore指令获取N秒之前的数据轮询进行处理。

  • SETEX key seconds value 设置指定key的值,并将 key 的过期时间设为 seconds 秒
  • SETNX key value 只有在 key 不存在时设置 key 的值

SETEX命令允许用户为键设置一个过期时间,这是通过在Redis的数据库结构中的expires字典中记录每个键的过期时间来实现的。这个过期时间是以毫秒精度的UNIX时间戳来表示的。

Redis的通用命令

  • KEYS pattern 查找所有符合给定模式( pattern)的 key
  • EXISTS key 检查给定 key 是否存在
  • TYPE key 返回 key 所储存的值的类型
  • DEL key 该命令用于在 key 存在是删除 key

使用StringRedisTemplate写入Redis时,手动把对象序列化为JSON读取Redis时,手动把读取到的JSON反序列化为对象

redis怎么应对高并发?

  1. 采用Redis集群模式数据分片:在多个节点上分布数据分片,即使某个节点不可用,也不会影响其他节点的正常运行,从而避免缓存雪崩现象。跨机房部署:通过在不同地理位置的机房部署Redis集群,可以进一步提升容灾能力,保证在某个机房出现故障时依然能提供服务。
  2. 使用持久化和预热缓存数据持久化:在重启Redis之前,通过执行SAVE指令将数据持久化到磁盘,确保数据不会因为重启而全部失效。人工触发预热:重启后手动或自动预热缓存,保证缓存在服务恢复后能够立即起到加速访问的作用。
  3. 随机设置缓存过期时间分散失效时间:为缓存设置不同的过期时间,避免大量缓存在同一时刻集中失效,从而防止缓存雪崩的发生

怎么保证Redis的高并发高可用:主从集群,再加上使用redis中的哨兵模式

redis为什么这么快?

1、完全基于内存的,C语言编写,没有磁盘IO上的开销。数据存在内存中,读写速度快。

2、采用单线程,避免不必要的上下文切换以及锁等同步机制的开销

3、使用多路I/O复用模型,基于select/epoll等I/O多路复用技术实现高吞吐量网络I/O

IO多路复用模型的思路就是:系统提供了select、poll、epoll函数可以同时监控多个fd(文件描述符)的操作,有了这个函数后,应用线程通过调用select函数就可以同时监控多个fd,一旦某个描述符就绪(一般是读就绪或者写就绪),select函数就会返回可读/可写状态,这时询问线程再去通知想请求IO操作的线程,对应线程此时再发起IO请求去读/写数据
局限性

  • *事务处理:**Redis只支持简单的事务处理,对于复杂的事务无能为力,比如跨多个键的事务处理。
  • *数据持久化:**Redis是内存数据库,数据存储在内存中,如果服务器崩溃或断电,数据可能丢失。虽然Redis提供了数据持久化机制,但有一些限制。
  • *数据处理:**Redis只支持一些简单的数据结构,比如字符串、列表、哈希表等。如果需要处理复杂的数据结构,比如关系型数据库中的表,那么Redis可能不是一个好的选择。
  • *数据安全:**Redis没有提供像主数据库那样的安全机制,比如用户认证、访问控制等等。

Redis遇到哈希冲突怎么办?链地址法

redis作缓存读写一致性问题

Cache Aside Pattern(旁路缓存模式)

  • 先更新 db
  • 然后直接删除 cache 。

 :

  • 从 cache 中读取数据,读取到就直接返回
  • cache 中读取不到的话,就从 db 中读取数据返回
  • 再把数据放到 cache 中。

理论上来说还是可能会出现数据不一致性的问题,不过概率非常小

缺陷 1:首次请求数据一定不在 cache 的问题

缺陷 2:写操作比较频繁的话导致 cache 中的数据会被频繁被删除,这样会影响缓存命中率 。

数据库和缓存数据强一致场景:更新 db 的时候同样更新 cache,采用redisson实现的读写锁。

Read/Write Through Pattern(读写穿透)

写(Write Through):

  • 先查 cache,cache 中不存在,直接更新 db。
  • cache 中存在,则先更新 cache,然后 cache 服务自己更新 db(同步更新 cache 和 db)。

读(Read Through):

  • 从 cache 中读取数据,读取到就直接返回 。
  • 读取不到的话,先从 db 加载,写入到 cache 后返回响应。

Write Behind Pattern(异步缓存写入)

只更新缓存,由缓存自己异步更新数据库

mysql的数据如何与redis进行同步呢?

增加 cache 更新重试机制(常用):如果 cache 服务当前不可用导致缓存删除失败的话,我们就隔一段时间进行重试,重试次数可以自己定。如果多次重试还是失败的话,我们可以把当前更新失败的 key 存入队列中,等缓存服务可用之后,再将缓存中对应的 key 删除即可。

强一致方案

1.采用redisson实现的读写锁。强一致

2.延时双删(删cache写db删cache)数据仍可能存在不一致性。

3.先写db再删除缓存策略,数据仍可能存在不一致性。

4.binlog+异步删缓存:通过CDC监听 Binlog,将变更事件异步推送到消费端。
消费端收到事件后再去 删cache,强一致。

缓存穿透,缓存雪崩,缓存击穿问题

缓存穿透

1.布隆过滤器

布隆过滤器的核心思想是使用多个哈希函数来将元素映射到位图中的多个位置上。当一个元素被加入到布隆过滤器中时,它会被多次哈希,并将对应的位数组位置设置为1。当需要判断一个元素是否在布隆过滤器中时,我们只需将该元素进行多次哈希,并检查对应的位数组位置是否都为1,如果其中有任意一位为0,则说明该元素不在集合中;如果所有位都为1,则说明该元素可能在集合中(因为有可能存在哈希冲突),需要进一步检查。
2.如果查询缓存和数据库的数据没有找到,则直接设置一个默认值(可以是空值)存到缓存中
缓存击穿:redis某个热点key过期或者刚开始,但是此时有大量的用户访问该过期key

第一可以使用互斥锁:只有一个请求可以获取到互斥锁,然后到DB中将数据查询并加入到缓存,之后所有请求就可以从缓存中得到响应

第二监控数据,实时调整:监控哪些数据是热门数据,实时的调整key的过期时长。

缓存雪崩

将缓存失效时间分散开,比如可以在原有的失效时间基础上增加一个随机值

缓存预热

缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。

缓存降级(防止雪崩):对于不重要的缓存数据,可以采取服务降级策略,例如一个比较常见的做法就是,Redis出现问题,不去数据库查询,而是直接返回默认值给用户。

redis内存管理

Redis 通过一个叫做过期字典(可以看作是 hash 表)来保存数据过期的时间。

Redis数据过期策略

第一种是惰性删除

第二种是 定期删除

Redis数据淘汰策略

  1. volatile-lru(least recently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰。
  2. volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰。
  3. volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰。
  4. allkeys-lru(least recently used):当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)。
  5. allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰。
  6. no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。这个应该没人使用吧!

4.0 版本后增加以下两种:

  1. volatile-lfu(least frequently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰。
  2. allkeys-lfu(least frequently used):当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key。

Redis持久化机制

  • RDB(snapshotting,快照)是一个快照文件,定时对整个数据做快照,把redis内存存储的数据写到磁盘上,当redis实例宕机恢复数据的时候,方便从RDB的快照文件中恢复数据。RDB方法通过fork创建子进程来进行实际的快照保存工作,这样可以最小化对主线程的影响。当满足一定条件时,例如在指定的时间间隔内数据集发生了指定次数的更改,Redis会自动执行bgsave命令来生成RDB文件。此外,也可以通过手动执行save或bgsave命令来触发RDB快照的生成。
  • AOF(append-only file,只追加文件)的含义是追加文件,在AOF模式下,Redis将所有修改数据的操作以文本形式记录到一个日志文件中,即每条写命令都被附加到文件的末尾。这种持久化方法能够更好地保证数据完整性,因为即使出现系统崩溃,也可以从AOF文件中重现所有的写操作来恢复数据。

AOF 工作基本流程是怎样的?

  1. 命令追加(append):所有的写命令会追加到 AOF 缓冲区中:所有写入命令会先追加到缓冲区(aof_buf)中,而不是直接写入文件。这是为了减少磁盘IO操作,提升性能。Redis使用单线程响应命令,如果每次写操作都直接追加到硬盘,性能会受到严重影响。因此,先将命令写入缓冲区是一种折中方式。
  2. 文件同步(fsync):AOF 缓冲区根据对应的持久化方式( fsync 策略)向AOF文件做同步操作。这一步需要调用 fsync 函数(系统调用), fsync 针对单个文件操作,对其进行强制硬盘同步,fsync 将阻塞直到写入磁盘完成后返回,保证了数据持久化。(在AOF文件中,每一个写命令都会被记录下来,其中包括对键值的修改、添加和删除操作。然而,某些键可能会被多次修改或删除,这就造成了数据的冗余。在AOF重写过程中,只记录最终的结果,从而大大减少了文件大小。)
  3. 文件重写(rewrite):随着 AOF 文件越来越大,需要定期对 AOF 文件进行重写,达到压缩的目的。随着不断写入AOF文件,其体积会不断增大。为了减小文件体积并提高恢复速度,Redis提供了AOF文件重写机制。Redis通过fork创建一个子进程来进行重写,子进程根据RDB内存快照将AOF文件数据写入新的AOF文件,父进程继续接收并缓存新的写命令,同时将这些新的命令通过增量写入新的AOF文件。完成后,新AOF文件替换旧文件。
  4. 重启加载(load):当 Redis 重启时,可以加载 AOF 文件进行数据恢复。

AOF 持久化方式有哪些?

fsync是一个在Linux操作系统中非常重要的函数,用于将缓冲区中的数据立即写入磁盘,从而确保数据的持久性和安全性

策略有三种:always、everysec和no。

为什么是在执行完命令之后记录日志呢?

  • 避免额外的检查开销,AOF 记录日志不会对命令进行语法检查;
  • 在命令执行完之后再记录,不会阻塞当前的命令执行。

redis事务

redis事务不能回滚,不满足原子性和持久性。

Redis 可以通过 MULTIEXECDISCARD 和 WATCH 等命令来实现事务(Transaction)功能

MULTI命令后可以输入多个命令,Redis 不会立即执行这些命令,而是将它们放到队列,当调用了 EXEC命令后,再执行所有的命令。

这个过程是这样的:

  1. 开始事务(MULTI);
  2. 命令入队(批量操作 Redis 的命令,先进先出(FIFO)的顺序执行);
  3. 执行事务(EXEC)。

DISCARD命令取消一个事务,它会清空事务队列中保存的所有命令。

WATCH命令监听指定的 Key,当调用 EXEC 命令执行事务时,如果一个被 WATCH 命令监视的 Key 被 其他客户端/Session 修改的话,整个事务都不会被执行。

不过,如果 WATCH 与 事务 在同一个 Session 里,并且被 WATCH 监视的 Key 被修改的操作发生在事务内部,这个事务是可以被执行成功的。

不能回滚,不支持原子性

AOF 持久化的fsync策略为 no、everysec 时都会存在数据丢失的情况 。不支持持久性
如何解决 Redis 事务的缺陷?

利用 Lua 脚本来批量执行多条 Redis 命令
redis bigkey(大 Key)

如何发现 bigkey?

使用 Redis 自带的 SCAN 命令

如何处理 bigkey?

拆分BigKey

手动清理

采用合适的数据结构

开启 lazy-free(惰性删除/延迟释放)

hotkey:如果一个 key 的访问次数比较多且明显多于其他 key 的话,那这个 key 就可以看作是 hotkey(热 Key)

访问 hotkey 的请求超出了 Redis 的处理能力,Redis 就会直接宕机。这种情况下,大量请求将落到后面的数据库上,可能会导致数据库崩溃。

如何发现 hotkey?

使用 Redis 自带的 --hotkeys 参数来查找。

如何解决 hotkey?

  • 读写分离:主节点处理写请求,从节点处理读请求。
  • 使用 Redis Cluster:将热点数据分散存储在多个 Redis 节点上。
  • 二级缓存:hotkey 采用二级缓存的方式进行处理,将 hotkey 存放一份到 JVM 本地内存中(可以用 Caffeine)。

分布式锁redission

一个线程加锁成功后,会另开一个线程(称为看门狗Watch dog)进行监控,不断监听持有锁的线程,给线程增加持有锁的时间,也就是“续期”,规则是每隔releaseTime(锁的过期时间)/3的时间做一次续期(就是重新设置锁过期时间为releaseTime),手动释放锁,此时通知对应线程的Watch dog不需要再监听了。

redis本身怎么实现锁?

  • SETEX key seconds value 设置指定key的值,并将 key 的过期时间设为 seconds 秒
  • SETNX key value 只有在 key 不存在时设置 key 的值

分布式锁能解决主从一致性的问题吗?不能使用红锁解决(要求在大多数redis节点上都成功创建锁)

如果业务非要保证数据的强一致性,建议使用zookeeper实现的分布式锁,它是可以保证强一致性的。

redis集群方案有哪些

  • 主从复制
  • 哨兵模式
  • 分片集群

1.主从复制作用

1.数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。

2.故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余。

3.负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis数据时应用连接主节点,读Redis数据时应用连接从节点),分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量

4.高可用基石:除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。

2.主从同步流程

主从同步分为了两个阶段,一个是全量同步,一个是增量同步

全量同步是指从节点第一次与主节点建立连接的时候使用全量同步,流程是这样的:

第一:从节点请求主节点同步数据,其中从节点会携带自己的replication id和offset偏移量。

第二:主节点判断是否是第一次请求,主要判断的依据就是,主节点与从节点是否是同一个replication id,如果不是,就说明是第一次同步,那主节点就会把自己的replication id和offset发送给从节点,让从节点与主节点的信息保持一致。

第三:在同时主节点会执行bgsave,生成rdb文件后,发送给从节点去执行,从节点先把自己的数据清空,然后执行主节点发送过来的rdb文件,这样就保持了一致

当然,如果在rdb生成执行期间,依然有请求到了主节点,而主节点会以命令的方式(AOF方式)记录到一个日志文件,最后把这个日志文件发送给从节点,这样就能保证主节点与从节点完全一致了,后期再同步数据的时候,都是依赖于这个日志文件,这个就是全量同步

增量同步指的是,当从节点服务重启之后,数据就不一致了,所以这个时候,从节点会请求主节点同步数据,主节点还是判断不是第一次请求,不是第一次就获取从节点的offset值,然后主节点从AOF命令日志中获取offset值之后的数据,发送给从节点进行数据同步

哨兵模式(解决高可用问题)

哨兵机制可以自动切换主从节点。

客户端连接Redis的时候,先连接哨兵,哨兵会告诉客户端Redis主节点的地址,然后客户端连接上Redis并进行后续的操作。当主节点宕机的时候,哨兵监测到主节点宕机,会重新推选出某个表现良好的从节点成为新的主节点,然后通过发布订阅模式通知其他的从服务器,让它们切换主机。

redis集群脑裂问题

可以设置主从数据复制和同步的延迟时间,达不到要求就拒绝请求,就可以避免大量的数据丢失

分片集群Redis cluster

分片集群采用哈希槽(hash slots)机制来实现数据的分区。具体来说,Redis分片集群共有16384个哈希槽,每个数据键通过CRC16校验后对16384取模来决定放置到哪个槽。每个节点负责一定数量的槽,并处理映射到这些槽上的键值数据。这种机制确保了数据能够均匀地分布到不同的节点上,从而实现负载均衡和高效访问。

Redis cluster集群节点最小配置6个节点以上(3主3从),其中主节点提供读写操作,从节点作为备用节点,不提供请求,只作为故障转移使用。

posted @ 2025-04-22 13:04  哒令,哒哒哒哒哒~令  阅读(30)  评论(0)    收藏  举报