1.MySQL
1.1什么是MySQL?
MySQL
是⼀种关系型数据库,在
Java
企业级开发中⾮常常⽤,因为
MySQL
是开源免费的,并
且⽅便扩展。阿⾥巴巴数据库系统也⼤量⽤到了
MySQL
,因此它的稳定性是有保障的。
MySQL
是开放源代码的,因此任何⼈都可以在
GPL(General Public License)
的许可下下载并根据个性
化的需要对其进⾏修改。
MySQL
的默认端⼝号是
3306
。
1.2存储引擎
查看MySQL提供的所有存储引擎 show engines;
MySQL 当前默认的存储引擎是InnoDB,并且在5.7版本所有的存储引擎中 只有 InnoDB 是事务性存储引擎,也就是说只有 InnoDB ⽀持事务。
MyISAM和InnoDB区别
1.
是否⽀持⾏级锁
: MyISAM
只有表级锁
,⽽
InnoDB
⽀持⾏级锁
和表级锁
,
默认为⾏级锁。
2.
是否⽀持事务和崩溃后的安全恢复:
MyISAM
强调的是性能,每次查询具有原⼦性
,
其执⾏
速度⽐
InnoDB
类型更快,但是不提供事务⽀持。但是
InnoDB
提供事务⽀持事务,外部键等
⾼级数据库功能。 具有事务
、回滚
和崩溃修复能⼒
的事务安全
型表。
3.
是否⽀持外键:
MyISAM
不⽀持,⽽InnoDB
⽀持。
4.
是否⽀持
MVCC(多版本并发控制)
:仅
InnoDB
⽀持。应对⾼并发事务
, MVCC
⽐单纯的加锁更⾼效
;MVCC
只在 READ COMMITTED (读已提交)
和 REPEATABLE READ (可重复读)
两个隔离级别下⼯作
;MVCC
可以使⽤ 乐观(optimistic)
锁 和 悲观(pessimistic)
锁来实现
;
各数据库中
MVCC
实现并不统⼀。
悲观锁(Pessimistic Locking):
悲观锁的核心思想是,在整个数据处理过程中,始终假设最坏的情况会发生,即认为其他线程随时可能修改数据,因此在访问数据之前就进行加锁操作,确保每次只有一个线程能够访问数据。
乐观锁(Optimistic Locking):
乐观锁的核心思想是,认为在数据处理过程中不会发生并发冲突,因此不加锁,而是在更新数据时进行版本检查,通过比较版本号来判断数据是否被修改。如果版本号相同,则可以更新数据;如果版本号不同,则说明数据已经被其他线程修改,需要进行相应的处理。
1.3字符集及校对规则
字符集指的是⼀种从⼆进制编码到某类字符符号的映射。校对规则则是指某种字符集下的排序规
则。
MySQL
中每⼀种字符集都会对应⼀系列的校对规则。
MySQL采⽤的是类似继承的⽅式指定字符集的默认值,每个数据库以及每张数据表都有⾃⼰的默认值,他们逐层继承。
1.4索引
MySQL
索引使⽤的数据结构主要有
B+Tree
索引
和
哈希索引
。对于哈希索引来说,底层的数据结
构就是哈希表,因此在绝⼤多数需求为单条记录查询的时候,可以选择哈希索引,查询性能最
快;其余⼤部分场景,建议选择
B+Tree
索引。
MyISAM:
B+Tree
叶节点的
data域存放的是数据记录的地址。索引⽂件和数据⽂件是分离的。在索引检索的时候,⾸先按照B+Tree搜索算法搜索索引,如果指定的
Key
存在,则取出其
data
域的值,然后以
data
域的值为地址读取相应的数据记录。这被称为“
⾮聚簇索引
”
。
InnoDB:
其数据⽂件本身就是索引⽂件。
其表数据⽂件本身就是按B+Tree
组织的⼀个索引结构,树的叶节点
data
域保存了完整的数据记录。这个索引的
key
是数据表的主键,因此
InnoDB
表数据⽂件本身就是主索引。这被称为
“
聚
簇索引(或聚集索引)”。⽽其余的索引都作为二级索引,二级索引的data
域存储相应记录主
键的值⽽不是地址,这也是和MyISAM不同的地⽅。在根据聚集索引搜索时,直接找到key所在的节点即可取出数据;在根据二级索引查找时,则需要先取出主键的值,再⾛⼀遍聚集索引(回表)。 因此,在设计表的时候,不建议使⽤过⻓的字段作为主键,也不建议使⽤⾮唯一的字段作为主键,这样会造成主索引频繁分裂。
1.5查询缓存的使⽤
开启查询缓存后在同样的查询条件以及数据情况下,会直接在缓存中返回结果
。这⾥的查询条件包括查询本身、当前要查询的数据库、客户端协议版本号等⼀些可能影响结果的信息。因此任何两个查询在任何字符上的不同都会导致缓存不命中。此外,如果查询中包含任何⽤户⾃定义函数、存储函数、⽤户变量、临时表、MySQL
库中的系统表,其查询结果也不会被缓存。
缓存建⽴之后,
MySQL
的查询缓存系统会跟踪查询中涉及的每张表,如果这些表(数据或结构)
发⽣变化,那么和这张表相关的所有缓存数据都将失效。
缓存虽然能够提升数据库的查询性能,但是缓存同时也带来了额外的开销,每次查询后都要做⼀
次缓存操作,失效后还要销毁。
因此,开启缓存查询要谨慎,尤其对于写密集的应⽤来说更是如
此。如果开启,要注意合理控制缓存空间⼤⼩,⼀般来说其⼤⼩设置为⼏⼗
MB
⽐较合适。
1.6什么是事务?
事务是逻辑上的⼀组操作的集合,要么都执⾏,要么都不执⾏。
1.7事物的四⼤特性(ACID)
1.
原⼦性(
Atomicity
):
事务是最⼩的执⾏单位,不允许分割。事务的原⼦性确保动作要么
全部完成,要么完全不起作⽤;
2.
⼀致性(
Consistency
):
执⾏事务前后,数据保持⼀致,多个事务对同⼀个数据读取的结
果是相同的;
3.
隔离性(
Isolation
):
并发访问数据库时,⼀个⽤户的事务不被其他事务所⼲扰,各并发
事务之间数据库是独⽴的;
4.
持久性(
Durability
):
⼀个事务被提交之后。它对数据库中数据的改变是持久的,即使数
据库发⽣故障也不应该对其有任何影响。
1.8并发事务带来哪些问题?
![](https://img-blog.csdnimg.cn/direct/e6b49f7af1c04d1bb62b17a934223153.png)
1.9事务隔离级别有哪些?MySQL的默认隔离级别是?
![](https://img-blog.csdnimg.cn/direct/cd9c48f0b96f4fde9b2e91852b5e0dbe.png)
InnoDB 存储引擎在(可重读)事务隔离级别下使⽤的是Next-Key Lock 锁算法,因此可以避免幻读的产⽣,这与其他数据库系统(如 SQL Server) 是不同的。所以说InnoDB 存储引擎的默认⽀持的隔离级别是 REPEATABLE-READ(可重读) 已经可以完全保证事务的隔离性要求,即达到了SQL标准的 (可串⾏化) 隔离级别。
因为隔离级别越低,事务请求的锁越少,所以⼤部分数据库系统的隔离级别都是 READ-COMMITTED(读取提交内容) ,但是你要知道的是 InnoDB 存储引擎默认使⽤ REPEAaTABLE-READ(可重读) 并不会有任何性能损失。InnoDB 存储引擎在 分布式事务 的情况下⼀般会⽤到 SERIALIZABLE(可串⾏化) 隔离级别。
分布式事务:一次大的操作由不同的小操作组成,这些小的操作分布在不同的服务器上,且属于不同的应用,分布式事务需要保证这些小操作要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证数据库中的数据一致性。
1.10锁机制与InnoDB锁算法
MyISAM
采⽤表级锁
(table-level locking)
。
InnoDB
⽀持⾏级锁
(row-level locking)
和表级锁
,
默认为⾏级锁
表级锁:
MySQL
中锁定
粒度最⼤
的⼀种锁,对当前操作的整张表加锁,实现简单,资源消
耗也⽐较少,加锁快,不会出现死锁。其锁定粒度最⼤,触发锁冲突的概率最⾼,并发度最
低,
MyISAM
和
InnoDB
引擎都⽀持表级锁。
⾏级锁:
MySQL
中锁定
粒度最⼩
的⼀种锁,只针对当前操作的⾏进⾏加锁。 ⾏级锁能⼤
⼤减少数据库操作的冲突。其加锁粒度最⼩,并发度⾼,但加锁的开销也最⼤,加锁慢,会
出现死锁。
InnoDB
存储引擎的锁的算法有三种:
Record lock
:行锁,单个⾏记录上的锁
Gap lock:间隙锁,锁定⼀个范围,不包括记录本身
Next-key lock
:临键锁,
record+gap
锁定⼀个范围,包含记录本身
1. innodb
对于⾏的查询使⽤
next-key lock
2. Next-locking keying
为了解决
Phantom Problem
幻读问题
3.
当查询的索引含有唯⼀属性时,将
next-key lock
降级为
record key
4. Gap
锁设计的⽬的是为了阻⽌多个事务将记录插⼊到同⼀范围内,⽽这会导致幻读问题的产
⽣
5.
有两种⽅式显式关闭
gap
锁:(除了外键约束和唯⼀性检查外,其余情况仅使⽤
record
lock
)
A.
将事务隔离级别设置为
RC B.
将参数
innodb_locks_unsafe_for_binlog
设置为
1
1.11⼤表优化
当MySQL单表记录数过⼤时,数据库的CRUD性能会明显下降,⼀些常⻅的优化措施如下:
1.限定数据的范围
务必禁⽌不带任何限制数据范围条件的查询语句。⽐如:我们当⽤户在查询订单历史的时候,我
们可以控制在⼀个⽉的范围内;
2.读
/
写分离
经典的数据库拆分⽅案,主库负责写,从库负责读;
3.垂直分库/表(表结构不一样)
根据数据库⾥⾯数据表的相关性进⾏拆分。
例如,⽤户表中既有⽤户的登录信息⼜有⽤户的基
本信息,可以将⽤户表拆分成两个单独的表,甚⾄放到单独的库做分库。
简单来说垂直拆分是指数据表列的拆分,把⼀张列⽐较多的表拆分为多张表。
垂直拆分的优点:
可以使得列数据变⼩,在查询时减少读取的
Block
数,减少
I/O
次数。此
外,垂直分区可以简化表的结构,易于维护。
垂直拆分的缺点:
主键会出现冗余,需要管理冗余列,并会引起
Join
操作,可以通过在应⽤
层进⾏
Join
来解决。此外,垂直分区会让事务变得更加复杂;
4.⽔平分库/表(表结构一样)
保持数据表结构不变,通过某种策略存储数据分⽚。这样每⼀⽚数据分散到不同的表或者库中,
达到了分布式的⽬的。 ⽔平拆分可以⽀撑⾮常⼤的数据量。
⽔平拆分是指数据表⾏的拆分,表的⾏数超过
200
万⾏时,就会变慢,这时可以把⼀张的表的数
据拆成多张表来存放。举个例⼦:我们可以将⽤户信息表拆分成多个⽤户信息表,这样就可以避
免单⼀表数据量过⼤对性能造成影响。
⽔平拆分可以⽀持⾮常⼤的数据量。需要注意的⼀点是:分表仅仅是解决了单⼀表数据过⼤的问
题,但由于表的数据还是在同⼀台机器上,其实对于提升
MySQL
并发能⼒没有什么意义,所以
⽔平拆分最好分库
。
数据库分⽚的两种常⻅⽅案:
客户端代理:
分⽚逻辑在应⽤端,封装在
jar
包中,通过修改或者封装
JDBC
层来实现。
当
当⽹的
Sharding-JDBC
、阿⾥的
TDDL
是两种⽐常⽤的实现。
中间件代理: 在应⽤和数据中间加了⼀个代理层。分⽚逻辑统⼀维护在中间件服务中。
我
们现在谈的
Mycat
、
360
的
Atlas
、⽹易的
DDB
等等都是这种架构的实现。
1.12解释⼀下什么是池化设计思想。什么是数据库连接池?为什么需要数据库连接池?
我们常⻅的如
java
线程池、
jdbc
连接池、
redis
连接池等就是这类设计的代表实现。这种设计会初始预设资源,解决的问题就是抵消每次获取资源的消耗,如创建线程的开销,获取远程连接的开销等。就好⽐你去⻝堂打饭,打饭的⼤妈会先把饭盛好⼏份放那⾥,你来了就直接拿着饭盒加菜即可,不⽤再临时⼜盛饭⼜打菜,效率就⾼了。除了初始化资源,池化设计还包括如下这些特征:池⼦的初始值、池⼦的活跃值、池⼦的最⼤值等,这些特征可以直接映射到java
线程池和数据库连接池的成员属性中。
数据库连接本质就是⼀个
socket
的连接。数据库服务端还要维护⼀些缓存和⽤户权限信息之类
的 所以占⽤了⼀些内存。我们可以把数据库连接池是看做是维护的数据库连接的缓存,以便将来
需要对数据库的请求时可以重⽤这些连接。为每个⽤户打开和维护数据库连接,尤其是对动态数
据库驱动的⽹站应⽤程序的请求,既昂贵⼜浪费资源。
在连接池中,创建连接后,将其放置在池
中,并再次使⽤它,因此不必建⽴新的连接。如果使⽤了所有连接,则会建⽴⼀个新连接并将其
添加到池中。
连接池还减少了⽤户必须等待建⽴与数据库的连接的时间。
1.13分库分表之后,id 主键如何处理?
因为要是分成多个表之后,每个表都是从
1
开始累加,这样是不对的,我们需要⼀个全局唯⼀的
id
来⽀持。
⽣成全局
id
有下⾯这⼏种⽅式:
UUID
:不适合作为主键,因为太⻓了,并且⽆序不可读,查询效率低。⽐较适合⽤于⽣成唯⼀的名字的标示⽐如⽂件的名字。
数据库⾃增
id
:
两台数据库分别设置不同步⻓,⽣成不重复
ID
的策略来实现⾼可⽤。这种⽅
式⽣成的
id
有序,但是需要独⽴部署数据库实例,成本⾼,还会有性能瓶颈。
利⽤
redis
⽣成
id :
性能⽐较好,灵活⽅便,不依赖于数据库。但是,引⼊了新的组件造成
系统更加复杂,可⽤性降低,编码更加复杂,增加了系统成本。
Twitter
的
snowflake
算法
美团的
Leaf
分布式
ID
⽣成系统
:
Leaf
是美团开源的分布式
ID
⽣成器,能保证全局唯⼀性、
趋势递增、单调递增、信息安全,⾥⾯也提到了⼏种分布式⽅案的对⽐,但也需要依赖关系
数据库、
Zookeeper等中间件。
1.14⼀条SQL语句在MySQL中如何执⾏的
1.MySQL 主要分为 Server 层和引擎层,Server 层主要包括连接器、查询缓存、分析器、优化器、执行器,同时还有一个日志模块(binlog),这个日志模块所有执行引擎都可以共用,redolog 只有 InnoDB 有。•引擎层是插件式的,目前主要包括,MyISAM,InnoDB,Memory 等。
2•SQL 的执行过程分为两类,一类对于查询等过程如下:权限校验---》查询缓存---》分析器---》优化器---》权限校验---》执行器---》引擎
3•对于更新等语句执行流程如下:分析器----》权限校验----》执行器---》引擎---redo log prepare---》binlog---》redo log commit
1.15MySQL⾼性能优化规范建议
![](https://img-blog.csdnimg.cn/direct/679d21a8d02f46f68d026743d61919cb.png)
1.16⼀条SQL语句执⾏得很慢的原因有哪些?
1、大多数情况下很正常,偶尔很慢,则有如下原因
(1)、数据库在刷新脏页,例如 redo log 写满了需要同步到磁盘。
(2)、执行的时候,遇到锁,如表锁、行锁。
2、这条 SQL 语句一直执行的很慢,则有如下原因。
(1)、没有用上索引:例如该字段没有索引;由于对字段进行运算、函数操作导致无法用索引。
(2)、数据库选错了索引。
2.Redis
2.1简单介绍⼀下 Redis 呗!
简单来说
Redis
就是⼀个使⽤
C
语⾔开发的数据库
,不过与传统数据库不同的是
Redis
的数据
是存在内存中的
,也就是它是内存数据库,所以读写速度⾮常快,因此
Redis
被⼴泛应⽤于缓存
⽅向。另外,Redis
除了做缓存之外,
Redis
也经常⽤来做分布式锁,甚⾄是消息队列。
Redis
提供了多种数据类型来⽀持不同的业务场景。
Redis
还⽀持事务 、持久化、
Lua
脚本、多
种集群⽅案
2.2分布式缓存常⻅的技术选型⽅案有哪些?
Memcached 和 Redis(主)
分布式缓存主要解决的是单机缓存的容量受服务器限制并且⽆法保存通⽤的信息。因为,本地缓
存只在当前服务⾥有效,⽐如如果你部署了两个相同的服务,他们两者之间的缓存数据是⽆法共
同的。
2.3说⼀下 Redis 和 Memcached 的区别和共同点
共同点
:
1.
都是基于内存的数据库,⼀般都⽤来当做缓存使⽤。
2.
都有过期策略。
3.
两者的性能都⾮常⾼。
区别
:
1.
Redis
⽀持更丰富的数据类型(⽀持更复杂的应⽤场景)
。
Redis
不仅仅⽀持简单的
k/v (key,value)
类型的数据,同时还提供 list
,
set
,
zset
,
hash
等数据结构的存储。
Memcached
只⽀持最简单的 k/v
数据类型。
2.
Redis
⽀持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进
⾏使⽤
,
⽽
Memecache
把数据全部存在内存之中。
3. Redis
有灾难恢复机制。 因为可以把缓存中的数据持久化到磁盘上。
4. Redis
在服务器内存使⽤完之后,可以将不⽤的数据放到磁盘上。但是,
Memcached 在服
务器内存使⽤完之后,就会直接报异常。
5. Memcached 没有原⽣的集群模式,需要依靠客户端来实现往集群中分⽚写⼊数据;但是
Redis ⽬前是原⽣⽀持
cluster
模式的.
6. Memcached
是多线程,⾮阻塞
IO
复⽤的⽹络模型;
Redis
使⽤单线程的多路
IO 复⽤模
型。 (
Redis 6.0
引⼊了多线程
IO )
7. Redis
⽀持发布订阅模型、
Lua
脚本、事务等功能,⽽
Memcached
不⽀持。并且,Redis
⽀持更多的编程语⾔。
8. Memcached
过期数据的删除策略只⽤了惰性删除,⽽
Redis 同时使⽤了惰性删除与定期删
除。
2.4缓存数据的处理流程是怎样的?
1.
如果⽤户请求的数据在缓存中就直接返回。
2.
缓存中不存在的话就看数据库中是否存在。
3.
数据库中存在的话就更新缓存中的数据。
4.
数据库中不存在的话就返回空数据。
2.5为什么要⽤ Redis/为什么要⽤缓存?
是为了提升⽤户体验以及应对更多的⽤户。
⾼性能
:
我们设想这样的场景:
假如⽤户第⼀次访问数据库中的某些数据的话,这个过程是⽐较慢,毕竟是从硬盘中读取的。但是,如果说,⽤户访问的数据属于⾼频数据并且不会经常改变的话,那么我们就可以很放⼼地将该⽤户访问的数据存在缓存中。
这样有什么好处呢?
那就是保证⽤户下⼀次再访问这些数据的时候就可以直接从缓存中获取了。
操作缓存就是直接操作内存,所以速度相当快。
不过,要保持数据库和缓存中的数据的⼀致性。 如果数据库中的对应数据改变的之后,同步改变
缓存中相应的数据即可!
⾼并发:
⼀般像
MySQL
这类的数据库的
QPS
⼤概都在
1w
左右(
4
核
8g
) ,但是使⽤
Redis
缓存之后
很容易达到
10w+
,甚⾄最⾼能达到
30w+
(就单机
redis
的情况,
redis
集群的话会更⾼)。
QPS(Query Per Second):服务器每秒可以执⾏的查询次数;
2.6Redis 常⻅数据结构以及使⽤场景分析
1.string
1.
介绍
:
string
数据结构是简单的
key-value
类型。虽然
Redis
是⽤
C
语⾔写的,但是
Redis
并没有使⽤
C
的字符串表示,⽽是⾃⼰构建了⼀种
简单动态字符串
(
simple dynamic
string
,
SDS
)。相⽐于
C
的原⽣字符串,
Redis
的
SDS
不光可以保存⽂本数据还可以保存
⼆进制数据,并且获取字符串⻓度复杂度为
O(1)
(
C
字符串为
O(N)
)
,
除此之外
,Redis
的
SDS API
是安全的,不会造成缓冲区溢出。
2.
常⽤命令
:
set,get,strlen,exists,dect,incr,setex
等等。
3.
应⽤场景
:⼀般常⽤在需要计数的场景,⽐如⽤户的访问次数、热点⽂章的点赞转发数量等
等。
2.list
1.
介绍
:
list
即是
链表
。链表是⼀种⾮常常⻅的数据结构,特点是易于数据元素的插⼊和删除
并且且可以灵活调整链表⻓度,但是链表的随机访问困难。许多⾼级编程语⾔都内置了链表的实现⽐如 Java
中的
LinkedList
,但是
C
语⾔并没有实现链表,所以
Redis
实现了⾃⼰的链表数据结构。Redis
的
list
的实现为⼀个
双向链表
,即可以⽀持反向查找和遍历,更⽅便操作,不过带来了部分额外的内存开销。
2.
常⽤命令
:
rpush,lpop,lpush,rpop,lrange
,
llen
等。
3.
应⽤场景
:
发布与订阅或者说消息队列、慢查询。
3.hash
1.
介绍
:
hash
类似于
JDK1.8
前的
HashMap
,内部实现也差不多
(
数组 + 链表
)
。不过,Redis 的
hash
做了更多优化。另外,
hash
是⼀个
string
类型的 field 和
value
的映射表,
特别适合⽤于存储对象
,后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的值。 ⽐如我们可以 hash
数据结构来存储⽤户信息,商品信息等等。
2.
常⽤命令:
hset,hmset,hexists,hget,hgetall,hkeys,hvals
等。
3.
应⽤场景
:
系统中对象数据的存储。
4.set
1.
介绍 :
set
类似于
Java
中的
HashSet
。
Redis
中的
set
类型是⼀种⽆序集合,集合中的元素没有先后顺序。当你需要存储⼀个列表数据,⼜不希望出现重复数据时,set
是⼀个很好的选择,并且 set
提供了判断某个成员是否在⼀个
set
集合内的重要接⼝,这个也是
list
所不能提供的。可以基于 set
轻易实现交集、并集、差集的操作。⽐如:你可以将⼀个⽤户所有的关注⼈存在⼀个集合中,将其所有粉丝存在⼀个集合。Redis
可以⾮常⽅便的实现如共同关注、共同粉丝、共同喜好等功能。这个过程也就是求交集的过程。
2.
常⽤命令:
sadd,spop,smembers,sismember,scard,sinterstore,sunion
等。
3.
应⽤场景
:
需要存放的数据不能重复以及需要获取多个数据源交集和并集等场景
5.sorted set
1.
介绍:
和
set
相⽐,
sorted set
增加了⼀个权重参数
score
,使得集合中的元素能够按
score进⾏有序排列,还可以通过 score
的范围来获取元素的列表。有点像是
Java
中
HashMap和 TreeSet
的结合体。
2.
常⽤命令:
zadd,zcard,zscore,zrange,zrevrange,zrem
等。
3.
应⽤场景:
需要对数据根据某个权重进⾏排序的场景。⽐如在直播系统中,实时排⾏信息包含直播间在线⽤户列表,各种礼物排⾏榜,弹幕消息(可以理解为按消息维度的消息排⾏榜)等信息
2.7Redis 单线程模型详解
Redis
基于
Reactor
模式来设计开发了⾃⼰的⼀套⾼效的事件处理模型
,这套事件处理模型对应的是
Redis中的⽂件事件处理器(file event handler
)。由于⽂件事件处理器(
file event handler
)是单线程⽅式运⾏的,所以我们⼀般都说 Redis
是单线程模型。
既然是单线程,那怎么监听⼤量的客户端连接呢?
Redis
通过
IO
多路复⽤程序
来监听来⾃客户端的⼤量连接(或者说是监听多个
socket
),它会
将感兴趣的事件及类型
(
读、写)注册到内核中并监听每个事件是否发⽣。这样的好处⾮常明显: I/O
多路复⽤技术的使⽤让
Redis
不需要额外创建多余的线程来监听客户
端的⼤量连接,降低了资源的消耗
。另外, Redis
服务器是⼀个事件驱动程序,服务器需要处理两类事件:
1.
⽂件事件
; 2.
时间事件。我们接触最多的还是 ⽂件事件
(客户端进⾏读取写⼊等操作,涉及⼀系列⽹络信)。
⽂件事件处理器(
file event handler
)主要是包含
4
个部分:
1.多个
socket
(客户端连接)
2.IO
多路复⽤程序(⽀持多个客户端连接的关键)
3.⽂
件事件分派器(将
socket
关联到相应的事件处理器)
4.事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)
2.8Redis 没有使⽤多线程?为什么不使⽤多线程?
虽然说
Redis
是单线程模型,但是, 实际上,
Redis
在
4.0
之后的版本中就已经加⼊了对多线程
的⽀持。
不过,
Redis 4.0
增加的多线程主要是针对⼀些⼤键值对的删除操作的命令,使⽤这些命令就会使⽤主处理之外的其他线程来“
异步处理
”
。 ⼤体上来说,Redis 6.0
之前主要还是单线程处理。
那,
Redis6.0
之前 为什么不使⽤多线程?
我觉得主要原因有下⾯
3
个:
1.
单线程编程容易并且更容易维护;
2. Redis
的性能瓶颈不再在
CPU
,主要在内存和⽹络;
3.
多线程就会存在死锁、线程上下⽂切换等问题,甚⾄会影响性能。
2.9Redis6.0 之后为何引⼊了多线程?
Redis6.0
引⼊多线程主要是为了提⾼⽹络
IO 读写性能
,因为这个算是
Redis
中的⼀个性能瓶颈
(
Redis
的瓶颈主要受限于内存和⽹络)。虽然,Redis6.0
引⼊了多线程,但是
Redis
的多线程只是在⽹络数据的读写这类耗时操作上使⽤了, 执⾏命令仍然是单线程顺序执⾏。因此,你也不需要担⼼线程安全问题。Redis6.0 的多线程默认是禁⽤的,只使⽤主线程。如需开启需要修改
redis
配置⽂件
redis.conf。开启多线程后,还需要设置线程数,否则是不⽣效的。同样需要修改 redis
配置⽂件
redis.conf
:
2.10Redis 给缓存数据设置过期时间有啥⽤?
有助于缓解内存的消耗,以及保证过期时间
因为内存是有限的,如果缓存中的所有数据都是⼀直保存的话,分分钟直接
Out of memory
。Redis ⾃带了给缓存数据设置过期时间的功能
Redis
中除了字符串类型有⾃⼰独有设置过期时间的命令
setex
外,其他⽅法都需要依靠
expire
命令来设置过期时间 。另外,
persist
命令可以移除⼀个键的过期时间
很多时候,我们的业务场景就是需要某个数据只在某⼀时间段内存在,⽐如我们的短信验证码可
能只在
1
分钟内有效,⽤户登录的
token
可能只在
1
天内有效。
如果使⽤传统的数据库来处理的话,⼀般都是⾃⼰判断过期,这样更麻烦并且性能要差很多。
2.11Redis是如何判断数据是否过期的呢?
Redis
通过⼀个叫做过期字典(可以看作是
hash
表)来保存数据过期的时间。过期字典的键指向
Redis
数据库中的某个
key(
键
)
,过期字典的值是⼀个
long long
类型的整数,这个整数保存了
key
所
指向的数据库键的过期时间(毫秒精度的
UNIX
时间戳)。
过期字典是存储在redisDb这个结构⾥的
2.12过期的数据的删除策略了解么?
1.
惰性删除
:只会在取出
key
的时候才对数据进⾏过期检查。这样对
CPU
最友好,但是可能会
造成太多过期
key
没有被删除。
2.
定期删除
: 每隔⼀段时间抽取⼀批
key
执⾏删除过期
key
操作。并且,
Redis
底层会通过限
制删除操作执⾏的时⻓和频率来减少删除操作对
CPU
时间的影响。
定期删除对内存更加友好,惰性删除对CPU
更加友好。两者各有千秋,所以
Redis
采⽤的是
定期
删除
+
惰性
/
懒汉式删除
。
但是,仅仅通过给 key
设置过期时间还是有问题的。因为还是可能存在定期删除和惰性删除漏掉
了很多过期
key
的情况。这样就导致⼤量过期
key
堆积在内存⾥,然后就
Out of memory
了。
怎么解决这个问题呢?答案就是:
Redis
内存淘汰机制。
2.13Redis 内存淘汰机制了解么?
1.
volatile-lru
(
least recently used
)
:从已设置过期时间的数据集中挑选最近最少使⽤的数据淘汰
2.
volatile-ttl
:从已设置过期时间的数据集
中挑选将要过期的数据淘汰
3.
volatile-random
:从已设置过期时间的数据集
中任意选择数据淘汰
4.
allkeys-lru(least recently used)
:当内存不⾜以容纳新写⼊数据时,在键空间中,移除
最近最少使⽤的
key
5.
allkeys-random
:从数据集
中任意选择数据淘汰
6.
no-eviction
:禁⽌驱逐数据,也就是说当内存不⾜以容纳新写⼊数据时,新写⼊操作会报
错。
4.0
版本后增加以下两种:
7.
volatile-lfu
(
least frequently used
)
:从已设置过期时间的数据集
中挑选最不经常使⽤的数据淘汰
8.
allkeys-lfu
(
least frequently used
)
:当内存不⾜以容纳新写⼊数据时,在键空间中,移
除最不经常使⽤的
key
2.14Redis 持久化机制(怎么保证 Redis 挂掉之后再重启数据可以进⾏恢复)
Redis
不同于
Memcached
的很重要⼀点就是,
Redis
⽀持持久化,⽽且⽀持两种不同的持久化
操作。
Redis
的⼀种持久化⽅式叫快照(
snapshotting
,
RDB
),另⼀种⽅式是只追加⽂件
(
append-only file, AOF
)。
快照(
snapshotting
)持久化(
RDB
)
Redis
可以通过创建快照来获得存储在内存⾥⾯的数据在某个时间点上的副本。
Redis
创建快照
之后,可以对快照进⾏备份,可以将快照复制到其他服务器从⽽创建具有相同数据的服务器副本
(
Redis
主从结构,主要⽤来提⾼
Redis
性能),还可以将快照留在原地以便重启服务器的时候
使⽤。快照持久化是 Redis
默认采⽤的持久化⽅式
在
Redis.conf
配置⽂件中默认有此下配置:
![](https://img-blog.csdnimg.cn/direct/cde5a026e3ad45afa0512b030c17a9d1.png)
AOF
(
append-only file
)持久化
与快照持久化相⽐,
AOF
持久化 的实时性更好,因此已成为主流的持久化⽅案。默认情况下Redis 没有开启
AOF
(
append only file
)⽅式的持久化,可以通过
appendonly
参数开启:
![](https://img-blog.csdnimg.cn/direct/0f55186ef2884facba390aac681f528b.png)
开启
AOF
持久化后每执⾏⼀条会更改
Redis
中的数据的命令,
Redis
就会将该命令写⼊硬盘中的 AOF
⽂件。
AOF
⽂件的保存位置和
RDB
⽂件的位置相同,都是通过
dir
参数设置的,默认的⽂件名是 appendonly.aof
。
在
Redis
的配置⽂件中存在三种不同的
AOF
持久化⽅式,它们分别是:
![](https://img-blog.csdnimg.cn/direct/d8a6ac4d59ae45bd92b01b84294c8e14.png)
为了兼顾数据和写⼊性能,⽤户可以考虑
appendfsync everysec
选项 ,让
Redis
每秒同步⼀次
AOF
⽂件,
Redis
性能⼏乎没受到任何影响。⽽且这样即使出现系统崩溃,⽤户最多只会丢失⼀
秒之内产⽣的数据。当硬盘忙于执⾏写⼊操作的时候,
Redis
还会优雅的放慢⾃⼰的速度以便适
应硬盘的最⼤写⼊速度。
Redis 4.0
对于持久化机制的优化
Redis 4.0
开始⽀持
RDB
和
AOF 的混合持久化。AOF 重写的时候就直接把
RDB
的内容写到
AOF
⽂件开头。这样做的好处是可以结合 RDB
和
AOF
的优点
,
快速加载同时避免丢失过多的数据。当然缺点也是有的,AOF ⾥⾯的
RDB
部分是压缩格式不再是
AOF
格式,可读性较差。
AOF
重写
AOF
重写可以产⽣⼀个新的
AOF
⽂件,这个新的
AOF
⽂件和原有的
AOF
⽂件所保存的数据库
状态⼀样,但体积更⼩。
AOF
重写是⼀个有歧义的名字,该功能是通过读取数据库中的键值对来实现的,程序⽆须对现有
AOF
⽂件进⾏任何读⼊、分析或者写⼊操作。在执⾏ BGREWRITEAOF(bgrewriteAOF)
命令时,
Redis
服务器会维护⼀个
AOF
重写缓冲区,该缓冲区会在⼦进程创建新 AOF
⽂件期间,记录服务器执⾏的所有写命令。当⼦进程完成创建新
AOF
⽂件的⼯作之后,服务器会将重写缓冲区中的所有内容追加到新 AOF
⽂件的末尾,使得新旧两个
AOF
⽂件所保存的数据库状态⼀致。最后,服务器⽤新的 AOF
⽂件替换旧的
AOF
⽂件,以此来完成AOF ⽂件重写操作
2.15Redis 事务
Redis
可以通过
MULTI
,
EXEC
,
DISCARD
和
WATCH
等命令来实现事务
(transaction)
功能。
使⽤
MULTI
命令后可以输⼊多个命令。
Redis
不会⽴即执⾏这些命令,⽽是将它们放到队列,当
调⽤了
EXEC
命令将执⾏所有命令。
Redis
是不⽀持
roll back
的,因⽽不满⾜原⼦性的(⽽且不满⾜持久性)。
Redis
官⽹也解释了⾃⼰为啥不⽀持回滚。简单来说就是
Redis
开发者们觉得没必要⽀持回滚,这
样更简单便捷并且性能更好。
Redis
开发者觉得即使命令执⾏错误也应该在开发过程中就被发现
⽽不是⽣产过程中。
你可以将
Redis
中的事务就理解为 :
Redis
事务提供了⼀种将多个命令请求打包的功能。然后,
再按顺序执⾏打包的所有命令,并且不会被中途打断。
2.16缓存穿透
什么是缓存穿透?
缓存穿透说简单点就是⼤量请求的
key
根本不存在于缓存中,导致请求直接到了数据库上,根本
没有经过缓存这⼀层。举个例⼦:某个⿊客故意制造我们缓存中不存在的
key
发起⼤量请求,导
致⼤量请求落到数据库。
缓存穿透情况的处理流程是怎样的?
⽤户的请求最终都要跑到数据库中查询⼀遍。
有哪些解决办法?
最基本的就是⾸先做好参数校验,⼀些不合法的参数请求直接抛出异常信息返回给客户端。⽐如
查询的数据库
id
不能⼩于
0
、传⼊的邮箱格式不对的时候直接返回错误消息给客户端等等。
1
)缓存⽆效
key
如果缓存和数据库都查不到某个
key
的数据就写⼀个到
Redis
中去并设置过期时间,具体命令如
下:
SET key value EX 10086
。这种⽅式可以解决请求的
key
变化不频繁的情况,如果⿊客恶意
攻击,每次构建不同的请求
key
,会导致
Redis
中缓存⼤量⽆效的
key
。很明显,这种⽅案并不
能从根本上解决此问题。如果⾮要⽤这种⽅式来解决穿透问题的话,尽量将⽆效的
key
的过期时
间设置短⼀点⽐如
1
分钟。
2
)布隆过滤器
布隆过滤器是⼀个⾮常神奇的数据结构,通过它我们可以⾮常⽅便地判断⼀个给定数据是否存在
于海量数据中。我们需要的就是判断
key
是否合法,有没有感觉布隆过滤器就是我们想要找的那
个
“
⼈
”
。
具体是这样做的:把所有可能存在的请求的值都存放在布隆过滤器中,当⽤户请求过来,先判断
⽤户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户
端,存在的话才会⾛下⾯的流程。
但是,需要注意的是布隆过滤器可能会存在误判的情况。总结来说就是:
布隆过滤器说某个元素
存在,⼩概率会误判。布隆过滤器说某个元素不在,那么这个元素⼀定不在。
当⼀个元素加⼊布隆过滤器中的时候,会进⾏哪些操作:
1.
使⽤布隆过滤器中的哈希函数对元素值进⾏计算,得到哈希值(有⼏个哈希函数得到⼏个哈希值)。
2.
根据得到的哈希值,在位数组中把对应下标的值设置为
1
。
当我们需要判断⼀个元素是否存在于布隆过滤器的时候,会进⾏哪些操作:
1.
对给定元素再次进⾏相同的哈希计算;
2.
得到值之后判断位数组中的每个元素是否都为
1
,如果值都为
1
,那么说明这个值在布隆过滤器中,如果存在⼀个值不为 1
,说明该元素不在布隆过滤器中。
然后,⼀定会出现这样⼀种情况:
不同的字符串可能哈希出来的位置相同。
(可以适当增加位数
组⼤⼩或者调整我们的哈希函数来降低概率)
2.17缓存雪崩
1.缓存在同⼀时间⼤⾯积的失效,后⾯的请求都直接落到了数据库上,造成数据库短时间内承受⼤量请求。
2.有⼀些被⼤量访问数据(热点缓存)在某⼀时刻⼤⾯积失效,导致对应的请求直接落到了数据库上。
有哪些解决办法?
针对
Redis
服务不可⽤的情况:
1.
采⽤
Redis
集群,避免单机出现问题整个缓存服务都没办法使⽤。
2.
限流,避免同时处理⼤量的请求。
针对热点缓存失效的情况:
1.
设置不同的失效时间⽐如随机设置缓存的失效时间。
2.
缓存永不失效。
2.18缓存击穿
给某一个key设置了过期时间,当key过期的时候,恰好这时间点对这个key有大量的并发请求过来,这些并发的请求可能会瞬间把DB压垮
![](https://img-blog.csdnimg.cn/direct/d44ec95b3c6f477bac29c3982522bf68.png)
2.18如何保证缓存和数据库数据的⼀致性(双写一致性)?
双写一致性:当修改了数据库的数据也要同时更新缓存的数据,缓存和数据库的数据要保持一致
![](https://img-blog.csdnimg.cn/direct/8cdd70db6f624eb7a495893002a71ae7.png)
允许延时一致的业务
,采用异步通知
① 使用MQ中间中间件,更新数据之后,通知缓存删除
② 利用canal中间件,不需要修改业务代码,伪装为mysql的一个从节点,canal通过读取binlog数据更新缓存
强一致性的
,采用Redisson提供的读写锁
① 共享锁:读锁readLock,加锁之后,其他线程可以共享读操作
② 排他锁:独占锁writeLock也叫,加锁之后,阻塞其他线程读写操作
![](https://img-blog.csdnimg.cn/direct/5b2a38a3ddf1407fb0bac5b0273024c2.png)
![](https://img-blog.csdnimg.cn/direct/06fa54134bf042f6bcce0d7d868371c2.png)
延迟双删:如果先删除缓存,那么再次缓存时若数据库还未更新则又不一致了。如果先操作数据库再删除缓存,如果返回缓存在删除缓存后面,那么缓存里面的值还是旧的
![](https://img-blog.csdnimg.cn/direct/a7504d6e35534f8bbea87ac2ab68bdf7.png)