七、MySQL
1.0 JDBC操作数据库的基本步骤
· JDBC操作数据库的基本步骤
· 建立数据库连接
· 创建Statement对象。
· 执行SQL语句。
· 处理查询结果。
· 关闭连接和释放资源。
伪代码
public class JdbcExample {
public static void main(String[] args) {
String url = "jdbc:mysql://localhost:3306/mydatabase";
String user = "username";
String password = "password";
String query = "SELECT * FROM mytable";
try {
// 1. 加载并注册JDBC驱动
Class.forName("com.mysql.cj.jdbc.Driver");
// 2. 建立数据库连接
Connection connection = DriverManager.getConnection(url, user, password);
// 3. 创建Statement对象
Statement statement = connection.createStatement();
// 4. 执行SQL查询
ResultSet resultSet = statement.executeQuery(query);
// 5. 处理查询结果
while (resultSet.next()) {
int id = resultSet.getInt("id");
String name = resultSet.getString("name");
// 处理其他列...
System.out.println("ID: " + id + ", Name: " + name);
}
// 6. 关闭连接和释放资源
resultSet.close();
statement.close();
connection.close();
} catch (ClassNotFoundException e) {
System.err.println("JDBC Driver not found.");
e.printStackTrace();
} catch (SQLException e) {
System.err.println("SQL Error occurred.");
e.printStackTrace();
}
}
}
}
1.1 Statement和PreparedStatement的区别
安全性:Statement在执行SQL语句时存在安全风险,因为它不能进行预编译,也不能使用占位符,这可能导致SQL语句拼接错误,进而容易受到SQL注入攻击。而PreparedStatement是Statement的子类,它可以预编译SQL语句,并使用占位符来避免SQL注入攻击,因此更加安全。
效率:每次使用Statement执行SQL语句时,数据库都需要重新编译该语句,这可能会消耗大量的时间和资源。而PreparedStatement支持预编译,即SQL语句在第一次执行时就被编译并存储在数据库中,后续执行时直接调用已编译的SQL语句,从而提高了执行效率。此外,PreparedStatement还支持批处理,可以一次性执行多条SQL语句,进一步提高了效率。
功能:Statement的功能相对简单,主要用于执行不带参数的SQL语句。而PreparedStatement则更加强大,它允许执行带有动态参数的SQL语句,这些参数可以在执行SQL语句之前预编译,从而提高了代码的灵活性和可读性。
1.2 数据库连接池的基本原理与作用
原理:
数据库连接池的基本原理在于“连接复用”。在程序启动时,根据预设的配置,建立足够数量的数据库连接,并将这些连接组成一个连接池。当应用程序需要访问数据库时,它可以从连接池中获取一个已经存在的连接,而不是重新建立一个新的连接。使用完连接后,应用程序会将连接归还给连接池,而不是直接关闭它。这样,连接池中的连接可以被多次复用,从而避免了频繁创建和关闭数据库连接的开销。
作用:
数据库连接池的作用主要体现在以下几个方面:
资源重用:通过复用连接,数据库连接池显著减少了创建和关闭连接的开销,从而提高了系统的整体性能。
更快的系统响应速度:由于连接池中的连接已经预先建立并处于可用状态,应用程序可以迅速获取连接并执行数据库操作,从而缩短了系统的响应时间。
控制资源使用:连接池通过限制连接的数量,防止了因过多连接而导致的系统资源浪费和负载异常。这使得系统能够在大量用户应用时保持稳定性。
统一管理:连接池提供了一种统一的连接管理方式,避免了数据库连接泄露等问题,同时也方便了监控和管理数据库连接的使用情况。
1.3 数据库三大范式
数据库的三大范式是关系型数据库设计时的重要规则,它们确保了数据的完整性和一致性。
第一范式
强调原子性,即数据库表的每一列都是不可分割的最小数据单元。
第二范式
在第一范式的基础上,要求非主键字段完全依赖于整个主键,而非主键的一部分。
第三范式
在第二范式的基础上,进一步要求非主键字段之间不存在传递依赖关系,确保数据结构清晰和简洁。
遵循这些范式可以帮助减少数据冗余,避免数据更新异常,并优化数据库性能。实际开发,特定需求,可以放宽范式的要求。
1.4 什么是事务
事务是逻辑上的一组操作,要么都执行,要么都不执行。
事务最经典的例子转账:小明要给小红转账100元,转账过程涉及到两个关键操作就是:小明的余额减少100元,将小红的余额增加100元。如果这两个操作之间突然出现错误,比如银行系统崩溃,导致小明余额减少而小红的余额没有增加,这样就不对了。事务就是保证这两个关键操作要么都成功,要么都要失败。
1.5 事务的ACID
原子性(Atomicity):事务被视为一个不可分割的工作单位,一个事务中的所有操作要么全部成功提交,要么全部回滚撤销,不存在部分成功部分失败的情况。
一致性(Consistency):事务开始之前和事务结束以后,数据库的完整性约束没有被破坏 。比如A向B转账,不可能A扣了钱,B却没收到。
隔离性(Isolation):同一时间,只允许一个事务请求同一数据,不同的事务之间彼此没有任何干扰。比如A正在从一张银行卡中取钱,在A取钱的过程结束前,B不能向这张卡转账
持久性(Durability):事务处理结束后,对数据的修改就是永久的,即便数据库宕机, 恢复后会读取redo log中的数据恢复数据.
1.6 事务隔离级别
事务的隔离级别分为:读未提交、读已提交、可重复读、串行化。
读未提交: 一个事务可以读取另一个事务未提交的数据;并发操作时会导致脏读,不可重复读、幻读。
读已提交: 只允许读取事务已经提交的数据,解决脏读问题,并发操作会导致幻读或不可重复读。
可重复读(MySQL的默认隔离级别): 用行级锁来保证一个事务在相同查询条件下两次查询结果一致 , 可以避免脏读, 不可重复读, 但无法避免幻读。
可串行化: 最高的隔离级别,用表级锁来保证所有事务串行化,该级别可以防止所有异常情况。但是该级别下,会导致大量的操作超时和锁竞争,从而大大降低数据库的性能,一般不使用这样事务隔离级别。
1.7 事务的并发问题及解决方案
1.7.1 脏读(读取未提交数据)
A事务读取B事务尚未提交的数据,如果B事务发生错误并执行回滚操作,那么A事务读取到的数据就是脏数据。
解决方案
使用数据库锁机制:在读取数据之前,先将该行数据加锁,直到事务提交或者回滚,其他事务才能对该行数据进行修改。
设置合适的事务隔离级别:将事务的隔离级别设置为读已提交, 事务就不会访问到脏数据。
使用乐观锁机制:在读取数据时,先获取该行数据的版本号,之后在更新数据时,先检查当前版本号是否与读取时的版本号一致,如果一致,则说明该行数据未被其他事务修改,可以进行更新操作,否则需要重新读取数据并进行相应操作。
1.7.2 不可重复读(前后多次读取,数据内容不一致)
事务A多次读取同一数据,事务B在事务A多次读取的过程中,对数据作了修改并提交,导致事务A多次读取同一数据时,结果不一致。
解决方案
使用锁机制:通过在读取数据时加上锁,可以保证在同一事务中多次读取同一数据时,读取的结果是一致的。常用的锁包括共享锁和排他锁。
设置合适的事务隔离级别:使用可重复读和串行化事务隔离级别可以解决数据库不可重复读的问题。
1.7.3 幻读(前后多次读取,数据总量不一致)
事务A在执行读取操作,需要两次统计数据的总量,前一次查询数据总量后,此时事务B执行了新增数据的操作并提交后,事务A再次读取时数据总量发生了变化。
解决方案
使用表级锁:在读取数据时加上表级锁,防止其他事务在该表中新增或删除数据。但这种方法可能会降低并发性能。
使用多版本并发控制(MVCC):这是许多数据库系统(如PostgreSQL和MySQL的InnoDB存储引擎)中用于解决幻读问题的方法。MVCC允许每个事务看到数据的一个一致的快照,从而避免了幻读问题。
设置合适的事务隔离级别:可重复读和串行化隔离级别也可以帮助解决幻读问题。
1.8 数据库锁机制划分
按操作划分
- DML锁:用于保护数据的完整性,如行级锁和表级锁。
- DDL锁:用于保护数据库对象的结构,如索引等的结构定义。
按锁的粒度划分
- 表级锁:锁定整个表,实现逻辑简单,系统负面影响小,但并发度较低。
- 行级锁:只锁定当前操作的行,粒度最小,冲突概率最低,但开销较大。
- 页级锁:介于表级锁和行级锁之间,锁定粒度适中。
按锁级别划分
- 共享锁:也称为读锁,允许多个事务同时读取同一资源,但不允许写操作。
- 排他锁:也称为写锁,阻止其他事务对资源的读写操作。
按加锁方式划分
- 自动锁:由数据库管理系统自动添加和释放的锁。
- 显示锁:需要用户或应用程序明确请求和释放的锁。
按使用方式划分
- 乐观锁:认为冲突不太可能发生,因此不会立即锁定资源,而是在数据提交更新时检查是否有冲突。
- 悲观锁:认为冲突很可能会发生,因此会立即锁定资源以防止其他事务的并发访问。
1.9 MySQL怎么保证原子性
MySQL通过以下方式来保证事务的原子性:
事务日志(transaction log):MySQL使用事务日志来记录所有的事务操作。在事务提交之前,所有的修改操作都先被写入日志中,而不是直接写入磁盘上的数据文件。这意味着,如果在事务执行过程中发生故障,MySQL可以通过回滚日志来撤销事务中的操作,使数据回滚到事务开始前的状态。
回滚日志(undo log):Undo Log记录了事务执行前的旧数据信息。当事务执行过程中出现错误,或者用户执行ROLLBACK语句进行事务回滚时,可以利用Undo Log中的信息将数据库恢复到事务开始前的状态。
锁机制:MySQL使用锁来控制对数据的并发访问,保证事务的隔离性和原子性。在事务执行期间,MySQL会根据事务的隔离级别对涉及的数据进行加锁,防止其他事务对数据进行修改。当事务成功提交或回滚后,MySQL会释放相应的锁,确保事务的原子性。
2.0 MySQL索引分类
普通索引:允许在定义索引的列中插入重复值和空值。普通索引是最基本的索引类型,没有任何限制。
唯一索引:与前面的普通索引类似,但不同的地方是:唯一索引列的所有值都只能出现一次,是唯一的。它允许有空值,但如果有多个空值,则这些空值是不重复的。
主键索引:它是一种特殊的唯一索引,一个表只能有一个主键,不允许有空值。主键索引的创建与表定义的一部分一起创建(CREATE TABLE 语句指定主键)。
全文索引:全文索引用来查找文本中的关键字,只能在MyISAM和InnoDB存储引擎上使用。对于大量的文本数据,全文索引能带来很大的性能提升。
组合索引(或称为复合索引):指在多个字段上创建的索引。创建组合索引时,列值的组合必须唯一。只有在查询条件中使用了创建索引时的第一个字段,索引才会被使用。
2.1 MySQL索引的好处和坏处
好处
- 提高查询速度
- 保证数据的唯一性
- 加速表与表之间的连接
- 降低分组和排序的时间
- 降低IO和CPU的消耗
坏处
- 占用磁盘空间
- 增加插入、删除和更新的时间
- 维护成本
2.2 索引的使用
不一定创建索引就会使用:虽然创建了索引,但数据库优化器在决定是否使用索引时会考虑多种因素,如查询条件、数据量、索引类型等。所以,即使创建了索引,也不一定会被使用。
组合索引的使用:在使用组合索引时,需要遵循“最左前缀”原则,即查询条件中必须包含组合索引的最左边的字段,否则索引可能不会被使用。
八、Redis
1.0 为什么Redis很快
原理
Redis是一种高性能的,开源的,C语言编写的非关系型数据库,可以对关系型数据库起到补充作用,同时支持持久化,可以将数据同步保存到磁盘。
Redis速度快的几个重要原因:
-
数据结构简单,对数据操作也简单。
-
完全基于内存。
-
使用多路IO复用模型,充分利用CPU资源。
-
单线程。
1.0.1 Redis使用单线程的好处
- 代码更清晰,处理逻辑更简单。
- 不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为锁而导致的性能消耗。
- 能够避免多线程带来的线程切换和上下文切换的开销,充分利用CPU资源。
1.1 Redis的数据存储结构、使用场景
Redis使用key-value结构来存储数据,其中key为字符串类型,而value支持多种数据类型。
数据存储结构
- String(字符串):最基础的数据结构,可以存储字符串、整数或浮点数。
- Hash(哈希):键值对的无序散列集合
- List(列表):有序的字符串列表,可以在列表的两端进行插入、删除和查找操作。
- Set(集合):无序且唯一的字符串集合,支持集合的交集、并集和差集等操作。
- Sorted Set(有序集合):有序的字符串集合,每个成员都关联着一个分数,可以根据分数进行排序和范围查询。
使用场景
- String类型的使用场景
1.1. 缓存:作为缓存对象来加速Web应用的访问,存储数据库中的热点数据,减少对数据库的访问压力。
1.2 计数器:例如,用于统计网站的访问量、文章的阅读量等。
1.3 防攻击:记录请求频率,用于防止恶意请求和攻击。
1.4 验证码、登录过期:存储验证码信息,或者用于记录用户的登录状态,确保登录过期的准确性。 - List类型的使用场景:
2.1 队列:实现任务队列、消息队列等,用于异步处理任务或消息。
2.2 秒杀:在电商平台的秒杀活动中,利用Redis的List结构实现用户的排队和出队操作,确保活动的公平性和性能。 - Set类型的使用场景:
3.1去重:在需要去除重复元素的场景中,如用户提交的数据、日志信息等。
3.2 共同好友:通过交集运算,快速找到两个用户的共同好友。
1.1.1 Redis在项目中使用场景
共享session
在分布式系统下,服务会部署在不同的tomcat,因此多个tomcat的session无法共享,以前存储在session中的数据无法实现共享,可以用redis代替session,解决分布式系统间数据共享问题。
数据缓存
Redis采用内存存储,读写效率较高。我们可以把数据库的访问频率高的热点数据存储到redis中,这样用户请求时优先从redis中读取,减少数据库压力,提高并发能力。
异步队列
Reids在内存存储引擎领域的一大优点是提供 list 和 set 操作,这使得Redis能作为一个很好的消息队列平台来使用。而且Redis中还有pub/sub这样的专用结构,用于1对N的消息通信模式。
分布式锁
Redis中的乐观锁机制,可以帮助我们实现分布式锁的效果,用于解决分布式系统下的多线程安全问题
1.2 Redis每种存储结构的简单命令
1.2.1 String
- set key value 设置指定key的值key相同的情况下,后设置的值覆盖前设置的值。
- get key 获取指定key的值,key不存在返回nil。
- setex key seconds value 设置指定key的值,并同时设置key的过期时间(单位秒),后设置的值覆盖前设置的值。
- setnx key value 只有在key不存在时设置key的值,返回1表示设置成功。key存在,返回0表示设置失败。
1.2.2 hash
-
hset key field value将哈希表key中的字段field的值设为value
-
hget key field 获取存储在哈希表中指定字段的值
-
hdel key field 删除存储在哈希表中的指定字段
-
hkeys key 获取哈希表中所有字段
-
hvals key 获取哈希表中所有值
-
hgetall key 获取在哈希表中指定key的所有字段和值
//设置值
hset 001 name zhangsan ,hset 001 age 20,hset 001 city beijing
//获取存储在哈希表中name的value值
hget 001 name 结果zhangsan
//删除age字段
hdel 001 age
//获取哈希表中所有字段
hkeys 001 结果 name city
//获取哈希表中所有值
hvals 001 结果zhangsan beijing
//获取在哈希表中指定key的所有字段和值
hgetall 001 结果 name zhangsan city beijing
1.2.3 List
-
lpush key value1 [value2] 将一个或多个值插入到列表头部
-
lrange key start stop 获取列表指定范围内的元素
-
rpop key 移除并获取列表最后一个元素
-
llen key 获取列表长度
-
brpop key1 [key2 ] timeout 移出并获取列表的最后一个元素,如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止
//在列表中插入多个值
lpush list a b c d
//获取列表中的元素,从0开始到结束。
lrange list 0 -1 结果输出 d c b a
//移除并获取列表最后一个元素
rpop list 结果为a
//获取列表长度
llen list 结果为3
//移出并获取列表的最后一个元素
brpop list 10 结果为b
brpop list 10 结果为c
brpop list 10 结果为d
brpop list 10 列表中没有元素,阻塞等待10s后结束
1.2.4 Set
-
sadd key member1 [member 2] 向集合添加一个或多个成员
-
smembers key 返回集合中的所有成员
-
scard key 获取集合的成员数
-
sinter key1 [key2] 返回给定所有集合的交集
-
sunion key1 [key2] 返回所有给定集合的并集
-
sdiff key1 [key2] 返回给定所有集合的差集
-
srem key member1 [member2] 移除集合中一个或多个成员
向集合添加多个成员
sadd set1 a b c d m n
sadd set2 a b c d x y z
//得到set1集合所有成员
smembers set1 结果 b d a c m n
//获取set1集合的成员数
scard set1 结果为6
//返回给定集合的交集
sinter set1 set2 结果为a b c d
//返回给定集合的并集
sunion set1 set2 结果为a b c d m n x y z
//返回存在于set1中但不存在于set2中的成员
sdiff set1 set2 结果为 m n
//移除set1集合中的m n成员
srem set1 m n
1.2.5 sorted set
-
zadd key score1 member1 [score2 member2] 向有序集合添加一一个或多个成员,或者更新已存在成员的分数
-
zrange key start stop [WITHSCORES] 通过索引区间返回有序集合中指定区间内的成员
-
zincrby key increment member 有序集合中对指定成员的分数加上增量increment
-
zrem key member [member] 移除有序集合中的一个或多个成员
//设置成员变量分数
zadd zset 10.0 a 9.0 c 6.0 d 8.0 e
//设置a的分数
zadd zset 12.0 a
//返回指定范围的成员
zrange zset 0 -1 结果为 d e c a
//给zset集合中的e元素的分数加一
zincrby zset 1.0 e
//移除zset集合中的多个成员
zrem zset e c
1.2.6通用命令
-
keys pattern 查找所有符合给定模式( pattern)的key
-
exists key 检查给定key是否存在,key存在,则返回1;否则返回0
-
type key 返回key所储存的值的类型
-
ttl key 返回给定key的剩余生存时间(TTL, time to live),以秒为单位
-
del key 删除一个或多个key,key存在,则删除该key,并返回被删除的key的数量;如果key不存在,则忽略该key,并继续删除其他key(如果有)。
1.3 怎么防止Redis宕机数据丢失问题
Redis的持久化是一种防止数据丢失的重要机制,通过把内存中的数据保存到磁盘上,即使Redis发生宕机,重启后也可以从磁盘加载数据,恢复之前的状态。
1.4 Redis持久化方式
RDB(Redis DataBase)
Redis的默认持久化方式。它按照指定的时间间隔,将内存中的整个数据集生成一份快照,并保存为二进制文件。当Redis重启时,通过重新加载这个快照文件来恢复数据。
优点:速度快,适合用于定期备份。
缺点:可能会丢失两次快照之间的数据。
AOF(Append Only File)
通过记录Redis服务器执行的写命令来持久化数据,所有写命令都会被追加到AOF文件的末尾。当Redis重启时,通过重新执行AOF文件中的命令来恢复数据。
优点:数据安全性更高,因为所有写命令都被保存。
缺点:文件可能较大,性能可能稍差于RDB,且恢复时需要重新执行所有命令。
根据实际需要来选择,通常二者可以结合来使用。
Redis 提供了两种数据持久化的方式,一种是 RDB,另一种是 AOF。默认情况下,Redis 使用的是 RDB 持久化。
RDB持久化文件体积较小,但是保存数据的频率一般较低,可靠性差,容易丢失数据。另外RDB写数据时会采用Fork函数拷贝主进程,可能有额外的内存消耗,文件压缩也会有额外的CPU消耗。
AOF持久化可以做到每秒钟持久化一次,可靠性高。但是持久化文件体积较大,导致数据恢复时读取文件时间较长,效率略低。
1.5 Redis内存不够怎么解决
方式一:增加物理内存
方式二:使用淘汰策略,删掉一些老旧数据
方式三:集群
1.6 Redis的Key过期策略
1.6.1 为什么需要内存回收
-
在Redis中,set指令可以指定key的过期时间,当过期时间到达以后,key就失效了;
-
Redis是基于内存操作的,所有的数据都是保存在内存中,一台机器的内存是有限的。
基于以上两点,为了保证Redis能继续提供可靠的服务,Redis需要一种机制清理掉不常用的、无效的、多余的数据,失效后的数据需要及时清理,这就需要内存回收了。
Redis的内存回收主要分为过期删除策略和内存淘汰策略两部分。
1.6.2 过期删除策略
定时删除
对于每一个设置了过期时间的key都会创建一个定时器,一旦到达过期时间就立即删除。该策略可以立即清除过期的数据,对内存较友好,但是缺点是占用了大量的CPU资源去处理过期的数据,会影响Redis的吞吐量和响应时间。
惰性删除
当访问一个key时,才判断该key是否过期,过期则删除。该策略能最大限度地节省CPU资源,但是对内存却十分不友好。有一种极端的情况是可能出现大量的过期key没有被再次访问,因此不会被清除,导致占用了大量的内存。
定期删除
每隔一段时间,扫描Redis中过期key字典,并清除部分过期的key。该策略是前两者的一个折中方案,还可以通过调整定时扫描的时间间隔和每次扫描的限定耗时,在不同情况下使得CPU和内存资源达到最优的平衡效果。
1.6.3 内存淘汰策略
Redis的内存淘汰策略,是指内存达到maxmemory极限时,使用某种算法来决定清理掉哪些数据,以保证新数据的存入。
内存淘汰机制包括:
volatile-lru : 在设置了过期时间的在键空间中,移除最近最少使用的 key。
volatile-lfu: 在设置了过期时间的在键空间中,移除使用频率最低的key。
volatile-ttl:在设置了过期时间的键空间中,优先移除有更早过期时间的 key 。
volatile-random:在设置了过期时间的键空间中,随机移除某个 key。
allkeys-lru:在键空间中, 移除最近最少使用的key。
allkeys-lfu:在键空间中,移除使用频率最低的key。
allkeys-random:在键空间中,随机移除某个key。
注意:在配置文件中,通过maxmemory-policy可以配置要使用哪一个淘汰机制。
总结
Redis过期策略包含定时删除、惰性删除和定期删除两部分。
定时删除是设置了过期时间的key都会创建一个定时器,一旦到达过期时间就立即删除。
惰性删除是当用户查询某个Key时,会检查这个Key是否已经过期,过期则删除。
定期删除是Redis内部有一个定时任务,会清除部分过期的key。
但是这些策略都无法保证过期key一定删除,还可能导致内存溢出。
Redis的内存淘汰策略,是指内存达到maxmemory极限时,使用某种算法来决定清理掉哪些数据,以保证新数据的存入。
1.7 Redis事务和MySQL事务的区别
实现机制
MySQL:基于日志系统,确保数据操作的持久性和原子性。
Redis:基于命令队列,执行一系列预定义的命令。
原子性
MySQL:具有强原子性,操作失败会回滚。
Redis:弱原子性,命令失败不会回滚其他命令。
数据持久性与安全性
MySQL:数据持久性强,适用于关键业务数据。
Redis:主要用于内存操作,持久化主要用于故障恢复。
使用场景
Redis:适合缓存、实时计数等高速读写场景。
MySQL:适合存储结构化数据,支持复杂查询和事务处理。
1.8 为什么会有缓存和数据一致性的问题
对于热点数据,我们可以放入redis缓存中,因为我们使用Mysql的话,DB是扛不住的。因此采用缓存中间件来增加查询效率,但需要保证Redis中读取数据与数据库存储数据是一致的。
1.9 怎么保证MySQL和Redis缓存数据一致性
先更新数据库,再更新缓存
优点:
确保数据的一致性,因为缓存是直接从数据库中获取的。
减轻数据库的压力,因为只有在数据发生变化时才会更新数据库。
缺点:
如果数据没有发生变化,仍然会执行数据库更新操作,浪费资源。
需要额外的代码来处理缓存更新和数据库更新的顺序。
先更新数据库,再删除缓存
其可能执行的流程顺序为:
客户端1 触发更新数据A的逻辑
客户端2 触发查询数据A的逻辑
客户端3 触发查询数据A的逻辑
客户端1 更新数据库中数据A
客户端2 查询缓存中数据A,命中返回(旧数据)
客户端1 让缓存中数据A失效
客户端3 查询缓存中数据A,未命中
客户端3 查询数据库中数据A,并更新到缓存中。
最终,缓存和数据库的数据是一致的,但是会有一些线程读到旧的数据
先更新缓存,再更新数据库
优点:
如果数据发生变化,可以立即在缓存中反映出来,提高用户体验。
减少对数据库的压力,因为只有在数据发生变化时才会更新数据库。
缺点:
如果缓存和数据库之间的同步出现问题,可能导致数据不一致。
需要额外的代码来处理缓存更新和数据库更新的顺序。
先删除缓存,再更新数据库
这种方法在并发下最容易出现长时间的脏数据,不可取
其可能的执行流程顺序为:
客户端1 触发更新数据A的逻辑
客户端2 触发查询数据A的逻辑
客户端1 删除缓存中数据A
客户端2 查询缓存中数据A,未命中
客户端2 从数据库查询数据A,并更新到缓存中(旧数据)
客户端1 更新数据库中数据A。
最后缓存中的数据 A 跟数据库中的数据 A 是不一致的,缓存中的数据A是旧的脏数据。
延时双删
上面我们提到,如果是先删缓存、再更新数据库,在没有出现失败时可能会导致数据的不一致。解决办法,那就是采用延时双删的策略,延时双删的基本思路如下:
删除缓存;
更新数据库;
休眠一会(比如1秒);
再次删除缓存。
休眠时间 = 读业务逻辑数据的耗时 + 几百毫秒。为了确保读请求结束,写请求可以删除读请求可能带来的缓存脏数据。
读取binlog异步删除缓存
使用Canal框架,伪装成MySQL的salve节点,监听MySQL的binlog日志变化,发送到MQ队列里面,然后通过ACK机制确认处理这条更新消息,删除缓存,保证数据缓存一致性.
1.9 SpringCache常用注解
@EnableCaching:放在主启动类上,启用Spring Cache支持,使得后续的缓存注解生效。
@Cacheable:标注方法结果可缓存。方法首次调用结果存入缓存,相同参数后续调用直接从缓存中获取结果。
打在类上,表示类中所有符合缓存条件的方法都会开启缓存。
@CacheEvict:标注方法或类,用于清除缓存
@CachePut:标注方法每次都会被调用,更新缓存。
@Caching:在方法上组合多个缓存操作,如查询、更新和删除。
@CacheConfig:类级别注解,共享缓存配置。
2.0 缓存穿透-解决方案
缓存穿透:请求某个Key对应的数据时,在缓存中没有命中数据,在数据库中也没有命中数据,数据库会返回空值,而Redis也不会缓存这个空值,这就造成了缓存穿透的问题。
解决方案:
缓存空值:当第一次从数据库中查询出来的结果为空时,我们就把当前key设置空值加载到缓存,并设置合理的过期时间,这样,下次用户过来访问这个不存在的数据,那么在redis中也能找到这个数据就不会在访问数据库了。
- 优点:实现简单,维护方便
- 缺点:
- 额外的内存消耗
- 可能造成短期的不一致:如果数据库中的这个空对象在之后被插入了数据,而缓存中的空对象还没有过期,那么就会造成数据不一致。不过,这种情况通常可以通过设置较短的过期时间或者利用缓存失效策略来降低影响。
- 设置null值可能被恶意针对,攻击者使用大量不存在的不重复key ,那么就会缓存大量不存在key数据。我们还可以对Key规定格式模板,然后对不存在的key做正则规范匹配,如果完全不符合就不用存null值到redis,而是直接返回错误。
使用布隆过滤器:将所有可能存在的数据哈希到一个足够大的bitmap中,BloomFilter通过判断key是否存在,key存在,则放行,请求到redis中查找数据,未找到,则在数据库中查询数据后,再将其放入到redis中,如果key不存在,则直接返回。
- 优点:内存占用较少,没有多余key
- 缺点:
- 实现复杂
- 存在误判可能:布隆过滤器在判断key是否存在时,可能会因为哈希冲突而产生误判,即某些不存在的key也可能被判断为存在。但是,布隆过滤器不会漏报,即如果布隆过滤器判断某个key不存在,那么这个key在数据库中一定不存在。因此,误判只会造成额外的数据库查询,但不会影响结果的正确性。
2.1 缓存击穿-解决方案
缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。。
常见的解决方案有两种:
互斥锁
在缓存失效的时候,不是立即去加载数据,而是先使用Redis的setnx去set一个互斥key,当操作返回成功时,再进行加载数据的操作并重新写入缓存;否则,就重试整个get缓存的方法。
逻辑过期
不使用redis提供的过期时间,在将数据缓存到Redis时,除了存储实际的数据值外,还添加一个字段来标识数据的逻辑过期时间。
实现流程:假设线程1去查询缓存,检查数据的逻辑过期时间,如果过期了,此时线程1去获得互斥锁,那么其他线程会进行阻塞,获得了锁的线程开启一个异步线程2去进行以前的重构数据的逻辑,线程2完成这个逻辑后,才释放锁, 而线程1直接进行返回过期数据,假设现在线程3过来访问,由于线程线程2持有着锁,所以线程3无法获得锁,线程3也直接返回过期数据,线程2释放锁后,其他线程才能返回正确的数据。
这种方案巧妙在于,异步的构建缓存,缺点在于在构建完缓存之前,返回的都是脏数据。
进行对比
**互斥锁方案:**由于保证了互斥性,所以数据一致,且实现简单,因为仅仅只需要加一把锁而已,也没其他的事情需要操心,所以没有额外的内存消耗,缺点在于有锁就有死锁问题的发生,且只能串行执行性能肯定受到影响。
逻辑过期方案: 线程读取过程中不需要等待,性能好,有一个额外的线程持有锁去进行重构数据,但是在重构数据完成前,其他的线程只能返回之前的数据,且实现起来麻烦。
2.2 缓存雪崩-解决方案
缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力
解决方案:
- 给不同的Key的过期时间添加随机值
- 避免redis节点宕机引起雪崩,搭建主从集群,保证高可用
- 给缓存业务添加降级限流策略
- 给业务添加多级缓存
2.3 Redis的主从的优点缺点
优点
读写分离:主从复制允许主节点处理写操作,从节点处理读操作。
数据备份:数据可以从主节点复制到从节点,实现数据的热备份。
负载均衡:在主从复制的基础上,配合读写分离,可以分担服务器的负载。
高可用基础:主从复制是哨兵模式和集群能够实施的基础。
缺点
不能分担写的压力:主从复制允许主节点处理写操作。
主的单点故障:主节点出现故障,需要手动或从其他机制(如哨兵模式)进行故障转移,这可能会导致一定的停机时间。
数据同步延迟:主从复制是异步的,数据同步延迟。
网络带宽消耗:将数据同步到从节点,可能会占用较大的网络带宽。
2.4 解释一下Redis的哨兵模式优点及不足
Redis的哨兵模式(Sentinel)确实是一种用于监控Redis主从服务器的高可用性解决方案。在哨兵模式中,哨兵进程会周期性地发送PING命令来检测主节点和从节点的状态。一旦发现主节点出现故障,哨兵进程会根据投票机制自动选择一个从节点作为新的主节点,并通知其他从节点切换到新的主节点上,从而实现故障转移。
优点
故障自动转移:当主节点出现故障时,哨兵可以自动选择一个从节点作为新的主节点,无需人工干预。
集群监控:哨兵可以监控主从集群中的Master和Slave进程是否正常工作,并及时发现和处理故障。
可扩展性:通过加入更多哨兵节点,可以轻松地扩展Redis集群的容量和性能。
缺点
写压力问题:哨兵模式并没有解决Redis主从模式的写压力问题。所有的写操作仍然需要在主节点上执行,因此如果写操作非常频繁或数据量很大,主节点可能会成为性能瓶颈。
存储扩容问题:哨兵模式本身并不能实现存储的扩容。虽然可以通过增加从节点来分担读压力,但总的存储容量仍然受限于主节点的内存大小。如果需要更大的存储容量,可能需要考虑使用Redis集群或其他分布式存储解决方案。
配置复杂性:哨兵模式的配置相对复杂,需要正确配置主从节点以及哨兵节点的数量和参数。
延迟问题:由于哨兵需要进行频繁的状态检查和转移操作,可能会对系统带来一定的延迟。
2.5 Redis如何模拟队列和栈
模拟队列
在Redis中,List的左进右出(LPUSH + RPOP 或 RPUSH + LPOP)行为可以用来模拟队列。队列是一种先进先出(FIFO)的数据结构。
- 使用LPUSH key value [value …]命令将一个或多个值插入到列表的左侧(头部)。
- 使用RPOP key命令移除并获取列表的最后一个元素(尾部),如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。
模拟栈
在Redis中,List的同一边进同一边出(LPUSH + LPOP 或 RPUSH + RPOP)行为可以用来模拟栈。栈是一种后进先出(LIFO)的数据结构。
使用LPUSH key value [value …]命令将一个或多个值插入到列表的左侧(头部)。
使用LPOP key命令移除并获取列表的第一个元素(头部)。
2.6 Redis存储单个对象怎么存,存储对象集合怎么存
存储单个对象
- 使用String:如果对象可以被序列化为一个字符串,那么直接使用Redis的String类型来存储。这种方法简单直接,但需要注意序列化和反序列化的性能开销。
- 使用Hash:Redis的Hash类型非常适合存储对象,因为它允许你以键值对的形式存储数据。你可以将对象的属性作为Hash的field,属性值作为Hash的value。
存储对象集合
- 使用Set:Redis的Set类型可以用来存储不重复的对象集合。但是,这里的对象通常指的是对象的唯一标识符(如ID),而不是整个对象本身。你可以将对象的ID存储在一个Set中,以便进行快速的去重和成员检查操作。
- 使用Hash的集合:使用多个Hash来模拟一个对象集合,每个Hash存储一个对象。但是,这种方法不如使用Set或List(对于有序集合)来存储对象的ID更加高效和直观。
- 使用List:可以将对象的ID(或整个对象,如果它们可以被序列化为字符串)按照特定的顺序存储在一个List中。
2.7 Redis的集群方式
Redis集群可以分为主从集群和分片集群两类。
主从集群一般一主多从,主库用来写数据,从库用来读数据。结合哨兵,可以再主库宕机时从新选主,目的是保证Redis的高可用。
分片集群是数据分片,我们会让多个Redis节点组成集群,并将16383个插槽分到不同的节点上。存储数据时利用对key做hash运算,得到插槽值后存储到对应的节点即可。因为存储数据面向的是插槽而非节点本身,因此可以做到集群动态伸缩。目的是让Redis能存储更多数据。
2.8 Redis实现分布式锁
分布式锁要满足的条件
- 多进程互斥:同一时刻,只有一个进程可以获取锁
- 保证锁可以释放:任务结束或出现异常,锁一定要释放,避免死锁
- 阻塞锁(可选):获取锁失败时可否重试
- 重入锁(可选):获取锁的代码递归调用时,依然可以获取锁
最基本的分布式锁
利用Redis的setnx命令,实现互斥
的效果。为了保证服务宕机时也可以释放锁,需要利用expire命令给锁设置一个有效期。
1.如果expire之前服务宕机怎么办?
要保证setnx和expire命令的原子性,redis的set命令可以满足:
set key value [NX] [EX time]
需要添加nx和ex的选项:
- NX:与setnx一致,第一次执行成功
- EX:设置过期时间
2.释放锁的时候,如果自己的锁已经过期了,怎么办?
锁中存储当前进程和线程标识,释放锁时对锁的标识判断,如果是自己的则删除,不是则放弃操作。
但是这两步操作要保证原子性,需要通过Lua脚本来实现。
if redis.call("get",KEYS[1]) == ARGV[1] then
redis.call("del",KEYS[1])
end
可重入分布式锁
如果有重入的需求,则除了在锁中记录进程标识,还要记录重试次数,流程如下:
下面我们假设锁的key为“lock
”,hashKey是当前线程的id:“threadId
”,锁自动释放时间假设为20
获取锁的步骤:
- 1.判断lock是否存在
EXISTS lock
- 存在,说明有人获取锁了,下面判断是不是自己的锁
- 判断当前线程id作为hashKey是否存在:
HEXISTS lock threadId
- 不存在,说明锁已经有了,且不是自己获取的,锁获取失败,end
- 存在,说明是自己获取的锁,重入次数+1:
HINCRBY lock threadId 1
,去到步骤3
- 判断当前线程id作为hashKey是否存在:
- 2.不存在,说明可以获取锁,
HSET key threadId 1
- 3.设置锁自动释放时间,
EXPIRE lock 20
- 存在,说明有人获取锁了,下面判断是不是自己的锁
释放锁的步骤:
- 1.判断当前线程id作为hashKey是否存在:
HEXISTS lock threadId
- 不存在,说明锁已经失效,不用管了
- 存在,说明锁还在,重入次数减1:
HINCRBY lock threadId -1
,获取新的重入次数
- 2.判断重入次数是否为0:
- 为0,说明锁全部释放,删除key:
DEL lock
- 大于0,说明锁还在使用,重置有效时间:
EXPIRE lock 20
- 为0,说明锁全部释放,删除key:
对应的Lua脚本如下:
首先是获取锁:
local key = KEYS[1]; -- 锁的key
local threadId = ARGV[1]; -- 线程唯一标识
local releaseTime = ARGV[2]; -- 锁的自动释放时间
if(redis.call('exists', key) == 0) then -- 判断是否存在
redis.call('hset', key, threadId, '1'); -- 不存在, 获取锁
redis.call('expire', key, releaseTime); -- 设置有效期
return 1; -- 返回结果
end;
if(redis.call('hexists', key, threadId) == 1) then -- 锁已经存在,判断threadId是否是自己
redis.call('hincrby', key, threadId, '1'); -- 不存在, 获取锁,重入次数+1
redis.call('expire', key, releaseTime); -- 设置有效期
return 1; -- 返回结果
end;
return 0; -- 代码走到这里,说明获取锁的不是自己,获取锁失败
然后是释放锁:
local key = KEYS[1]; -- 锁的key
local threadId = ARGV[1]; -- 线程唯一标识
local releaseTime = ARGV[2]; -- 锁的自动释放时间
if (redis.call('HEXISTS', key, threadId) == 0) then -- 判断当前锁是否还是被自己持有
return nil; -- 如果已经不是自己,则直接返回
end;
local count = redis.call('HINCRBY', key, threadId, -1); -- 是自己的锁,则重入次数-1
if (count > 0) then -- 判断是否重入次数是否已经为0
redis.call('EXPIRE', key, releaseTime); -- 大于0说明不能释放锁,重置有效期然后返回
return nil;
else
redis.call('DEL', key); -- 等于0说明可以释放锁,直接删除
return nil;
end;
高可用的锁
九、RabbitMQ
1.0 为什么选择了RabbitMQ而不是其它的MQ
kafka是以吞吐量高而闻名,不过其数据稳定性一般,而且无法保证消息有序性。
阿里巴巴的RocketMQ基于Kafka的原理,弥补了Kafka的缺点,继承了其高吞吐的优势,其客户端目前以Java为主。但是我们担心阿里巴巴开源产品的稳定性,所以就没有使用。
RabbitMQ基于面向并发的语言Erlang开发,吞吐量不如Kafka,但是消息可靠性较好,并且消息延迟极低,集群搭建比较方便。支持多种协议,并且有各种语言的客户端,比较灵活。Spring对RabbitMQ的支持也比较好,使用起来比较方便,比较符合我们公司的需求。
综合考虑并发需求以及稳定性需求,我们选择了RabbitMQ。
1.1 RabbitMQ的使用场景
异步处理任务,提高程序的响应时间
将耗时任务放入RabbitMQ队列,应用程序可立即响应用户,提高响应速度。
提高稳定性
通过消息确认机制,即使消费者处理失败,RabbitMQ也能确保消息不丢失,提高系统可靠性。
服务解耦
RabbitMQ作为中间件,使生产者和消费者独立工作,降低系统组件间的耦合度,增强系统灵活性。
流量削峰
高并发请求通过RabbitMQ队列异步处理,避免系统拥堵,同时支持消息持久化,保障系统稳定性。
延迟队列
基于RabbitMQ的死信队列或者DelayExchange插件,可以实现消息发送后,延迟接收的效果。
1.2 RabbitMQ的交换机有哪几种
Fanout:广播,将消息交给所有绑定到交换机的队列
Direct:定向,把消息交给符合指定RoutingKey的队列
Topic:通配符,把消息交给匹配符合RoutingKey的队列
1.3 RabbitMQ工作流程
消息发送
- 生产者与RabbitMQ Broker建立TCP连接,并创建一个信道(Channel)。
- 生产者通过该信道将消息发送给Broker,并指定一个Exchange(交换机)。
- Exchange根据消息的Routing Key(路由键)将消息转发到指定的队列(Queue)中。
消息接收
- 消费者与RabbitMQ Broker建立TCP连接,并创建一个信道。
- 消费者通过该信道监听指定的队列。
- 当有消息到达队列时,Broker默认会将消息推送给消费者。
- 消费者接收并处理消息。
- 消费者向Broker发送确认消息(ack),告知消息已经被成功处理。
6 .Broker从队列中删除该消息。
1.4 RabbitMQ如何防止消息丢失
消息确认机制
RabbitMQ的消息确认机制默认是自动签收,也就是说消息一旦被消费者接收,就自动签收,消息就从队列里清除了。对于关键消息,为了确保消息被正确处理,可以使用手动确认模式。(使用手动确认模式确保关键消息被正确处理。如果消费者未确认消息并断开连接,RabbitMQ会重新将消息放入队列)
消息持久化
将消息、队列和交换机设置为持久化,以防止RabbitMQ重启或崩溃时消息丢失。
- 消息持久化:发送消息时设置delivery_mode为2。
- 队列持久化:声明队列时设置durable为true。
- 交换机持久化:声明交换机时设置durable为true。
集群和镜像队列
部署RabbitMQ集群并使用镜像队列提高可靠性。即使某个节点故障,其他节点上的队列仍然可用。
1.5 RabbitMQ如何避免消息堆积
提高消费者处理速度
尽可能优化业务代码,提高业务性能;接收到消息后,开启线程池,并发处理多个消息。
优点:成本低,改改代码即可。
缺点:开启线程池会带来额外的性能开销,对于高频、低时延的任务不合适。推荐任务执行周期较长的业务。
增加更多消费者
一个队列绑定多个消费者,共同争抢任务,提高消息处理速度。
增加队列消息存储上限
在RabbitMQ的1.8版本后,加入了新的队列模式:Lazy Queue
这种队列不会将消息保存在内存中,而是在收到消息后直接写入磁盘中,理论上没有存储上限。可以解决消息堆积问题。
优点:磁盘存储更安全;存储无上限;避免内存存储带来的Page Out问题,性能更稳定;
缺点:磁盘存储受到IO性能的限制,消息时效性不如内存模式,但影响不大。
1.6 RabbitMQ中如何防止消息重复消费
消息幂等性
生产者给每个消息分配一个唯一的ID,并在消息体中携带这个ID。消费者在处理消息之前,先根据这个消息ID去查询数据库或检查某个去重机制(如Redis Set)来判断该消息是否已经被处理过。如果消息已经被处理过,则直接丢弃该消息;否则,正常处理消息。
消息去重
使用外部存储系统(如Redis、Memcached等)来记录已经处理过的消息ID。当消费者接收到消息时,首先检查这个消息ID是否已经在外部存储系统中存在。如果存在,说明该消息已经被处理过,直接丢弃;否则,处理消息并在处理完成后将消息ID存储到外部系统中。
消息确认机制(ACK)
使用RabbitMQ的手动确认模式(manual ack),而不是默认的自动确认模式。消费者在处理完消息后,向RabbitMQ发送确认消息(ACK),告知RabbitMQ该消息已经被成功处理。如果RabbitMQ没有收到确认消息,并且消费者连接断开,RabbitMQ会将该消息重新放入队列中,等待其他消费者处理。
注意:确保在网络不稳定的情况下,消费者能够成功发送ACK,避免因为网络问题导致的消息重复消费。
事务性消费
在处理消息时使用事务性操作,确保消息只有在完全处理完成后才会被确认。这需要数据库支持事务操作,并且在处理消息时保持数据库连接。
分布式锁
在处理消息时,使用分布式锁来确保同一时间只有一个消费者能够处理某个特定的消息。
这可以防止多个消费者同时处理同一个消息,从而避免消息重复消费的问题。
1.7 RabbitMQ中怎么确认生产者发送的消息是否发送到交换机
Confirm Callback(确认回调)
当消息被RabbitMQ的Broker成功路由到一个或多个队列时,会发送一个确认(ack)给生产者。如果消息未能路由到任何队列,则会发送一个nack给生产者。生产者可以设置一个确认回调来处理这些ack和nack。
Returned Callback(返回回调)
如果消息被路由到队列,但随后由于某些原因(如队列已满、TTL过期等)被退回给生产者,生产者可以设置一个返回回调来处理这些被退回的消息。
1.8 如何保证RabbitMQ的高可用
做好交换机、队列、消息的持久化
搭建RabbitMQ的镜像集群,做好主从备份。当然也可以使用仲裁队列代替镜像集群。
十、ElasticSearch
1.0 ES的优势
基于Lucene的分布式搜索服务:ES是基于Lucene构建的,但它解决了原生Lucene使用的不足,如分布式搜索、实时搜索等。
实时文件存储和搜索:ES支持实时分析搜索,每个字段都可以被索引并搜索。
可扩展性:ES可以很容易地扩展到上百台服务器,处理PB级结构化或非结构化数据。
简单的API:通过RESTful API,ES可以与各种语言的客户端甚至命令行进行交互。
易用性:ES上手容易,只需少量学习即可在生产环境中使用。
1.1 ES为什么那么快
ES之所以快,主要是因为它基于Lucene的全文检索引擎,并采用了倒排索引这一关键数据结构。倒排索引与传统的数据库索引(如B树)不同,它是以词项为关键字,记录词项在文档中的出现情况(如文档ID、位置等)。这种结构非常适合于文本搜索,因为可以快速定位到包含特定词项的文档。
1.2 Lucene创建索引原理
分词处理
Lucene首先将文档内容切分成多个可索引的单元(如单词或词组)。对于英文,直接按空格分隔;对于中文,则使用中文分词算法进行分词。
过滤
分词后,Lucene会去除一些无意义的词汇(如停用词)和标点符号,以减少索引的大小并提高搜索效率。
词元处理
接着,Lucene会对剩下的词元进行标准化处理,比如将英文单词转为小写、进行词干提取(即去除时态、单复数等后缀)以及同义词替换等,以提高搜索的准确性和效率。
创建字典
Lucene将所有标准化后的词元收集起来,形成一个字典,并按照字母顺序排序。每个词元在字典中都有一个唯一的标识,并与一个或多个文档列表(倒排列表)关联。
生成倒排索引
基于字典和文档列表,Lucene构建倒排索引。倒排索引记录了哪些文档包含了哪些词元,以及词元在文档中的位置信息。这样,当用户搜索某个词时,Lucene可以快速定位到包含这个词的文档。
优化与压缩
为了减小索引文件的大小并提高搜索性能,Lucene会对生成的倒排索引进行优化和压缩。这通常包括合并小的索引文件、删除旧或不再需要的索引项等操作。
1.3 ES中的keyword和text类型区别
keyword:keyword类型的数据在索引时不会被分词,直接建立索引,支持模糊、精确、聚合查询。
text:text类型的数据在索引时会被分析器分词,建立索引,支持模糊、精确查询,不支持聚合查询。
keyword类型通常用于存储不需要分词的字段,如年龄、性别、邮编、邮箱地址、主机名、状态码、标签等。这些字段通常用于过滤、排序和聚合等操作。
text类型通常用于存储需要全文搜索的字段,如邮件内容、地址、博客文章内容等。
1.4 ES的节点类型以及作用
主节点
设置:node.master: true
作用:负责集群级别的操作,如创建或删除索引、跟踪集群中哪些节点是活动的、决定哪些分片应该分配给哪些数据节点等。不涉及数据文档的存储或检索。
**数据节点 **
设置:node.data: true
作用:存储索引数据,包括文档的增删改查操作以及聚合操作等。执行数据相关的CRUD请求。
客户端节点(负载均衡器)
设置:node.master: false 和 node.data: false
作用:不存储数据,也不成为主节点。主要负责路由请求,处理搜索和聚合请求,并将这些请求分发到数据节点。可以作为应用程序和集群之间的中间层,以减轻数据节点的负载。
1.5 ES的分层结构
Index:索引库,包含有一堆相似结构的文档数据,类比Mysql中的数据库。在ES 7.x及以后,一个Index只包含一个默认的Type(_doc)。
Type:类型,它是index中的一个逻辑数据分类,类比Mysql中的表
Document:文档:是ES中的最小数据单元,通常用json结构标识,类比Mysql中的一行数据
Field:字段:类比Mysql中的一个列
因此,可以说在7.x及之后的版本中,“库表合一”的概念更为准确,即一个Index对应一个默认的Type(_doc),而这个Type中包含了多个Document。
1.6 ES添加文档的过程
1.客户端请求协调节点
客户端将写入请求(如添加文档)发送到ES集群的任意节点。这个节点会充当协调节点(coordinating node)的角色,负责将请求转发到正确的分片(shard)。
2.选择主分片
协调节点根据文档的_id字段值以及一个固定的分片算法(通常是hash(document_id) % (num_of_primary_shards))来计算应该写入哪个主分片(primary shard)。
3.写入主分片
协调节点将请求转发到包含目标主分片的节点,主分片将文档索引到Lucene段中,并向协调节点发送写入成功的确认。
4.异步复制
ES异步地将文档复制到与该主分片关联的所有副本分片(replica shards)上。写入操作不会等待所有副本分片都完成复制。
5.返回结果
默认情况下,协调节点在收到主分片写入成功的确认后立即返回结果给客户端。客户端可以配置等待条件,如等待一定数量的副本分片也成功写入,但这会增加延迟。
6.故障恢复
如果主分片所在的节点发生故障,ES会自动选择一个副本分片来替代主分片,确保数据的高可用性和持久性。
1.7 ES更新文档的过程
1.发送更新请求
客户端向ES集群发送更新文档的请求,通常包含文档的索引、类型、_id和新的字段值。
2.请求处理
请求首先到达集群中的一个节点,该节点作为协调节点。协调节点根据文档的_id和路由信息确定哪个主分片处理该请求。
3.索引新版本
协调节点将请求转发到包含目标主分片的节点。主分片将创建一个新的文档版本,该版本包含更新后的字段值,并为其分配一个新的版本号(递增_version字段的值),并将其写入到一个新的Lucene段中。
4.同步副本
一旦主分片上的更新完成,该更新会被异步地复制到与该主分片关联的所有副本分片上。
5. 版本检查
如果客户端在更新请求中指定了_version参数,ES会检查提供的版本是否与当前文档的版本匹配。如果不匹配,更新操作会失败,并返回一个版本冲突错误。
6.返回结果
协调节点在收到主分片上的更新确认后,将结果返回给客户端。如果更新成功,响应通常会包含新的_version值。
1.8 ES删除文档的过程
1.发送删除请求
客户端向Elasticsearch集群发送删除文档的请求,指定文档的索引、类型和_id。
2.请求处理
请求首先到达集群中的一个节点,该节点作为协调节点。协调节点根据文档的_id确定应该删除哪个主分片上的文档。
3.删除标记
一旦协调节点确定了目标主分片,它就会向包含该分片的节点发送删除请求。
该节点上的主分片在内部的一个.del文件中将文档标记为删除,而不是立即从磁盘上删除它。标记为删除的文档仍然保留在Lucene段中,但在搜索结果中会被过滤掉。
4.合并和删除
随着时间的推移,ES会定期执行段的合并操作。在合并过程(异步)中,标记为删除的文档将不会被包含在新的段中,从而有效地从磁盘上删除它们。
5.返回结果
协调节点在收到主分片上的删除确认后,将结果返回给客户端。
1.9 怎么保持ES和MySQL的数据一致性
1.9.1 同步双写
最为简单的方式,在将数据写到MySQL时,同时将数据写到ES。
优点
- 业务逻辑简单
- 实时性高
缺点
- 硬编码,有需要写入MySQL的地方都需要添加写入ES的代码;
- 业务强耦合;
- 存在双写失败丢数据风险;
- 性能较差:本来MySQL的性能不是很高,再加一个ES,系统的性能必然会下降。
双写失败情况
- ES系统不可用;
- 程序和ES之间的网络故障;
- 程序重启,导致系统来不及写入ES等。
针对这种情况,有数据强一致性要求的,就必须双写放到事务中来处理,而一旦用上事物,则性能下降更加明显。
1.9.2 异步双写(MQ方式)
针对多数据源写入的场景,可以借助MQ实现异步的多源写入,这种情况下各个源的写入逻辑互不干扰,不会由于单个数据源写入异常或缓慢影响其他数据源的写入,虽然整体写入的吞吐量增大了,但是由于MQ消费是异步消费,所以不适合实时业务场景。
优点
- 性能高。
- 不易出现数据丢失问题,主要基于MQ消息的消费保障机制,比如ES宕机或者写入失败,还能重新消费MQ消息。
- 多源写入之间相互隔离,便于扩展更多的数据源写入。
缺点
- 硬编码问题,接入新的数据源需要实现新的消费者代码
- 系统复杂度增加:引入了消息中间件
- 可能出现延时问题:MQ是异步消费模型,用户写入的数据不一定可以马上看到,造成延时。
1.9.3 定时同步
上面两种方案中都存在硬编码问题,也就是有任何对MySQL进行增删改查的地方要么植入ES代码,要么替换为MQ代码,代码的侵入性太强。如果对实时性要求不高的情况下,可以考虑用定时器来处理.
具体步骤如下
- 1.数据库的相关表中增加一个字段为timestamp的字段,任何crud操作都会导致该字段的时间发生变化;
- 2.原来程序中的CURD操作不做任何变化;
- 3.增加一个定时器程序,让该程序按一定的时间周期扫描指定的表,把该时间段内发生变化的数据提取出来;
- 4.逐条写入到ES中。
该方案的典型实现是借助logstash实现数据同步,其底层实现原理就是根据配置定时器使用sql查询新增的数据写入ES中,实现数据的增量同步。
优点
- 不改变原来代码,没有侵入性、没有硬编码;
- 没有业务强耦合,不改变原来程序的性能;
- Worker代码编写简单不需要考虑增删改查;
缺点
- 时效性较差,由于是采用定时器根据固定频率查询表来同步数据,尽管将同步周期设置到秒级,也还是会存在一定时间的延迟。
- 对数据库有一定的轮询压力,一种改进方法是将轮询放到压力不大的从库上。
1.9.4 基于Binlog实时同步
binlog
MySQL的二进制日志binlog可以说是MySQL最重要的日志,它记录了所有的DDL和DML语句(除了数据查询语句select),以事件形式记录,还包含语句所执行的消耗的时间,MySQL的二进制日志是事务安全型的。
Canal
译意为管道,主要用途是基于 MySQL 数据库增量日志解析,提供增量数据订阅和消费。
canal可以用来监控数据库数据的变化,从而获得新增或修改的数据。
canal的工作原理
- 把自己伪装成MySQL slave,模拟MySQL slave的交互协议,向MySQL Mater发送 dump协议
- MySQL mater收到canal发送过来的dump请求,开始推送binary log给canal
- canal解析binary log对象,再发送到存储目的地。
利用MySQL的binlog来进行同步。其实现原理如下:
具体步骤如下
- 读取MySQL的binlog日志,获取指定表的日志信息;
- 将读取的信息转为MQ;
- 编写一个MQ消费程序;
- 不断消费MQ,每消费完一条消息,将消息写入到ES中。
优点
- 没有代码侵入、没有硬编码;
- 原有系统不需要任何变化,没有感知;
- 性能高;
- 业务解耦,不需要关注原来系统的业务逻辑。
缺点
- 构建Binlog系统复杂;
- 如果采用MQ消费解析的binlog信息,也会像方案二一样存在MQ延时的风险。
业界目前较为流行的方案:使用canal监听binlog同步数据到ES
1.9.4.1 Canal直接监听MySQL的binlog日志把数据同步到ES,为什么还加上RabbitMQ?
canal本身只是一个解析binlog的工具,无法持久化,服务器发生异常情况,比如说canal正在解析binlog文件服务器重启了,那么解析的数据就会丢失。而配置MQ可以实现数据持久化,服务器启动之后,MQ里的数据就会立即加载到内存中继续执行,从而保证数据一致性。
2.0 ES的聚合查询
指标聚合:求和,求最大值,最小值,平均数
数量统计聚合:这是一种特殊的度量聚合,用于计算非重复(唯一)值的近似数量,类似于SQL中的COUNT(DISTINCT …)。
去重聚合:计算非重复值的数量。
桶聚合:根据字段的值将数据划分为不同的桶,并对每个桶执行子聚合
排序后的桶聚合:在每个桶中按某个字段排序并返回顶部的N个文档。这可以用来模拟SQL中的GROUP BY后跟ORDER BY和LIMIT的行为。
最高权值聚合:在一个聚合中组合多个桶和度量聚合,并可以按多个字段对数据进行分组。
2.1 ES集群的三种颜色代表意义
绿色,黄色,红色,绿色代表集群健康,所有的主备分片都得到分配,如果有备分片没有node去分配,集群是黄色,黄色和绿色都是可用状态,如果有主分片的节点down机,集群不可写数据,呈现红色,代表集群不健康。