不多说了,直接开始总结
数据库
1. MySQL
1.1 MySQL的索引
- 索引的介绍:索引是一种用于快速查询和检索数据的数据结构,其本质可以堪称是一种排好序的数据结构;
- 常见的索引结构分类:
- B树
- B+树
- Hash
- 红黑树
- 索引的优缺点:
- 优点:使用索引可大大加快数据的检索速度,这也是创建索引的最主要的原因;唯一性索引,可以保证数据库表中每一行数据的唯一性;
- 缺点:创建索引和维护需要花费大量时间经历,会进一步降低SQL执行的效率;索引需要使用物理文件进行存储,会耗费一定的空间
- 索引底层的数据结构选型
- Hash表,是键值对的集合,通过键可以快速取出对应的值,因此哈希表可以快速检索数据(接近于O(1))
- 既然哈希表这么快,为什么 MySQL 没有使用其作为索引的数据结构呢? 主要是因为 Hash 索引不支持顺序和范围查询。假如我们要对表中的数据进行排序或者进行范围查询,那 Hash 索引可就不行了。并且,每次 IO 只能取一个。
- B树和B+树其实全称为:多路平衡查找树,B+树是B树的一个变体,B树和B+树中的B表示的是平衡的意思。
- B 树& B+树两者有何异同呢?
- B 树的所有节点既存放键(key) 也存放数据(data),而 B+树只有叶子节点存放 key 和 data,其他内节点只存放 key。
- B 树的叶子节点都是独立的;B+树的叶子节点有一条引用链指向与它相邻的叶子节点。
- B 树的检索的过程相当于对范围内的每个节点的关键字做二分查找,可能还没有到达叶子节点,检索就结束了。而 B+树的检索效率就很稳定了,任何查找都是从根节点到叶子节点的过程,叶子节点的顺序检索很明显。
- 在 B 树中进行范围查询时,首先找到要查找的下限,然后对 B 树进行中序遍历,直到找到查找的上限;而 B+树的范围查询,只需要对链表进行遍历即可。
- 综上,B+树与 B 树相比,具备更少的 IO 次数、更稳定的查询效率和更适于范围查询这些优势。
- 数据库中B+树索引和哈希索引的区别?
- 数据结构:B+树索引使用B+树作为底层数据结构,而哈希索引使用哈希表作为底层数据结构。
- 范围查询:B+树索引支持范围查询,可以按照顺序遍历索引中的数据。而哈希索引仅支持等值查询,无法进行范围查询。
- 内存占用:B+树索引相对较大,因为需要存储额外的索引节点和指针。而哈希索引相对较小,只需要存储键值对的映射关系。
- 索引维护:B+树索引在插入和删除数据时需要维护索引的平衡性,因此效率相对较低。而哈希索引在插入和删除数据时效率较高,但需要解决哈希碰撞的问题。
- B+树的叶子结点不一定存储整行结点,可以只存储主键和指向整行数据的指针或地址。这样设计可以减少每个叶子结点的存储空间,提高查询效率。
- 聚簇索引和非聚簇索引的区别如下:
- 数据存储方式:聚簇索引将数据记录按照索引的顺序直接存储到叶子节点当中,因此相邻的数据物理上也是相邻的。非聚簇索引的话则是数据和索引分开存储,索引仅包含指向数据记录的引用;
- 查询性能:聚簇索引适合用于范围查询和按照顺序遍历数据,因为相关物理上是连续存储的。而非聚簇索引适合于等值查询,需要根据索引定位到相关的数据记录的位置之后,再读取数据。
- 索引维护: 聚簇索引在插入和删除数据时候可能需要进行数据的移动和调整,维护索引的成本相对较高。而非聚簇索引的插入和删除操作只需对索引进行修改,不需要调整数据的物理存储位置。
1.2 MySQL的事务
什么是事务?
-
数据库中的事务表示的是对数据库执行一批操作,在同一个事务中,这些操作要么全部执行,要么全部失败。
-
事务是一个原子操作,是最小的执行单元,由一个或者多个SQL语句组成。
举个例子:
-
比如A用户给B用户转账100操作,过程如下:
- 从A账户扣100
- 给B账户加100
-
如果在事务的支持下,上面最终只有2种结果:
- 操作成功:A账户减少100;B账户增加100
- 操作失败:A、B两个账户都没有发生变化
-
事务的几个特性?
- 原子性(原子操作,要么全部成功,要么全部失败)
- 一致性(语义上,必须使得数据库从一个一致性状态到另外一个一致性状态当中)
- 隔离性(事务的执行不能被其他事务所干扰,事物内部的操作以及使用的数据,对并发的其他事务而言是隔离的,不能相互干扰)
- 持久性(事务一旦提交,对数据库中的修改就是永久性的)
Mysql中的事务操作
隐式事务:事务是自动开启,比如在提交、回滚和删除等
显示事务:需要开发者自己手动开启
//设置不自动提交事务
set autocommit=0;
//执行事务操作
commit|rollback;
// 提交事务
mysql> create table test1 (a int);
Query OK, 0 rows affected (0.01 sec)
mysql> select * from test1;
Empty set (0.00 sec)
mysql> set autocommit=0;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into test1 values(1);
Query OK, 1 row affected (0.00 sec)
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from test1;
+------+
| a |
+------+
| 1 |
+------+
1 row in set (0.00 sec)
// 回滚事务
mysql> set autocommit=0;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into test1 values(2);
Query OK, 1 row affected (0.00 sec)
mysql> rollback;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from test1;
+------+
| a |
+------+
| 1 |
+------+
1 row in set (0.00 sec)
// 语法2
start transaction;//开启事务
//执行事务操作
commit|rollback;
savepoint关键字
- 在事务中如果想回滚部分的数据,可以把一大批操作分为几个部分,指定回滚到的某个部分,使用savepoint进行实现。
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into test1 values (1);
Query OK, 1 row affected (0.00 sec)
mysql> savepoint part1;//设置一个保存点
Query OK, 0 rows affected (0.00 sec)
mysql> insert into test1 values (2);
Query OK, 1 row affected (0.00 sec)
mysql> rollback to part1;//将savepint = part1的语句到当前语句之间所有的操作回滚
Query OK, 0 rows affected (0.00 sec)
mysql> commit;//提交事务
Query OK, 0 rows affected (0.00 sec)
mysql> select * from test1;
+------+
| a |
+------+
| 1 |
+------+
1 row in set (0.00 sec)
- 从上面可以看出,执行了2次插入操作,最后只插入了1条数据。
- savepoint需要结合rollback to sp1一起使用,可以将保存点sp1到rollback to之间的操作回滚掉。
只读事务
-
表示在事务中执行的是一些只读操作,如查询,但是不会做insert、update、delete操作,数据库内部对只读事务可能会有一些性能上的优化。
用法如下:
start transaction read only; // 只读事务中执行delete会报错
事务中出现的问题
大多数出现的问题都是对于多个事务中的可见性来说的,也是并发事务产生的原因
-
更新丢失
- 第一类丢失更新 :A,B 事务同时操作同一数据,A先对改数据进行了更改,B再次更改时失败然后回滚,把A更新的数据也回滚了。(事务撤销造成的撤销丢失)
- 第二类丢失更新:A,B 事务同时操作同一数据,A先对改数据进行了更改,B再次更改并且提交,把A提交的数据给覆盖了。(事务提交造成的覆盖丢失)
-
脏读
- 一个事务在执行的过程中读取到的其他事务还没有提交的数据。简单说就是别人不要的,你拿过来读取提交了~
-
读已提交
- 一个事务操作过程中可以读取到其他事务已经提交的数据。事务中的每次读取操作,读取到的都是数据库中其他事务已提交的最新的数据(相当于当前读)
-
不可重复读
- 后续读取可以读到另一事务已提交的更新数据。“可重复读” 在同一事务中多次读取数据时, 能够保证所读数据一样, 也就是后续读取不能读到另一事务已提交的更新数据。
-
可重复读
- 一个事务操作中对于一个读取操作不管多少次,读取到的结果都是一样的。
-
幻读
- 可重复读模式下,比如有个用户表,手机号码为主键,有两个事物进行如下操作
- 事务A操作如下: 1、打开事务 2、查询号码为X的记录,不存在 3、插入号码为X的数据,插入报错(为什么会报错,先向下看) 4、查询号码为X的记录,发现还是不存在(由于是可重复读,所以读取记录X还是不存在的)
- 事物B操作:在事务A第2步操作时插入了一条X的记录,所以会导致A中第3步插入报错(违反了唯一约束)
- 上面操作对A来说就像发生了幻觉一样,明明查询X(A中第二步、第四步)不存在,但却无法插入成功
- 幻读可以这么理解:事务中后面的操作(插入号码X)需要上面的读取操作(查询号码X的记录)提供支持,但读取操作却不能支持下面的操作时产生的错误,就像发生了幻觉一样。
-
事务A在操作一堆数据的时候,事务B插入了一条数据,A事务再次(第二次)查询,发现多了一条数据,像是幻觉。与不可重复读类似,不同的是一个是修改删除操作,一个是新增操作。
事务的隔离级别
- 比如A、B两个事物同时进行的时候,A是否可以看到B已提交的数据或者B未提交的数据,这个需要依靠事务的隔离级别来保证,不同的隔离级别中所产生的效果是不一样的。
- 事务隔离级别主要指的是多个事务之间数据可见性以及数据正确性的问题。分为下面的4级
- 读未提交
- 读已提交
- 可重复读
- 串行
- 上面4中隔离级别越来越强,会导致数据库的并发性也越来越低。
各自隔离级别中可能会出现的问题
| 隔离级别 | 脏读可能性 | 不可重复读可能性 | 幻读可能性 |
|---|---|---|---|
| READ-UNCOMMITTED | 有 | 有 | 有 |
| READ-COMMITTED | 无 | 有 | 有 |
| REPEATABLE-READ | 无 | 无 | 有 |
| SERIALIZABLE | 无 | 无 | 无 |
小结
- 读未提交( Read Uncommitted )
- 读未提交是隔离级别最低的一种事务级别。在这种隔离级别下,一个事务会读到另一个事务更新后但未提交的数据,如果另一个事务回滚,那么当前事务读到的数据就是脏数据,这就是脏读(Dirty Read)。
- 读已提交( Read Committed )
- 在 Read Committed 隔离级别下,一个事务可能会遇到不可重复读(Non Repeatable Read)的问题。不可重复读是指,在一个事务内,多次读同一数据,在这个事务还没有结束时,如果另一个事务恰好修改了这个数据,那么,在第一个事务中,两次读取的数据就可能不一致。
- 可重复读( Repeatable Read )
- 在Repeatable Read隔离级别下,一个事务可能会遇到幻读(Phantom Read)的问题。幻读是指,在一个事务中,第一次查询某条记录,发现没有,但是,当试图更新这条不存在的记录时,竟然能成功,并且,再次读取同一条记录,它就神奇地出现了。幻读就是没有读到的记录,以为不存在,但其实是可以更新成功的,并且,更新成功后,再次读取,就出现了。
- 可串行化( Serializable )
-
Serializable 是最严格的隔离级别。在Serializable隔离级别下,所有事务按照次序依次执行,因此,脏读、不可重复读、幻读都不会出现。
-
虽然 Serializable 隔离级别下的事务具有最高的安全性,但是,由于事务是串行执行,所以效率会大大下降,应用程序的性能会急剧降低。如果没有特别重要的情景,一般都不会使用Serializable隔离级别。
-
默认隔离级别:如果没有指定隔离级别,数据库就会使用默认的隔离级别。在MySQL中,如果使用 InnoDB,默认的隔离级别是Repeatable Read。
2. Redis
2.1 Redis为什么这么快?
- Redis基于内存存储,内存的访问速度比磁盘快了千倍;
- Redis主要是单线程事件循环和IO多路复用
- Redis内置了多种优化过后的数据类型和结构,性能很高
2.2 Redis数据类型
1. 购物车信息用 String 还是 Hash 存储更好呢?
- 由于购物车中的商品频繁修改和变动,购物车信息建议使用 Hash 存储:
- 用户 id 为 key
- 商品 id 为 field,商品数量为 value
- 用户添加商品就是往Hash里面新增field和value
- 查询购物车信息就是遍历对应的Hash
- 更改商品数量就是直接修改对应的value值(直接用set或者是运算皆可)
- 删除商品就是删除Hash中对应的field
- 清空购物车直接删除对应的key(用户ID)即可
2. 使用Redis制作一个排行榜?
- Redis中存在一个数据类型叫做
Sorted Set。 - 有相关的命令:
ZRANGE(从小到大排序),ZREVRANGE(从大到小排序),ZREVRANK(指定元素排名)
2.3 Redis生产问题
1. 缓存穿透
- 问题描述:简单地说就是大量的请求key不合理,根本就不存在缓存中和数据库当中。容易导致请求直接到数据库层面,对数据库造成巨大的压力,直接被请求弄宕机了。
-
解决方法:
-
缓存无效key
- 如果缓存和数据库都查不到某个 key 的数据就写一个到 Redis 中去并设置过期时间,具体命令如下:
SET key value EX 10086。
- 如果缓存和数据库都查不到某个 key 的数据就写一个到 Redis 中去并设置过期时间,具体命令如下:
-
-
一般情况下我们是这样设计 key 的:
表名:列名:主键名:主键值
public Object getObjectInclNullById(Integer id) {
// 从缓存中获取数据
Object cacheValue = cache.get(id);
// 缓存为空
if (cacheValue == null) {
// 从数据库中获取
Object storageValue = storage.get(key);
// 缓存空对象
cache.set(key, storageValue);
// 如果存储数据为空,需要设置一个过期时间(300秒)
if (storageValue == null) {
// 必须设置过期时间,否则有被攻击的风险
cache.expire(key, 60 * 5);
}
return storageValue;
}
return cacheValue;
}
-
布隆过滤器
-
布隆过滤器是流程如下:
-
需要注意的是:布隆过滤器说某个元素存在,小概率会误判。布隆过滤器说某个元素不在,那么这个元素一定不在。
-
当一个元素加入布隆过滤器中的时候,会进行哪些操作:
- 使用布隆过滤器中的哈希函数对元素值进行计算,得到哈希值(有几个哈希函数得到几个哈希值)。
- 根据得到的哈希值,在位数组中把对应下标的值置为 1。
-
当我们需要判断一个元素是否存在于布隆过滤器的时候,会进行哪些操作:
- 对给定元素再次进行相同的哈希计算;
- 得到值之后判断位数组中的每个元素是否都为 1,如果值都为 1,那么说明这个值在布隆过滤器中,如果存在一个值不为 1,说明该元素不在布隆过滤器中。
-
然后,一定会出现这样一种情况:不同的字符串可能哈希出来的位置相同。 (可以适当增加位数组大小或者调整我们的哈希函数来降低概率)
-
2. 缓存击穿
-
问题描述:请求的key是热点数据(也就是系统中频繁访问的数据),某个数据是存在于数据库当中,但是并不存在于缓存当中。(通常是由于缓存中的那份数据已经过期了)
-
举个例子:秒杀进行过程中,缓存中的某个秒杀商品的数据突然过期,这就导致瞬时大量对该商品的请求直接落到数据库上,对数据库造成了巨大的压力。
-
解决方法:
- 设置热点数据永不过期或者过期的时间较长
- 针对热点数据提前预热装入缓存中并设置合理的过期时间
- 请求数据库写数据到缓存之前,先设置互斥锁,保证只有一个请求会落到数据库中,减少数据库的压力
-
总结缓存穿透和缓存击穿的区别?
-
缓存穿透当中,请求的key既不存在于缓存中,也不存在于数据库当中
-
缓存击穿当中,key对应的是热点数据,该数据存在于数据库当中,但是并不存在于缓存当中(通常是因为缓存中的那份数据已经过期了~)
-
3. 缓存雪崩
-
问题描述: 缓存在同一时间大面积的失效,导致大量的请求都直接落到了数据库上,对数据库造成了巨大的压力。 这就好比雪崩一样,摧枯拉朽之势,数据库的压力可想而知,可能直接就被这么多请求弄宕机了。
-
解决方法:
针对 Redis 服务不可用的情况:
- 采用 Redis 集群,避免单机出现问题整个缓存服务都没办法使用。
- 限流,避免同时处理大量的请求。
- 多级缓存,例如本地缓存+Redis 缓存的组合,当 Redis 缓存出现问题时,还可以从本地缓存中获取到部分数据。
针对热点缓存失效的情况:
-
设置不同的失效时间比如随机设置缓存的失效时间。
-
缓存永不失效(不太推荐,实用性太差)。
-
缓存预热,也就是在程序启动后或运行过程中,主动将热点数据加载到缓存中。
-
常见的缓存预热有:1. 定时服务,定时触发缓存预热的逻辑,将数据库中的热点数据查询出来并存入缓存中去。2. 使用消息队列,比如kafka,异地进行缓存预热,将数据库中的热点数据的主键或者ID发送到消息队列中去,由缓存服务消费消息队列的数据,根据主键或者ID查询数据库中的数据,并更新回缓存。
-
2.4 Redis中常用的缓存读写策略
1. 旁路缓存模式
-
比较适合用于缓存读写模式,适合请求读取较多的场景;
-
写:
- 先更新db
- 后直接删除缓存cache
如果顺序相反,先删除cache,再进行db的更新的话?
- 可能会造成 数据库(db)和缓存(Cache)数据不一致的问题。比如先是删除了缓存中的数据,再更新db。此时更新操作失败或被中断,则Redis中的缓存已经被删除,但数据库中的数据却没有更新,导致缓存与数据库不一致。这时候如果有其他应用程序直接从缓存中读取数据,就会读到错误的数据。
- 缓存击穿:如果在缓存被删除后,有大量的并发请求直接查询数据库,而此时缓存还没有重新生成,那么这些请求就会全部打到数据库上
- 采取的应对措施:
- 按照先更新数据库,再删除cache数据顺序
- 使用互斥锁避免缓存击穿
- 使用热点数据预加载,在系统启动或低峰期,预先将热点数据加载到缓存中,保证缓存中的数据始终是最新的
-
读:
- 从cache中读取数据,读取到数据就直接返回
- cache中读取不到数据的话,就从db中读取数据返回
- 再把数据放入cache中。
-
下面图示分别是写和读操作:
- 缺陷:
- 首次的请求数据一定不在cache当中(可以将热点数据提前放入cache中解决)
- 写操作比较频繁会导致cache中的数据被频繁删除,影响缓存的命中率
2. 读写穿透
-
写操作:
-
读操作:
-
缺陷:和 Cache Aside Pattern 一样, Read-Through Pattern 也有首次请求数据一定不再 cache 的问题,对于热点数据可以提前放入缓存中。
3. 异步缓存写入
- 与前两个又有很大的不同:Read/Write Through 是同步更新 cache 和 db,而 Write Behind 则是只更新缓存,不直接更新 db,而是改为异步批量的方式来更新 db。
- 不足:比如 cache 数据可能还没异步更新 db 的话,cache 服务可能就就挂掉了。
- 适合场景:db的读写性能很高,适合一些数据经常变化又对数据一致性要求没那么高的场景,比如浏览量、点赞量。
2.5 Redis中的持久化机制
- 持久化就是把内存中的数据写入到硬盘中去,大部分原因是为了重用数据,或者是需要做数据的同步。
1. RDB持久化
Redis 可以通过创建快照来获得存储在内存里面的数据在 某个时间点 上的副本。
-
RDB是将Redis在内存中的数据以二进制格式快照的方式写入磁盘,生成一个RDB文件。
-
RDB持久化适合用于备份,将快照复制到其他服务器中从而创建具有相同数据的服务器符本(Redis主从结构,主要用于提高redis性能)、灾难恢复和离线数据分析等场景。
-
RDB持久化可以手动触发,也可以基于配置项设置定期自动触发。
-
RDB文件通过压缩和序列化来减小文件大小。
-
RDB加载时,Redis会读取RDB文件并将其中的数据恢复到内存中。
2. AOF持久化
流程分为下面的几步:
-
命令追加(append):所有的写命令会追加到 AOF 缓冲区中。
-
文件写入(write):将 AOF 缓冲区的数据写入到 AOF 文件中。这一步需要调用
write函数(系统调用),write将数据写入到了系统内核缓冲区之后直接返回了(延迟写)。注意!!!此时并没有同步到磁盘。 -
文件同步(fsync):AOF 缓冲区根据对应的持久化方式(
fsync策略)向硬盘做同步操作。这一步需要调用fsync函数(系统调用),fsync针对单个文件操作,对其进行强制硬盘同步,fsync将阻塞直到写入磁盘完成后返回,保证了数据持久化。 -
文件重写(rewrite):随着 AOF 文件越来越大,需要定期对 AOF 文件进行重写,达到压缩的目的。
-
重启加载(load):当 Redis 重启时,可以加载 AOF 文件进行数据恢复。
-
AOF以日志追加的方式记录所有的写操作指令,将这些指令写入AOF文件。
-
AOF文件记录了构建当前数据集状态所需的所有写操作。
-
AOF持久化适合用于保证数据的安全性和完整性。
-
AOF文件的写入可以使用不同的策略:每个写命令、每秒钟同步一次或者按条件触发。
-
AOF重写可以通过后台进程将AOF文件压缩为更小的文件,同时保留相同的数据集。
3. 如何选择RDB和AOF?
- 选择使用RDB还是AOF持久化取决于对数据恢复能力、性能和存储空间的需求。下面是对RDB和AOF持久化的优劣势进行分析:
RDB持久化的优势:
- 性能:RDB持久化生成的快照文件是以二进制格式保存在磁盘上的,加载时比AOF持久化更快速。
- 存储空间:RDB文件通常比AOF文件更小,因为它们经过了压缩和序列化处理,适用于需要节省磁盘空间的场景。
- 恢复速度:由于RDB文件保存了完整的数据快照,可以快速恢复数据,适合用于备份和灾难恢复。
RDB持久化的劣势:
- 数据丢失:由于RDB是定期生成的快照,如果Redis意外关闭或崩溃,最后一次生成的RDB文件中的数据可能会有丢失。
- 数据恢复不精确:RDB只能提供最近一次生成快照时的数据状态,无法提供更细粒度的操作历史。
AOF持久化的优势:
- 数据安全性:AOF持久化记录了所有写操作指令,通过重放这些指令可以还原完整的数据状态,可以提供更高的数据安全性。
- 数据恢复精确:AOF持久化记录了每个写操作,可以精确到指令级别地进行数据恢复。
- 可读性:AOF文件是一个文本文件,可以查看和分析其中的写操作历史。
AOF持久化的劣势:
- 性能:相对于RDB持久化,AOF持久化会增加写操作的负担,可能对性能造成一定影响。
- 存储空间:由于AOF文件保存了完整的写操作历史,相比RDB文件,AOF文件通常更大。
综上所述,根据实际需求可以选择以下策略:
- 如果对数据恢复速度、存储空间和加载性能要求较高,且可以接受一定程度的数据丢失,可以选择使用RDB持久化。
- 如果对数据安全性和精确恢复要求较高,可以选择使用AOF持久化。
- 对于更高的数据安全性和灾难恢复能力,可以同时使用RDB和AOF持久化,以提供多重保障。
2.6 Redis的线程模式
-
多线程和单线程总结:
- Redis 选择使用单线程模型处理客户端的请求主要还是因为 CPU 不是 Redis 服务器的瓶颈,所以使用多线程模型带来的性能提升并不能抵消它带来的开发成本和维护成本,系统的性能瓶颈也主要在网络 I/O 操作上;
- Redis 引入多线程操作也是出于性能上的考虑,对于一些大键值对的删除操作,通过多线程非阻塞地释放内存空间也能减少对 Redis 主线程阻塞的时间,提高执行的效率。
2.7 Redis集群模式
- Redis集群主要是通过将多个Redis节点连接到一起以实现高可用性、数据分片和负载均衡的技术。允许的是Redis在不同的节点中提供服务、提高整体性能和可靠性。根据搭建的方式和集群的特性,Redis集群主要包含三大模式:主从复制模式、哨兵模式、Cluster模式
Redis集群的作用和优势
-
高可用性:Redis集群可以在某个节点发生故障时,自动进行故障转移,保证服务的持续可用。
-
负载均衡:Redis集群可以将客户端请求分发到不同的节点上,有效地分摊节点的压力,提高系统的整体性能。
-
容灾恢复:通过主从复制或哨兵模式,Redis集群可以在主节点出现故障时,快速切换到从节点,实现业务的无缝切换。
-
数据分片:在Cluster模式下,Redis集群可以将数据分散在不同的节点上,从而突破单节点内存限制,实现更大规模的数据存储。
-
易于扩展:Redis集群可以根据业务需求和系统负载,动态地添加或移除节点,实现水平扩展。
1. 主从复制模式
-
主从复制是Redis的一种基本集群模式,它通过将一个Redis节点(主节点)的数据复制到一个或多个其他Redis节点(从节点)来实现数据的冗余和备份。
主节点负责处理客户端的写操作,同时从节点会实时同步主节点的数据。客户端可以从从节点读取数据,实现读写分离,提高系统性能。
-
-
优点:
- 配置简单,易于实现。
- 实现数据冗余,提高数据可靠性。
- 读写分离,提高系统性能。
-
缺点:
- 主节点故障时,需要手动切换到从节点,故障恢复时间较长。
- 主节点承担所有写操作,可能成为性能瓶颈。
- 无法实现数据分片,受单节点内存限制。
-
主从复制模式适用于以下场景:
-
数据备份和容灾恢复:通过从节点备份主节点的数据,实现数据冗余。
-
读写分离:将读操作分发到从节点,减轻主节点压力,提高系统性能。
-
在线升级和扩展:在不影响主节点的情况下,通过增加从节点来扩展系统的读取能力。
-
2. 哨兵模式
-
哨兵模式是在主从复制基础上加入了哨兵节点,实现了自动故障转移。哨兵节点是一种特殊的Redis节点,它会监控主节点和从节点的运行状态。当主节点发生故障时,哨兵节点会自动从从节点中选举出一个新的主节点,并通知其他从节点和客户端,实现故障转移。
-
-
优点:
- 自动故障转移,提高系统的高可用性。
- 具有主从复制模式的所有优点,如数据冗余和读写分离。
-
缺点:
- 配置和管理相对复杂。
- 依然无法实现数据分片,受单节点内存限制。
-
哨兵模式适用于以下场景:
- 高可用性要求较高的场景:通过自动故障转移,确保服务的持续可用。
- 数据备份和容灾恢复:在主从复制的基础上,提供自动故障转移功能。
-
总结:哨兵模式在主从复制模式的基础上实现了自动故障转移,提高了系统的高可用性。
3. Cluster模式
-
Cluster模式是Redis的一种高级集群模式,它通过数据分片和分布式存储实现了负载均衡和高可用性。在Cluster模式下,Redis将所有的键值对数据分散在多个节点上。每个节点负责一部分数据,称为槽位。通过对数据的分片,Cluster模式可以突破单节点的内存限制,实现更大规模的数据存储。
-
-
优点:
- 数据分片,实现大规模数据存储。
- 负载均衡,提高系统性能。
- 自动故障转移,提高高可用性。
-
缺点:
- 配置和管理较复杂。
- 一些复杂的多键操作可能受到限制。
-
Cluster模式适用于以下场景:
- 大规模数据存储:通过数据分片,突破单节点内存限制。
- 高性能要求场景:通过负载均衡,提高系统性能。
- 高可用性要求场景:通过自动故障转移,确保服务的持续可用。
-
总结:Cluster模式在提供高可用性的同时,实现了数据分片和负载均衡,适用于大规模数据存储和高性能要求的场景。然而,它的配置和管理相对复杂,且某些复杂的多键操作可能受到限制。
总结
-
主从复制模式:适用于数据备份和读写分离场景,配置简单,但在主节点故障时需要手动切换。
-
哨兵模式:在主从复制的基础上实现自动故障转移,提高高可用性,适用于高可用性要求较高的场景。
-
Cluster模式:通过数据分片和负载均衡实现大规模数据存储和高性能,适用于大规模数据存储和高性能要求场景。
Spring
1. IoC和Aop详解
1.1 IoC的定义
- 控制反转/反转控制,本质上是Java开发领域对象的创建和管理的问题。
- 传统的开发:使用的是在类A中通过new 关键字将B的对象 new出来;**使用IoC的思想进行开发:**通过IoC的容器(Spring框架)帮助我们进行实例化对象,我们需要的对象直接从IoC容器中查找即可。
- 控制和反转
- 控制:指的是对象的创建(实例化、管理)的权力
- 反转:控制权交给外部的环境(IoC容器)
1.2 IoC解决的问题?
IoC 的思想就是两方之间不互相依赖,由第三方容器来管理相关资源。这样有什么好处呢?
-
对象之间的耦合度或者说依赖程度降低;
-
资源变的容易管理;比如你用 Spring 容器提供的话很容易就可以实现一个单例
-
使用IoC的思想,使得我们将对象的控制权(创建、管理)交给IoC容器进行管理,我们在使用的时候直接向IoC容器去要就行:
- 上图中的IUserDao的接口开发,不需要重新在Service层new 新的对象实现类,而是直接交给Spring容器进行管理。(加入注解@Autowired)
-
IoC 最常见以及最合理的实现方式叫做依赖注入(Dependency Injection,简称 DI)。
- 所谓的依赖注入,其实就是把底层类作为参数传入上层,实现上层类对下层类的“控制”;可以用函数方法传递的方式进行。
- 其实还有另外两种方法:Setter传递和接口传递。这里就不多讲了,核心思路都是一样的,都是为了实现控制反转。
推荐阅读:[Spring IOC的好处]https://www.zhihu.com/question/23277575/answer/169698662
1.3 AOP定义
- AOP就是面向切面的编程,目的是将横切关注点(如日志记录、事务管理、权限控制、接口限流、接口幂等)从核心业务逻辑中分离出来,实现代码的复用和解耦,提高代码的可维护性和可扩展性。
1.4 AOP为什么叫面向切面编程
-
AOP的核心就是将横切关注点从核心业务逻辑中分离出来,形成一个个的切面(ASPECT)
-
AOP中的关键术语:
- 横切关注点(cross-cutting concerns) :多个类或对象中的公共行为(如日志记录、事务管理、权限控制、接口限流、接口幂等等)。
- 切面(Aspect):对横切关注点进行封装的类,一个切面是一个类。切面可以定义多个通知,用来实现具体的功能。
- 连接点(JoinPoint):连接点是方法调用或者方法执行时的某个特定时刻(如方法调用、异常抛出等)。
- 通知(Advice):通知就是切面在某个连接点要执行的操作。通知有五种类型,分别是前置通知(Before)、后置通知(After)、返回通知(AfterReturning)、异常通知(AfterThrowing)和环绕通知(Around)。前四种通知都是在目标方法的前后执行,而环绕通知可以控制目标方法的执行过程。
- 切点(Pointcut):一个切点是一个表达式,它用来匹配哪些连接点需要被切面所增强。切点可以通过注解、正则表达式、逻辑运算等方式来定义。比如
execution(* com.xyz.service..*(..))匹配com.xyz.service包及其子包下的类或接口。 - 织入(Weaving):织入是将切面和目标对象连接起来的过程,也就是将通知应用到切点匹配的连接点上。常见的织入时机有两种,分别是编译期织入(AspectJ)和运行期织入(AspectJ)。
1.5 AOP的应用示例
- 创建一个切面类
LoggingAspect,用于定义日志记录的横切逻辑:
@Aspect
@Component
public class LoggingAspect{
@Before("execution(* com.example.service.*.*(..))")
public void logMethodExecution(JoinPoint joinPoint) {
String className = joinPoint.getTarget().getClass().getSimpleName();
String methodName = joinPoint.getSignature().getName();
System.out.println("Executing method: " + className + "." + methodName);
}
}
- 创建一个业务服务类
UserService,其中包含一个需要记录日志的方法:
@Service
public class UserService{
public void createUser(String username,String Password){
// 业务逻辑代码
System.out.println("Creating user: "+username);
}
}
- 在应用程序的配置文件中,启用的是AOP并开始扫描切面类
@Configuration
@ComponentScan(basePackages="com.example")
@EnableAspectAutoProxy
public class AppConfig{
// 配置其他的Bean或设置
}
- 在以上示例中,切面类
LoggingAspect使用了注解@Aspect和@Component来标识它是一个切面,并且使用@Before注解来定义了一个前置通知,在目标方法执行前记录日志。 - 通过配置文件中的
@EnableAspectJAutoProxy注解,应用程序启用了AOP的自动代理功能。当调用UserService的createUser方法时,AOP框架会自动检测并织入LoggingAspect中定义的日志记录逻辑。 - 这样,每次调用
UserService的方法时,都会先执行切面类中定义的日志记录逻辑,从而实现了方法级别的日志记录,而无需在每个方法中重复编写日志代码。
2. Spring中的事务
2.1 事务的特性(ACID)
-
原子性:事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么就全部都不起作用;
-
一致性:执行事务的前后,数据需要保持一致,例如转账业务中,无论事务是否成功,转账者和收款人的总额应该都是保持不变的;
-
隔离性:并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;
-
持久性:一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。
-
MySQL是如何保证原子性?
-
在异常发生的时候,对已经执行了的操作进行回滚,在Mysql中,恢复机制是通过
undo log进行实现的。 -
如果执行过程中遇到异常的话,我们直接利用 回滚日志 中的信息将数据回滚到修改之前的样子。并且,回滚日志会先于数据持久化到磁盘上。
-
2.2 Spring中对事务管理的支持
-
编程式事务管理
- 通过
TransactionTemplate或者是TransactionManager手动管理事务。 - 实际操作很少用
- 通过
-
声明式事务管理
- 推荐使用(代码侵入性最小),实际是通过 AOP 实现(基于
@Transactional的全注解方式使用最多)。 - 使用
@Transactional注解进行事务管理的示例代码如下:
@Transactional(propagation = Propagation.REQUIRED) public void aMethod { //do something B b = new B(); C c = new C(); b.bMethod(); c.cMethod(); }
- 推荐使用(代码侵入性最小),实际是通过 AOP 实现(基于
2.3 事务熟悉详解之事务传播行为
- 事务传播行为是为了解决业务层方法之间互相调用的事务问题。
- 当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。
-
TransactionDefinition.PROPAGATION_REQUIRED- 使用的最多的一个事务传播行为,我们平时经常使用的
@Transactional注解默认使用就是这个事务传播行为。如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。也就是说:
- 如果外部方法没有开启事务的话,
Propagation.REQUIRED修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。 - 如果外部方法开启事务并且被
Propagation.REQUIRED的话,所有Propagation.REQUIRED修饰的内部方法和外部方法均属于同一事务 ,只要一个方法回滚,整个事务均回滚。
举例:如果
aMethod()和bMethod()使用的都是PROPAGATION_REQUIRED传播行为的话,两者使用的就是同一个事务,只要其中一个方法回滚,整个事务均回滚。@Service Class A { @Autowired B b; @Transactional(propagation = Propagation.REQUIRED) public void aMethod { //do something b.bMethod(); } } @Service Class B { @Transactional(propagation = Propagation.REQUIRED) public void bMethod { //do something } } - 使用的最多的一个事务传播行为,我们平时经常使用的
-
TransactionDefinition.PROPAGATION_REQUIRES_NEW- 创建一个新的事务,如果当前存在事务,则把当前事务挂起。也就是说不管外部方法是否开启事务,
Propagation.REQUIRES_NEW修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。 - 如果我们上面的
bMethod()使用PROPAGATION_REQUIRES_NEW事务传播行为修饰,aMethod还是用PROPAGATION_REQUIRED修饰的话。如果aMethod()发生异常回滚,bMethod()不会跟着回滚,因为bMethod()开启了独立的事务。但是,如果bMethod()抛出了未被捕获的异常并且这个异常满足事务回滚规则的话,aMethod()同样也会回滚,因为这个异常被aMethod()的事务管理机制检测到了。
- 创建一个新的事务,如果当前存在事务,则把当前事务挂起。也就是说不管外部方法是否开启事务,
-
TransactionDefinition.PROPAGATION_NESTED-
如果当前存在事务,就在嵌套事务内执行;如果当前没有事务,就执行与
TransactionDefinition.PROPAGATION_REQUIRED类似的操作。也就是说:- 在外部方法开启事务的情况下,在内部开启一个新的事务,作为嵌套事务存在。
- 如果外部方法无事务,则单独开启一个事务,与
PROPAGATION_REQUIRED类似。
-
如果
bMethod()回滚的话,aMethod()不会回滚。如果aMethod()回滚的话,bMethod()会回滚
-
2.4 @Transactional 注解使用详解
-
Transactional的作用范围- 方法:推荐将注解使用于方法上,不过需要注意的是:该注解只能应用到 public 方法上,否则不生效。
- 类:如果这个注解使用在类上的话,表明该注解对该类中所有的 public 方法都生效。
- 接口:不推荐在接口上使用。
-
Transactional的常用配置参数属性名 说明 propagation 事务的传播行为,默认值为 REQUIRED,可选的值在上面介绍过 isolation 事务的隔离级别,默认值采用 DEFAULT,可选的值在上面介绍过 timeout 事务的超时时间,默认值为-1(不会超时)。如果超过该时间限制但事务还没有完成,则自动回滚事务。 readOnly 指定事务是否为只读事务,默认值为 false。 rollbackFor 用于指定能够触发事务回滚的异常类型,并且可以指定多个异常类型 -
@Transactional的事务注解原理
-
@Transactional的工作机制是基于 AOP 实现的,AOP 又是使用动态代理实现的。如果目标对象实现了接口,默认情况下会采用 JDK 的动态代理,如果目标对象没有实现了接口,会使用 CGLIB 动态代理。
-


