一、Mysql 基础知识
1.为什么不推荐使用外键与级联?
- 增加了复杂性: a. 每次做 DELETE 或者 UPDATE 都必须考虑外键约束,会导致开发的时候很痛苦, 测试数据极为不方便; b. 外键的主从关系是定的,假如那天需求有变化,数据库中的这个字段根本不需要和其他表有关联的话就会增加很多麻烦。
- 增加了额外工作:数据库需要增加维护外键的工作,比如当我们做一些涉及外键字段的增,删,更新操作之后,需要触发相关操作去检查,保证数据的的一致性和正确性,这样会不得不消耗数据库资源。如果在应用层面去维护的话,可以减小数据库压力;
- 对分库分表不友好:因为分库分表下外键是无法生效的。
外键与级联更新适用于单机低并发,不适合分布式、高并发集群;级联更新是强阻塞,存在数据库更新风暴的风险;外键影响数据库的插入速度
2.默认字符集
在 MySQL5.7 中,默认字符集是 latin1
;在 MySQL8.0 中,默认字符集是 utf8mb4
3.字段类型
4.VARCHAR(100)和 VARCHAR(10)的区别是什么?
二者存储的字符范围不同,但存储相同的字符串,所占用磁盘的存储空间相同
不过,VARCHAR(100) 会消耗更多的内存。这是因为 VARCHAR 类型在内存中操作时,通常会分配固定大小的内存块来保存值,即使用字符类型中定义的长度。例如在进行排序的时候,VARCHAR(100)是按照 100 这个长度来进行的,也就会消耗更多内存。
5.DECIMAL 和 FLOAT/DOUBLE 的区别是什么?
DECIMAL 是定点数,FLOAT/DOUBLE 是浮数
DECIMAL 用于存储具有精度要求的小数,例如与货币相关的数据,可以避免浮点数带来的精度损失。在 Java 中,MySQL 的 DECIMAL 类型对应的是 Java 类 java.math.BigDecimal
。
二、事务
1.事务的特性
- 原子性(
Atomicity
):事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用; - 一致性(
Consistency
):执行事务前后,数据保持一致,例如转账业务中,无论事务是否成功,转账者和收款人的总额应该是不变的; - 隔离性(
Isolation
):并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的; - 持久性(
Durability
):一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。
2.并发事务带来的问题
脏读(Dirty read):B事务读取了A事务还未提交的数据后A事务回滚,数据并没有提交到数据库
丢失修改(Lost to modify):A、B事务读取同一个数据,A事务修改该数据后B事务也修改了该数据,导致A事务的修改丢失
不可重复读(Unrepeatable read):A事务内多次读同一数据,在A事务还未结束时,B事务也访问该数据并做出修改,导致A事务两次读到的数据不一样
幻读(Phantom read):A事务读取了几行数据后,并发事务B插入了一些数据,在随后的查询中,A事务发现多了一些原本不存在的记录
3.SQL事务隔离级别
- READ-UNCOMMITTED(读未提交) :允许读取尚未提交的数据变更
- READ-COMMITTED(读已提交) :允许读取并发事务已经提交的数据
- REPEATABLE-READ(可重复读) :对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改(MySQL默认隔离级别)
- SERIALIZABLE(可串行化) :所有的事务依次逐个执行,事务之间不产生干扰
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
READ-UNCOMMITTED | √ | √ | √ |
READ-COMMITTED | × | √ | √ |
REPEATABLE-READ | × | × | √ |
SERIALIZABLE | × | × | × |
4.SQL优化
避免使用select *
用union all代替union
小表驱动大表
批量操作
多用limit
in中值太多
增量查询
高效的分页
用连接查询代替子查询
join的表不宜过多
二、Redis
1.Redis 为什么这么快
- Redis 基于内存,内存的访问速度比磁盘快很多
- Redis 基于 Reactor 模式设计开发了一套高效的事件处理模型,主要是单线程事件循环和 IO 多路复用
- Redis 内置了多种优化过后的数据类型/结构实现,性能非常高
- Redis 通信协议实现简单且解析高效
2.Redis常用数据类型
- 5 种基础数据类型:String(字符串)、List(列表)、Hash(散列)、Set(集合)、Zset(有序集合)
- 3 种特殊数据类型:Bitmap (位图)、HyperLogLog(基数统计)、Geospatial (地理位置)
Redis 5 种基本数据类型对应的底层数据结构实现如下表所示:
String | List | Hash | Set | Zset |
---|---|---|---|---|
SDS | LinkedList/ZipList/QuickList | Dict、ZipList | Dict、Intset | ZipList、SkipList |
(1)String
String 是一种二进制安全的数据类型,可以用来存储任何类型的数据比如字符串、整数、浮点数、图片、序列化后的对象,一个String的value最多可以是512M
String的数据结构为简单动态字符串(Simple Dynamic String),是可修改的字符串,内部结构实现上类似于Java的ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配
(2)List
List 的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销
(3)Hash
Hash 是一个 String 类型的 field-value(键值对)的映射表,特别适用于存储对象,内部实现为数组 + 链表
(4)Set
Set是一种无序集合,且集合中的元素唯一,可以基于 Set 轻易实现交集、并集、差集的操作
常见应用场景:
- 存放的数据不能重复:网站 UV 统计、文章点赞、动态点赞
- 获取多个数据源交、并、差集:共同好友(交集)、共同粉丝(交集)、共同关注(交集)、好友推荐(差集)、音乐推荐(差集)、订阅号推荐(差集+交集)
- 随机获取元素:抽奖系统、随机点名
(5)Zset
Sorted Set 类似于 Set,但增加了一个权重参数 score
用来有序排列,还可以通过 score
的范围来获取元素的列表,有点像是 Java 中 HashMap
和 TreeSet
的结合体
ZSet 有两种不同的实现,分别是 ziplist 和 skiplist,具体使用哪种结构进行存储的规则如下:
- 当有序集合对象同时满足以下两个条件时,使用 ziplist:
- ZSet 保存的键值对数量少于 128 个;
- 每个元素的长度小于 64 字节。
- 如果不满足上述两个条件,那么使用 skiplist
(6)Bitmap
Bitmap 存储的是连续的二进制数字(0 和 1),通过 Bitmap, 只需要一个 bit 位来表示某个元素对应的值或者状态
(7)HyperLogLog
像UV(UniqueVisitor,独立访客)、独立IP数、搜索记录数等需要去重和计数的问题称为基数问题。
解决基数问题有很多种方案:
(1)数据存储在MySQL表中,使用distinct count计算不重复个数
(2)使用Redis提供的hash、set、bitmaps等数据结构来处理
以上的方案结果精确,但随着数据不断增加,导致占用空间越来越大,对于非常大的数据集是不切实际的。
HyperLogLog 是一种有名的基数计数概率算法 ,基于 LogLog Counting(LLC)优化改进得来,并不是 Redis 特有的,Redis 只是实现了这个算法并提供了一些开箱即用的 API。
Redis 提供的 HyperLogLog 占用空间非常非常小,只需要 12k 的空间就能存储接近2^64
个不同元素。这是真的厉害,这就是数学的魅力么!并且,Redis 对 HyperLogLog 的存储结构做了优化,采用两种方式计数:
- 稀疏矩阵:计数较少的时候,占用空间很小。
- 稠密矩阵:计数达到某个阈值的时候,占用 12k 的空间
(8)Geospatial
3.String 还是 Hash 存储对象数据更好呢?
- String 存储的是序列化后的对象数据,存放的是整个对象。Hash 是对对象的每个字段单独存储,可以对部分字段进行增删改查,节省网络流量
- String 存储相对来说更加节省内存,缓存相同数量的对象数据,String 消耗的内存约是 Hash 的一半。并且,存储具有多层嵌套的对象时也方便很多。如果系统对性能和资源消耗非常敏感的话,String 就非常适合。
在绝大部分情况,建议使用 String 来存储对象数据
4.Redis 的有序集合底层为什么要用跳表,而不用平衡树、红黑树或者 B+树?
- 平衡树 vs 跳表:平衡树的插入、删除和查询的时间复杂度和跳表一样都是 O(log n)。对于范围查询来说,平衡树也可以通过中序遍历的方式达到和跳表一样的效果。但是它的每一次插入或者删除操作都需要保证整颗树左右节点的绝对平衡,只要不平衡就要通过旋转操作来保持平衡,这个过程是比较耗时的。跳表诞生的初衷就是为了克服平衡树的一些缺点。跳表使用概率平衡而不是严格强制的平衡,因此,跳表中的插入和删除算法比平衡树的等效算法简单得多,速度也快得多。
- 红黑树 vs 跳表:相比较于红黑树来说,跳表的实现也更简单一些,不需要通过旋转和染色(红黑变换)来保证黑平衡。并且,按照区间来查找数据这个操作,红黑树的效率没有跳表高。
- B+树 vs 跳表:B+树更适合作为数据库和文件系统中常用的索引结构之一,它的核心思想是通过可能少的 IO 定位到尽可能多的索引来获得查询数据。对于 Redis 这种内存数据库来说,它对这些并不感冒,因为 Redis 作为内存数据库它不可能存储大量的数据,所以对于索引不需要通过 B+树这种方式进行维护,只需按照概率进行随机维护即可,节约内存。而且使用跳表实现 zset 时相较前者来说更简单一些,在进行插入时只需通过索引将数据插入到链表中合适的位置再随机维护一定高度的索引即可,也不需要像 B+树那样插入时发现失衡时还需要对节点分裂与合并。
5.Redis 性能优化
(1)使用批量操作减少网络传输
一个 Redis 命令的执行可以简化为:发送命令 -> 命令排队 -> 命令执行 -> 返回结果
其中,第 1 步和第 4 步耗费时间之和称为 Round Trip Time (RTT,往返时间) ,也就是数据在网络上传输的时间。
使用批量操作可以减少网络传输次数,进而有效减小网络开销,大幅减少 RTT。
此外,发送一次命令的 socket I/O 成本也比较高(涉及上下文切换,存在read()
和write()
系统调用),批量操作还可以减少 socket I/O 成本
(2)大量 key 集中过期问题
对于过期 key,Redis 采用的是 定期删除+惰性/懒汉式删除 策略
定期删除执行过程中,如果突然遇到大量过期 key,客户端请求必须等待定期清理过期 key 任务线程执行完成,导致客户端请求没办法被及时处理
解决方法:
- 给 key 设置随机过期时间
- 开启 lazy-free(惰性删除/延迟释放) 。lazy-free 特性是 Redis 4.0 开始引入的,指的是让 Redis 采用异步方式延迟释放 key 使用的内存,将该操作交给单独的子线程处理,避免阻塞主线程
建议不管是否开启 lazy-free,都尽量给 key 设置随机过期时间
(3)bigkey
如果一个 key 对应的 value 所占用的内存比较大,那这个 key 就可以看作是 bigkey。大概的参考标准:
- String 类型的 value 超过 1MB
- 复合类型(List、Hash、Set、Sorted Set 等)的 value 包含的元素超过 5000 个(不过,对于复合类型的 value 来说,不一定包含的元素越多,占用的内存就越多)。
bigkey 通常是由于下面这些原因产生的:
- 程序设计不当,比如直接使用 String 类型存储较大的文件对应的二进制数据。
- 对于业务的数据规模考虑不周到,比如使用集合类型的时候没有考虑到数据量的快速增长。
- 未及时清理垃圾数据,比如哈希中冗余了大量的无用键值对。
bigkey 除了会消耗更多的内存空间和带宽,还会对性能造成比较大的影响。
大 key 还会造成阻塞问题。具体来说,主要体现在下面三个方面:
- 客户端超时阻塞:由于 Redis 执行命令是单线程处理,然后在操作大 key 时会比较耗时,那么就会阻塞 Redis,从客户端这一视角看,就是很久很久都没有响应。
- 网络阻塞:每次获取大 key 产生的网络流量较大,如果一个 key 的大小是 1 MB,每秒访问量为 1000,那么每秒会产生 1000MB 的流量,这对于普通千兆网卡的服务器来说是灾难性的。
- 工作线程阻塞:如果使用 del 删除大 key 时,会阻塞工作线程,这样就没办法处理后续的命令。
大 key 造成的阻塞问题还会进一步影响到主从同步和集群扩容。
(4)hotkey
在 Redis 实例的每秒处理请求达到 5000 次,而其中某个 key 的每秒访问量就高达 2000 次,那这个 key 就可以看作是 hotkey
hotkey 出现的原因主要是某个热点数据访问量暴增,如重大的热搜事件、参与秒杀的商品。
处理 hotkey 会占用大量的 CPU 和带宽,可能会影响 Redis 实例对其他请求的正常处理。此外,如果突然访问 hotkey 的请求超出了 Redis 的处理能力,Redis 就会直接宕机。这种情况下,大量请求将落到后面的数据库上,可能会导致数据库崩溃。
因此,hotkey 很可能成为系统性能的瓶颈点,需要单独对其进行优化,以确保系统的高可用性和稳定性。
hotkey 的常见处理以及优化办法如下(这些方法可以配合起来使用):
- 读写分离:主节点处理写请求,从节点处理读请求。
- 使用 Redis Cluster:将热点数据分散存储在多个 Redis 节点上。
- 二级缓存:hotkey 采用二级缓存的方式进行处理,将 hotkey 存放一份到 JVM 本地内存中(可以用 Caffeine)。
(5)慢查询
(6)内存碎片
6.Redis 生产问题
(1)缓存穿透
大量请求的 key 是不合理的,根本不存在于缓存中,也不存在于数据库中 。这就导致这些请求直接到了数据库上,根本没有经过缓存这一层,对数据库造成了巨大的压力,可能造成宕机
(2)缓存击穿
请求的 key 对应的是 热点数据 ,该数据 存在于数据库中,但不存在于缓存中(通常是因为缓存中的那份数据已经过期) 。这可能会导致瞬时大量的请求直接打到数据库上,对数据库造成了巨大的压力,造成宕机
(3)缓存雪崩
缓存在同一时间大面积的失效或缓存服务宕机,导致大量的请求都直接落到了数据库上,对数据库造成了巨大的压力,导致宕机