<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>SQL | w3xse7en</title>
    <link>https://w3xse7en.github.io/docs/sql/</link>
      <atom:link href="https://w3xse7en.github.io/docs/sql/index.xml" rel="self" type="application/rss+xml" />
    <description>SQL</description>
    <generator>Wowchemy (https://wowchemy.com)</generator><language>zh</language>
    <image>
      <url>https://w3xse7en.github.io/images/icon_hua2ec155b4296a9c9791d015323e16eb5_11927_512x512_fill_lanczos_center_2.png</url>
      <title>SQL</title>
      <link>https://w3xse7en.github.io/docs/sql/</link>
    </image>
    
    <item>
      <title>MySQL</title>
      <link>https://w3xse7en.github.io/docs/sql/mysql/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      <guid>https://w3xse7en.github.io/docs/sql/mysql/</guid>
      <description>&lt;hr&gt;
&lt;h2&gt;目录&lt;/h2&gt;
&lt;nav id=&#34;TableOfContents&#34;&gt;
  &lt;ul&gt;
    &lt;li&gt;&lt;a href=&#34;#数据库三大范式&#34;&gt;数据库三大范式&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&#34;#mysql的binlog的几种录入格式&#34;&gt;MySQL的binlog的几种录入格式&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&#34;#索引有哪几种类型&#34;&gt;索引有哪几种类型&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&#34;#索引的数据结构b树&#34;&gt;索引的数据结构（B+树）&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&#34;#索引算法&#34;&gt;索引算法&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&#34;#索引设计的原则&#34;&gt;索引设计的原则&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&#34;#创建索引的原则&#34;&gt;创建索引的原则&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&#34;#创建索引时需要注意什么&#34;&gt;创建索引时需要注意什么&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&#34;#聚簇索引与非聚簇索引&#34;&gt;聚簇索引与非聚簇索引&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&#34;#事物的四大特性acid&#34;&gt;事物的四大特性(ACID)&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&#34;#事务的隔离级别&#34;&gt;事务的隔离级别&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&#34;#redo-log事务安全&#34;&gt;Redo Log（事务安全）&lt;/a&gt;
      &lt;ul&gt;
        &lt;li&gt;&lt;a href=&#34;#crash-safe-与两阶段提交&#34;&gt;crash safe 与两阶段提交&lt;/a&gt;&lt;/li&gt;
      &lt;/ul&gt;
    &lt;/li&gt;
    &lt;li&gt;&lt;a href=&#34;#mvcc多版本并发控制undo-log&#34;&gt;MVCC（多版本并发控制）（Undo Log）&lt;/a&gt;
      &lt;ul&gt;
        &lt;li&gt;&lt;a href=&#34;#mvcc实现原理&#34;&gt;MVCC实现原理&lt;/a&gt;&lt;/li&gt;
        &lt;li&gt;&lt;a href=&#34;#mvcc与事务&#34;&gt;MVCC与事务&lt;/a&gt;&lt;/li&gt;
        &lt;li&gt;&lt;a href=&#34;#为什么需要mvcc&#34;&gt;为什么需要MVCC&lt;/a&gt;&lt;/li&gt;
      &lt;/ul&gt;
    &lt;/li&gt;
    &lt;li&gt;&lt;a href=&#34;#当前读快照读-与-幻读&#34;&gt;当前读，快照读 与 幻读&lt;/a&gt;
      &lt;ul&gt;
        &lt;li&gt;&lt;a href=&#34;#当前读&#34;&gt;当前读&lt;/a&gt;&lt;/li&gt;
        &lt;li&gt;&lt;a href=&#34;#快照读&#34;&gt;快照读&lt;/a&gt;&lt;/li&gt;
      &lt;/ul&gt;
    &lt;/li&gt;
    &lt;li&gt;&lt;a href=&#34;#锁的粒度&#34;&gt;锁的粒度&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&#34;#锁的类别&#34;&gt;锁的类别&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&#34;#锁的算法&#34;&gt;锁的算法&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&#34;#什么是死锁怎么解决&#34;&gt;什么是死锁？怎么解决&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&#34;#乐观锁和悲观锁&#34;&gt;乐观锁和悲观锁&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&#34;#大表数据查询怎么优化&#34;&gt;大表数据查询，怎么优化？&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&#34;#参考&#34;&gt;参考&lt;/a&gt;&lt;/li&gt;
  &lt;/ul&gt;
&lt;/nav&gt;
&lt;hr&gt;
&lt;br&gt;
&lt;h2 id=&#34;数据库三大范式&#34;&gt;数据库三大范式&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;第一范式：每个列都不可以再拆分。&lt;/li&gt;
&lt;li&gt;第二范式：在第一范式的基础上，非主键列完全依赖于主键，而不能是依赖于主键的一部分。&lt;/li&gt;
&lt;li&gt;第三范式：在第二范式的基础上，非主键列只依赖于主键，不依赖于其他非主键。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在设计数据库结构的时候，要尽量遵守三范式，如果不遵守，必须有足够的理由。比如性能。事实上我们经常会为了性能而妥协数据库的设计。&lt;/p&gt;
&lt;h2 id=&#34;mysql的binlog的几种录入格式&#34;&gt;MySQL的binlog的几种录入格式&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;statement模式下，每一条会修改数据的sql都会记录在binlog中。不需要记录每一行的变化，减少了binlog日志量，节约了IO，提高性能。由于sql的执行是有上下文的，因此在保存的时候需要保存相关的信息，同时还有一些使用了函数之类的语句无法被记录复制。&lt;/li&gt;
&lt;li&gt;row级别下，不记录sql语句上下文相关信息，仅保存哪条记录被修改。记录单元为每一行的改动，基本是可以全部记下来但是由于很多操作，会导致大量行的改动(比如alter table)，因此这种模式的文件保存的信息太多，日志量太大。&lt;/li&gt;
&lt;li&gt;mixed，一种折中的方案，普通操作使用statement记录，当无法使用statement的时候使用row。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;此外，新版的MySQL中对row级别也做了一些优化，当表结构发生变化的时候，会记录语句而不是逐行记录。&lt;/p&gt;
&lt;h2 id=&#34;索引有哪几种类型&#34;&gt;索引有哪几种类型&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;主键索引: 数据列不允许重复，不允许为NULL，一个表只能有一个主键。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;唯一索引: 数据列不允许重复，允许为NULL值，一个表允许多个列创建唯一索引。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;ALTER TABLE table_name ADD UNIQUE (column);&lt;/code&gt; 创建唯一索引
&lt;code&gt;ALTER TABLE table_name ADD UNIQUE (column1,column2);&lt;/code&gt; 创建唯一组合索引&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;普通索引: 基本的索引类型，没有唯一性的限制，允许为NULL值。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;ALTER TABLE table_name ADD INDEX index_name (column);&lt;/code&gt;创建普通索引
&lt;code&gt;ALTER TABLE table_name ADD INDEX index_name(column1, column2, column3);&lt;/code&gt;创建组合索引。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;全文索引： 是目前搜索引擎使用的一种关键技术。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;ALTER TABLE table_name ADD FULLTEXT (column);&lt;/code&gt;创建全文索引&lt;/p&gt;
&lt;h2 id=&#34;索引的数据结构b树&#34;&gt;索引的数据结构（B+树）&lt;/h2&gt;
&lt;p&gt;一棵 B+ 树需要满足以下条件：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;节点的子树数和关键字数相同（B 树是关键字数比子树数少一）&lt;/li&gt;
&lt;li&gt;节点的关键字表示的是子树中的最大数，在子树中同样含有这个数据&lt;/li&gt;
&lt;li&gt;叶子节点包含了全部数据，同时符合左小右大的顺序&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;B+ 树的三个优点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;层级更低，IO 次数更少&lt;/li&gt;
&lt;li&gt;每次都需要查询到叶子节点，查询性能稳定&lt;/li&gt;
&lt;li&gt;叶子节点形成有序链表，范围查询方便&lt;/li&gt;
&lt;/ol&gt;








  











&lt;figure id=&#34;figure-b树数据结构&#34;&gt;


  &lt;a data-fancybox=&#34;&#34; href=&#34;https://w3xse7en.github.io/media/sql/mysql/mysql_btree.png&#34; data-caption=&#34;B&amp;#43;树数据结构&#34;&gt;


  &lt;img src=&#34;https://w3xse7en.github.io/media/sql/mysql/mysql_btree.png&#34; alt=&#34;&#34;  &gt;
&lt;/a&gt;


  
  
  &lt;figcaption&gt;
    B+树数据结构
  &lt;/figcaption&gt;


&lt;/figure&gt;









  











&lt;figure id=&#34;figure-b树插入过程&#34;&gt;


  &lt;a data-fancybox=&#34;&#34; href=&#34;https://w3xse7en.github.io/media/sql/mysql/mysql_btree_insert.gif&#34; data-caption=&#34;B&amp;#43;树插入过程&#34;&gt;


  &lt;img src=&#34;https://w3xse7en.github.io/media/sql/mysql/mysql_btree_insert.gif&#34; alt=&#34;&#34;  &gt;
&lt;/a&gt;


  
  
  &lt;figcaption&gt;
    B+树插入过程
  &lt;/figcaption&gt;


&lt;/figure&gt;









  











&lt;figure id=&#34;figure-b树删除过程&#34;&gt;


  &lt;a data-fancybox=&#34;&#34; href=&#34;https://w3xse7en.github.io/media/sql/mysql/mysql_btree_delete.gif&#34; data-caption=&#34;B&amp;#43;树删除过程&#34;&gt;


  &lt;img src=&#34;https://w3xse7en.github.io/media/sql/mysql/mysql_btree_delete.gif&#34; alt=&#34;&#34;  &gt;
&lt;/a&gt;


  
  
  &lt;figcaption&gt;
    B+树删除过程
  &lt;/figcaption&gt;


&lt;/figure&gt;

&lt;h2 id=&#34;索引算法&#34;&gt;索引算法&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;BTree算法&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;BTree是最常用的mysql数据库索引算法，也是mysql默认的算法。因为它不仅可以被用在=,&amp;gt;,&amp;gt;=,&amp;lt;,&amp;lt;=和between这些比较操作符上，而且还可以用于like操作符，只要它的查询条件是一个不以通配符开头的常量。&lt;/p&gt;
&lt;ol start=&#34;2&#34;&gt;
&lt;li&gt;Hash算法&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Hash Hash索引只能用于对等比较，例如=,&amp;lt;=&amp;gt;（相当于=）操作符。由于是一次定位数据，不像BTree索引需要从根节点到枝节点，最后才能访问到页节点这样多次IO访问，所以检索效率远高于BTree索引。&lt;/p&gt;
&lt;h2 id=&#34;索引设计的原则&#34;&gt;索引设计的原则&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;适合索引的列是出现在where子句中的列，或者连接子句中指定的列。&lt;/li&gt;
&lt;li&gt;基数较小的类，索引效果较差，没有必要在此列建立索引&lt;/li&gt;
&lt;li&gt;使用短索引，如果对长字符串列进行索引，应该指定一个前缀长度，这样能够节省大量索引空间&lt;/li&gt;
&lt;li&gt;不要过度索引。索引需要额外的磁盘空间，并降低写操作的性能。在修改表内容的时候，索引会进行更新甚至重构，索引列越多，这个时间就会越长。所以只保持需要的索引有利于查询即可。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&#34;创建索引的原则&#34;&gt;创建索引的原则&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;最左前缀匹配原则，组合索引非常重要的原则，mysql会一直向右匹配直到遇到范围查询(&amp;gt;、&amp;lt;、between、like)就停止匹配，
比如a = 1 and b = 2 and c &amp;gt; 3 and d = 4 如果建立(a,b,c,d)顺序的索引，d是用不到索引的，如果建立(a,b,d,c)的索引则都可以用到，a,b,d的顺序可以任意调整。&lt;/li&gt;
&lt;li&gt;较频繁作为查询条件的字段才去创建索引&lt;/li&gt;
&lt;li&gt;更新频繁字段不适合创建索引&lt;/li&gt;
&lt;li&gt;若是不能有效区分数据的列不适合做索引列(如性别，男女未知，最多也就三种，区分度实在太低)&lt;/li&gt;
&lt;li&gt;尽量的扩展索引，不要新建索引。比如表中已经有a的索引，现在要加(a,b)的索引，那么只需要修改原来的索引即可。&lt;/li&gt;
&lt;li&gt;定义有外键的数据列一定要建立索引。&lt;/li&gt;
&lt;li&gt;对于那些查询中很少涉及的列，重复值比较多的列不要建立索引。&lt;/li&gt;
&lt;li&gt;对于定义为text、image和bit的数据类型的列不要建立索引。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&#34;创建索引时需要注意什么&#34;&gt;创建索引时需要注意什么&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;非空字段：应该指定列为NOT NULL，除非你想存储NULL。在mysql中，含有空值的列很难进行查询优化，因为它们使得索引、索引的统计信息以及比较运算更加复杂。你应该用0、一个特殊的值或者一个空串代替空值；&lt;/li&gt;
&lt;li&gt;取值离散大的字段：（变量各个取值之间的差异程度）的列放到联合索引的前面，可以通过count()函数查看字段的差异值，返回值越大说明字段的唯一值越多字段的离散程度高；&lt;/li&gt;
&lt;li&gt;索引字段越小越好：数据库的数据存储以页为单位一页存储的数据越多一次IO操作获取的数据越大效率越高。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&#34;聚簇索引与非聚簇索引&#34;&gt;聚簇索引与非聚簇索引&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;聚簇索引：将数据存储与索引放到了一块，找到索引也就找到了数据&lt;/li&gt;
&lt;li&gt;非聚簇索引：将数据存储于索引分开结构，索引结构的叶子节点指向了数据的对应行，myisam通过key_buffer把索引先缓存到内存中，
当需要访问数据时（通过索引访问数据），在内存中直接搜索索引，然后通过索引找到磁盘相应数据，这也就是为什么索引不在key buffer命中时，速度慢的原因。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;事物的四大特性acid&#34;&gt;事物的四大特性(ACID)&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;原子性： 事务是最小的执行单位，不允许分割。事务的原子性确保动作要么全部完成，要么完全不起作用；&lt;/li&gt;
&lt;li&gt;一致性： 执行事务前后，数据保持一致，多个事务对同一个数据读取的结果是相同的；&lt;/li&gt;
&lt;li&gt;隔离性： 并发访问数据库时，一个用户的事务不被其他事务所干扰，各并发事务之间数据库是独立的；&lt;/li&gt;
&lt;li&gt;持久性： 一个事务被提交之后。它对数据库中数据的改变是持久的，即使数据库发生故障也不应该对其有任何影响。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;事务的隔离级别&#34;&gt;事务的隔离级别&lt;/h2&gt;
&lt;p&gt;为了达到事务的四大特性，数据库定义了4种不同的事务隔离级别，由低到高依次为Read uncommitted、Read committed、Repeatable read、Serializable，这四个级别可以逐个解决脏读、不可重复读、幻读这几类问题。&lt;/p&gt;
&lt;p&gt;SQL 标准定义了四个隔离级别：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;READ-UNCOMMITTED(读取未提交)： 最低的隔离级别，允许读取尚未提交的数据变更，可能会导致脏读、幻读或不可重复读。&lt;/li&gt;
&lt;li&gt;READ-COMMITTED(读取已提交)： 允许读取并发事务已经提交的数据，可以阻止脏读，但是幻读或不可重复读仍有可能发生。&lt;/li&gt;
&lt;li&gt;REPEATABLE-READ(可重复读)： 对同一字段的多次读取结果都是一致的，除非数据是被本身事务自己所修改，可以阻止脏读和不可重复读，但幻读仍有可能发生。&lt;/li&gt;
&lt;li&gt;SERIALIZABLE(可串行化)： 最高的隔离级别，完全服从ACID的隔离级别。所有的事务依次逐个执行，这样事务之间就完全不可能产生干扰，也就是说，该级别可以防止脏读、不可重复读以及幻读。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Mysql 默认采用的 REPEATABLE_READ隔离级别 Oracle 默认采用的 READ_COMMITTED隔离级别&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&#34;redo-log事务安全&#34;&gt;Redo Log（事务安全）&lt;/h2&gt;
&lt;p&gt;在工程存储项目中，有一个重要的概念，那就是 crash safe，即当服务器突然断电或宕机，需要保证已提交的数据或修改不会丢失，未提交的数据能够自动回滚，这就是 mysql ACID 特性中的一个十分重要的特性 &amp;ndash; Atomicity 原子性&lt;/p&gt;
&lt;p&gt;依靠 binlog 是无法保证 crash safe 的，因为 binlog 是事务提交时写入的，如果在 binlog 缓存中的数据持久化到硬盘之前宕机或断电，
在服务器恢复工作后，由于 binlog 缺失一部分已提交的操作数据，而主数据库中实际上这部分操作已经存在，从数据库因此无法同步这部分操作，从而造成主从数据库数据不一致，这是很严重的&lt;/p&gt;
&lt;p&gt;innodb 作为具体的一个存储引擎，他通过 redolog 实现了 crash safe 的支持&lt;/p&gt;
&lt;p&gt;mysql 有一个基本的技术理念，那就是 WAL，即  Write-Ahead Logging，先写日志，再写磁盘，从而保证每一次操作都有据可查，这里所说的“先写日志”中的日志就包括 innodb 的 redolog&lt;/p&gt;
&lt;h3 id=&#34;crash-safe-与两阶段提交&#34;&gt;crash safe 与两阶段提交&lt;/h3&gt;
&lt;p&gt;每条 redolog 都有两个状态 &amp;ndash; prepare 与 commit 状态&lt;/p&gt;
&lt;p&gt;例如对于一张 mysql 表，我们执行一条 SQL 语句：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;UPDATE A set C=C+1 WHERE ID=2&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;实际上，mysql 数据库会进行以下操作（下图中深色的是 mysql server 层所做的操作，浅色部分则是 innodb 存储引擎进行的操作）：&lt;/p&gt;








  











&lt;figure &gt;


  &lt;a data-fancybox=&#34;&#34; href=&#34;https://w3xse7en.github.io/media/sql/mysql/mysql_redo_log.png&#34; &gt;


  &lt;img src=&#34;https://w3xse7en.github.io/media/sql/mysql/mysql_redo_log.png&#34; alt=&#34;&#34;  &gt;
&lt;/a&gt;



&lt;/figure&gt;

&lt;p&gt;在写入 binlog 及事务提交前，innodb 先记录了 redolog，并标记为 prepare 状态，在事务提交后，innodb 会将 redolog 更新为 commit 状态，这样在异常发生时，就可以按照下面两条策略来处理：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当异常情况发生时，如果第一次写入 redolog 成功，写入 binlog 失败，MySQL 会当做事务失败直接回滚，保证了后续 redolog 和 binlog 的准确性&lt;/li&gt;
&lt;li&gt;如果第一次写入 redolog 成功，binlog 也写入成功，当第二次写入 redolog 时候失败了，那数据恢复的过程中，MySQL 判断 redolog 状态为 prepare，且存在对应的 binlog 记录，则会重放事务提交，数据库中会进行相应的修改操作&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;整个过程是一个典型的两阶段提交过程，由 binlog 充当了协调者的角色，针对每一次日志写入，innodb 都会随之记录一个 8 字节序列号 &amp;ndash; LSN（日志逻辑序列号 log sequence number），他会随着日志写入不断单调递增&lt;/p&gt;
&lt;p&gt;binlog、DB 中的数据、redolog 三者就是通过 LSN 关联到一起的，因为数据页上记录了 LSN、日志开始与结束均记录了 LSN、刷盘节点 checkpoint 也记录了 LSN，因此 LSN 成为了整套系统中的全局版本信息&lt;/p&gt;
&lt;p&gt;当异常发生并重新启动后，innodb 会根据出在 prepare 状态的 redo log 记录去查找相同 LSN 的 binlog、数据记录，从而实现异常后的恢复&lt;/p&gt;
&lt;h2 id=&#34;mvcc多版本并发控制undo-log&#34;&gt;MVCC（多版本并发控制）（Undo Log）&lt;/h2&gt;
&lt;p&gt;undo log 与 redo log 一起构成了 MySQL 事务日志，日志先行原则 WAL 除了包含 redo log 外，也包括 undo log，事务中的每一次修改，innodb 都会先记录对应的 undo log 记录&lt;/p&gt;
&lt;p&gt;redo log 用于数据的灾后重新提交，undo log 主要用于数据修改的回滚&lt;/p&gt;
&lt;p&gt;redo log 记录的是物理页的修改，undo log 记录的是逻辑日志&lt;/p&gt;
&lt;p&gt;delete 一条记录时，undo log 中会记录一条对应的 insert 记录，反之亦然，当 update 一条记录时，它记录一条对应相反的 update 记录，如果 update 的是主键，则是对先删除后插入的两个事件的反向逻辑操作的记录&lt;/p&gt;








  











&lt;figure id=&#34;figure-undo-log&#34;&gt;


  &lt;a data-fancybox=&#34;&#34; href=&#34;https://w3xse7en.github.io/media/sql/mysql/mysql_undo_log_chain.png&#34; data-caption=&#34;undo log&#34;&gt;


  &lt;img src=&#34;https://w3xse7en.github.io/media/sql/mysql/mysql_undo_log_chain.png&#34; alt=&#34;&#34;  &gt;
&lt;/a&gt;


  
  
  &lt;figcaption&gt;
    undo log
  &lt;/figcaption&gt;


&lt;/figure&gt;

&lt;p&gt;这样，在事务回滚时，我们就可以从 undo log 中反向读取相应的内容，并进行回滚，同时，我们也可以根据 undo log 中记录的日志读取到一条被修改后数据的原值&lt;/p&gt;
&lt;p&gt;正是依赖 undo log，innodb 实现了 ACID 中的 C &amp;ndash; Consistency 即一致性&lt;/p&gt;
&lt;h3 id=&#34;mvcc实现原理&#34;&gt;MVCC实现原理&lt;/h3&gt;
&lt;p&gt;InnoDB 中 MVCC 的实现方式为：每一行记录都有两个隐藏列：DATA_TRX_ID、DATA_ROLL_PTR（如果没有主键，则还会多一个隐藏的主键列）。&lt;/p&gt;








  











&lt;figure id=&#34;figure-隐藏列data_trx_id-data_roll_ptr&#34;&gt;


  &lt;a data-fancybox=&#34;&#34; href=&#34;https://w3xse7en.github.io/media/sql/mysql/mysql_mvcc_struct.jpeg&#34; data-caption=&#34;隐藏列：DATA_TRX_ID, DATA_ROLL_PTR&#34;&gt;


  &lt;img src=&#34;https://w3xse7en.github.io/media/sql/mysql/mysql_mvcc_struct.jpeg&#34; alt=&#34;&#34;  &gt;
&lt;/a&gt;


  
  
  &lt;figcaption&gt;
    隐藏列：DATA_TRX_ID, DATA_ROLL_PTR
  &lt;/figcaption&gt;


&lt;/figure&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;DATA_TRX_ID：记录最近更新这条行记录的事务 ID，大小为 6 个字节&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;DATA_ROLL_PTR：表示指向该行回滚段（rollback segment）的指针，大小为 7 个字节，InnoDB 便是通过这个指针找到之前版本的数据。该行记录上所有旧版本，在 undo 中都通过链表的形式组织。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;DB_ROW_ID：行标识（隐藏单调自增 ID），大小为 6 字节，如果表没有主键，InnoDB 会自动生成一个隐藏主键，因此会出现这个列。另外，每条记录的头信息（record header）里都有一个专门的 bit（deleted_flag）来表示当前记录是否已经被删除。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;多个事务并行操作某行数据的情况下，不同事务对该行数据的 UPDATE 会产生多个版本，然后通过回滚指针组织成一条 Undo Log 链&lt;/p&gt;








  











&lt;figure id=&#34;figure-版本链&#34;&gt;


  &lt;a data-fancybox=&#34;&#34; href=&#34;https://w3xse7en.github.io/media/sql/mysql/mysql_mvcc_undo_chain.jpeg&#34; data-caption=&#34;版本链&#34;&gt;


  &lt;img src=&#34;https://w3xse7en.github.io/media/sql/mysql/mysql_mvcc_undo_chain.jpeg&#34; alt=&#34;&#34;  &gt;
&lt;/a&gt;


  
  
  &lt;figcaption&gt;
    版本链
  &lt;/figcaption&gt;


&lt;/figure&gt;

&lt;h3 id=&#34;mvcc与事务&#34;&gt;MVCC与事务&lt;/h3&gt;








  











&lt;figure id=&#34;figure-多版本读&#34;&gt;


  &lt;a data-fancybox=&#34;&#34; href=&#34;https://w3xse7en.github.io/media/sql/mysql/mysql_mvcc_example.jpeg&#34; data-caption=&#34;多版本读&#34;&gt;


  &lt;img src=&#34;https://w3xse7en.github.io/media/sql/mysql/mysql_mvcc_example.jpeg&#34; alt=&#34;&#34;  &gt;
&lt;/a&gt;


  
  
  &lt;figcaption&gt;
    多版本读
  &lt;/figcaption&gt;


&lt;/figure&gt;

&lt;p&gt;在事务 A 提交前后，事务 B 读取到的 x 的值是什么呢？答案是：事务 B 在不同的隔离级别下，读取到的值不一样。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果事务 B 的隔离级别是读未提交（RU），那么两次读取均读取到 x 的最新值，即 20。&lt;/li&gt;
&lt;li&gt;如果事务 B 的隔离级别是读已提交（RC），那么第一次读取到旧值 10，第二次因为事务 A 已经提交，则读取到新值 20。&lt;/li&gt;
&lt;li&gt;如果事务 B 的隔离级别是可重复读或者串行（RR，S），则两次均读到旧值 10，不论事务 A 是否已经提交。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在不同的隔离级别下，数据库通过 MVCC 和隔离级别，让事务之间并行操作遵循了某种规则，来保证单个事务内前后数据的一致性。&lt;/p&gt;
&lt;h3 id=&#34;为什么需要mvcc&#34;&gt;为什么需要MVCC&lt;/h3&gt;
&lt;p&gt;InnoDB 相比 MyISAM 有两大特点，一是支持事务而是支持行级锁，事务的引入带来了一些新的挑战。相对于串行处理来说，并发事务处理能大大增加数据库资源的利用率，提高数据库系统的事务吞吐量，从而可以支持可以支持更多的用户。但并发事务处理也会带来一些问题，主要包括以下几种情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;更新丢失（Lost Update）：当两个或多个事务选择同一行，然后基于最初选定的值更新该行时，由于每个事务都不知道其他事务的存在，就会发生丢失更新问题 —— 最后的更新覆盖了其他事务所做的更新。如何避免这个问题呢，最好在一个事务对数据进行更改但还未提交时，其他事务不能访问修改同一个数据。&lt;/li&gt;
&lt;li&gt;脏读（Dirty Reads）：一个事务正在对一条记录做修改，在这个事务并提交前，这条记录的数据就处于不一致状态；这时，另一个事务也来读取同一条记录，如果不加控制，第二个事务读取了这些尚未提交的脏数据，并据此做进一步的处理，就会产生未提交的数据依赖关系。这种现象被形象地叫做 “脏读”。&lt;/li&gt;
&lt;li&gt;不可重复读（Non-Repeatable Reads）：一个事务在读取某些数据已经发生了改变、或某些记录已经被删除了！这种现象叫做“不可重复读”。&lt;/li&gt;
&lt;li&gt;幻读（Phantom Reads）：一个事务按相同的查询条件重新读取以前检索过的数据，却发现其他事务插入了满足其查询条件的新数据，这种现象就称为 “幻读”。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;以上是并发事务过程中会存在的问题，解决更新丢失可以交给应用，但是后三者需要数据库提供事务间的隔离机制来解决。实现隔离机制的方法主要有两种：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;加读写锁&lt;/li&gt;
&lt;li&gt;一致性快照读，即 MVCC&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&#34;当前读快照读-与-幻读&#34;&gt;当前读，快照读 与 幻读&lt;/h2&gt;
&lt;h3 id=&#34;当前读&#34;&gt;当前读&lt;/h3&gt;
&lt;p&gt;select&amp;hellip;lock in share mode (共享读锁)
select&amp;hellip;for update
update , delete , insert&lt;/p&gt;
&lt;p&gt;当前读, 读取的是最新版本, 并且对读取的记录加锁, 阻塞其他事务同时改动相同记录，避免出现安全问题。&lt;/p&gt;
&lt;p&gt;例如，假设要update一条记录，但是另一个事务已经delete这条数据并且commit了，如果不加锁就会产生冲突。所以update的时候肯定要是当前读，得到最新的信息并且锁定相应的记录。&lt;/p&gt;
&lt;h4 id=&#34;当前读的实现方式&#34;&gt;当前读的实现方式&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;next-key锁(行记录锁+Gap间隙锁)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;间隙锁：只有在Read Repeatable、Serializable隔离级别才有，就是锁定范围空间的数据，假设id有3,4,5，锁定id&amp;gt;3的数据，是指的4，5及后面的数字都会被锁定，因为此时如果不锁定没有的数据，例如当加入了新的数据id=6，就会出现幻读，间隙锁避免了幻读。&lt;/p&gt;
&lt;p&gt;1.对主键或唯一索引，如果当前读时，where条件全部精确命中(=或者in)，这种场景本身就不会出现幻读，所以只会加行记录锁。&lt;/p&gt;
&lt;p&gt;2.没有索引的列，当前读操作时，会加全表gap锁，生产环境要注意。&lt;/p&gt;
&lt;p&gt;3.非唯一索引列，如果where条件部分命中(&amp;gt;、&amp;lt;、like等)或者全未命中，则会加附近Gap间隙锁。例如，某表数据如下，非唯一索引2,6,9,9,11,15。如下语句要操作非唯一索引列9的数据，gap锁将会锁定的列是(6,11]，该区间内无法插入数据。&lt;/p&gt;








  











&lt;figure id=&#34;figure-非唯一索引列间隙锁&#34;&gt;


  &lt;a data-fancybox=&#34;&#34; href=&#34;https://w3xse7en.github.io/media/sql/mysql/mysql_gap_lock.png&#34; data-caption=&#34;&amp;lt;strong&amp;gt;非唯一索引列&amp;lt;/strong&amp;gt;间隙锁&#34;&gt;


  &lt;img src=&#34;https://w3xse7en.github.io/media/sql/mysql/mysql_gap_lock.png&#34; alt=&#34;&#34;  &gt;
&lt;/a&gt;


  
  
  &lt;figcaption&gt;
    &lt;strong&gt;非唯一索引列&lt;/strong&gt;间隙锁
  &lt;/figcaption&gt;


&lt;/figure&gt;

&lt;h3 id=&#34;快照读&#34;&gt;快照读&lt;/h3&gt;
&lt;p&gt;简单的select操作(不包括 select &amp;hellip; lock in share mode, select &amp;hellip; for update)。　　　　&lt;/p&gt;
&lt;p&gt;Read Committed隔离级别：每次select都生成一个快照读。&lt;/p&gt;
&lt;p&gt;Read Repeatable隔离级别：&lt;strong&gt;开启事务后第一个select语句才是快照读的地方，而不是一开启事务就快照读。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;快照读不会加锁，因此开启事务后进行快照读select，并不能阻止其他事务写入或修改当前事务里的数据，因此可能会出现幻读。&lt;/p&gt;
&lt;h4 id=&#34;快照读的实现方式&#34;&gt;快照读的实现方式&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;MVCC(undo log)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;锁的粒度&#34;&gt;锁的粒度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;行级锁：行级锁是Mysql中锁定粒度最细的一种锁，表示只针对当前操作的行进行加锁。行级锁能大大减少数据库操作的冲突。其加锁粒度最小，但加锁的开销也最大。行级锁分为共享锁 和 排他锁。特点：开销大，加锁慢；会出现死锁；锁定粒度最小，发生锁冲突的概率最低，并发度也最高。&lt;/li&gt;
&lt;li&gt;表级锁：表级锁是MySQL中锁定粒度最大的一种锁，表示对当前操作的整张表加锁，它实现简单，资源消耗较少，被大部分MySQL引擎支持。最常使用的MYISAM与INNODB都支持表级锁定。表级锁定分为表共享读锁（共享锁）与表独占写锁（排他锁）。特点：开销小，加锁快；不会出现死锁；锁定粒度大，发出锁冲突的概率最高，并发度最低。&lt;/li&gt;
&lt;li&gt;页级锁：页级锁是MySQL中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快，但冲突多，行级冲突少，但速度慢。所以取了折衷的页级，一次锁定相邻的一组记录。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;锁的类别&#34;&gt;锁的类别&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;共享锁: 又叫做读锁。 当用户要进行数据的读取时，对数据加上共享锁。共享锁可以同时加上多个。&lt;/li&gt;
&lt;li&gt;排他锁: 又叫做写锁。 当用户要进行数据的写入时，对数据加上排他锁。排他锁只可以加一个，他和其他的排他锁，共享锁都相斥。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;锁的算法&#34;&gt;锁的算法&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Record lock：单个行记录上的锁&lt;/li&gt;
&lt;li&gt;Gap lock：间隙锁，锁定一个范围，不包括记录本身&lt;/li&gt;
&lt;li&gt;Next-key lock：record+gap 锁定一个范围，包含记录本身&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;什么是死锁怎么解决&#34;&gt;什么是死锁？怎么解决&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;死锁是指两个或多个事务在同一资源上相互占用，并请求锁定对方的资源，从而导致恶性循环的现象。&lt;/li&gt;
&lt;li&gt;常见的解决死锁的方法
&lt;ul&gt;
&lt;li&gt;如果不同程序会并发存取多个表，尽量约定以相同的顺序访问表，可以大大降低死锁机会。&lt;/li&gt;
&lt;li&gt;在同一个事务中，尽可能做到一次锁定所需要的所有资源，减少死锁产生概率；&lt;/li&gt;
&lt;li&gt;对于非常容易产生死锁的业务部分，可以尝试使用升级锁定颗粒度，通过表级锁定来减少死锁产生的概率；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;如果业务处理不好可以用分布式事务锁或者使用乐观锁&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;乐观锁和悲观锁&#34;&gt;乐观锁和悲观锁&lt;/h2&gt;
&lt;p&gt;数据库管理系统（DBMS）中的并发控制的任务是确保在多个事务同时存取数据库中同一数据时不破坏事务的隔离性和统一性以及数据库的统一性。乐观并发控制（乐观锁）和悲观并发控制（悲观锁）是并发控制主要采用的技术手段。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;悲观锁：假定会发生并发冲突，屏蔽一切可能违反数据完整性的操作。在查询完数据的时候就把事务锁起来，直到提交事务。实现方式：使用数据库中的锁机制&lt;/li&gt;
&lt;li&gt;乐观锁：假设不会发生并发冲突，只在提交操作时检查是否违反数据完整性。在修改数据的时候把事务锁起来，通过version的方式来进行锁定。实现方式：乐一般会使用版本号机制或CAS算法实现。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;大表数据查询怎么优化&#34;&gt;大表数据查询，怎么优化？&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;优化shema、sql语句+索引；&lt;/li&gt;
&lt;li&gt;第二加缓存，memcached, redis；&lt;/li&gt;
&lt;li&gt;主从复制，读写分离；&lt;/li&gt;
&lt;li&gt;垂直拆分，根据你模块的耦合度，将一个大的系统分为多个小的系统，也就是分布式系统&lt;/li&gt;
&lt;li&gt;水平切分，针对数据量大的表，这一步最麻烦，最能考验技术水平，要选择一个合理的sharding key,
为了有好的查询效率，表结构也要改动，做一定的冗余，应用也要改，sql中尽量带sharding key，将数据定位到限定的表上去查，而不是扫描全部的表&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;参考&#34;&gt;参考&lt;/h2&gt;
&lt;p&gt;&lt;a href=&#34;https://chenjiabing666.github.io/2020/04/20/Mysql%E6%9C%80%E5%85%A8%E9%9D%A2%E8%AF%95%E6%8C%87%E5%8D%97/&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;Mysql最全面试指南&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://cloud.tencent.com/developer/article/1454636&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;InnoDB MVCC 机制，看这篇就够了&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://techlog.cn/article/list/10183403&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;mysql 异常情况下的事务安全 &amp;ndash; 详解 mysql redolog&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://techlog.cn/article/list/10183404&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;一文讲透 MySQL 的 MVCC 机制&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://www.cnblogs.com/wwcom123/p/10727194.html?spm=a2c6h.12873639.0.0.1bf85681isB566&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;【MySQL】当前读、快照读、MVCC&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://www.modb.pro/db/13606&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;MySQL InnoDB Cluster 详解&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://jeremyxu2010.github.io/2019/05/mysql-innodb-cluster%E5%AE%9E%E6%88%98/&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;MySQL InnoDB Cluster实战&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://zhuanlan.zhihu.com/p/128461028&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;MySQL-8.0 Group Replication 研究与改造汇总&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://www.zhihu.com/question/53617036&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;如何看待MySQL发布的Group Replication？&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://dbaplus.cn/news-141-2231-1.html&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;美团点评基于MGR的CMDB高可用架构搭建之路&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
</description>
    </item>
    
    <item>
      <title>Redis</title>
      <link>https://w3xse7en.github.io/docs/sql/redis/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      <guid>https://w3xse7en.github.io/docs/sql/redis/</guid>
      <description>&lt;hr&gt;
&lt;h2&gt;目录&lt;/h2&gt;
&lt;nav id=&#34;TableOfContents&#34;&gt;
  &lt;ul&gt;
    &lt;li&gt;&lt;a href=&#34;#数据结构&#34;&gt;数据结构&lt;/a&gt;
      &lt;ul&gt;
        &lt;li&gt;&lt;a href=&#34;#redis内部存储结构&#34;&gt;Redis内部存储结构&lt;/a&gt;&lt;/li&gt;
        &lt;li&gt;&lt;a href=&#34;#string&#34;&gt;String&lt;/a&gt;&lt;/li&gt;
        &lt;li&gt;&lt;a href=&#34;#hash&#34;&gt;Hash&lt;/a&gt;&lt;/li&gt;
        &lt;li&gt;&lt;a href=&#34;#rehash&#34;&gt;ReHash&lt;/a&gt;&lt;/li&gt;
        &lt;li&gt;&lt;a href=&#34;#skiplist&#34;&gt;SkipList&lt;/a&gt;&lt;/li&gt;
        &lt;li&gt;&lt;a href=&#34;#高级数据结构&#34;&gt;高级数据结构&lt;/a&gt;&lt;/li&gt;
      &lt;/ul&gt;
    &lt;/li&gt;
    &lt;li&gt;&lt;a href=&#34;#缓存&#34;&gt;缓存&lt;/a&gt;
      &lt;ul&gt;
        &lt;li&gt;&lt;a href=&#34;#缓存穿透击穿&#34;&gt;缓存穿透/击穿&lt;/a&gt;&lt;/li&gt;
        &lt;li&gt;&lt;a href=&#34;#缓存雪崩&#34;&gt;缓存雪崩&lt;/a&gt;&lt;/li&gt;
        &lt;li&gt;&lt;a href=&#34;#热点缓存&#34;&gt;热点缓存&lt;/a&gt;&lt;/li&gt;
      &lt;/ul&gt;
    &lt;/li&gt;
    &lt;li&gt;&lt;a href=&#34;#布隆过滤器bloom-filter&#34;&gt;布隆过滤器(Bloom Filter)&lt;/a&gt;
      &lt;ul&gt;
        &lt;li&gt;&lt;a href=&#34;#概念&#34;&gt;概念&lt;/a&gt;&lt;/li&gt;
        &lt;li&gt;&lt;a href=&#34;#原理&#34;&gt;原理&lt;/a&gt;&lt;/li&gt;
        &lt;li&gt;&lt;a href=&#34;#缺点&#34;&gt;缺点&lt;/a&gt;&lt;/li&gt;
      &lt;/ul&gt;
    &lt;/li&gt;
    &lt;li&gt;&lt;a href=&#34;#分布式锁&#34;&gt;分布式锁&lt;/a&gt;
      &lt;ul&gt;
        &lt;li&gt;&lt;a href=&#34;#为什么要用分布式锁&#34;&gt;为什么要用分布式锁&lt;/a&gt;&lt;/li&gt;
        &lt;li&gt;&lt;a href=&#34;#简单的分布式锁实现&#34;&gt;简单的分布式锁实现&lt;/a&gt;&lt;/li&gt;
        &lt;li&gt;&lt;a href=&#34;#redlock&#34;&gt;RedLock&lt;/a&gt;&lt;/li&gt;
        &lt;li&gt;&lt;a href=&#34;#个人想法&#34;&gt;个人想法&lt;/a&gt;&lt;/li&gt;
      &lt;/ul&gt;
    &lt;/li&gt;
    &lt;li&gt;&lt;a href=&#34;#持久化&#34;&gt;持久化&lt;/a&gt;
      &lt;ul&gt;
        &lt;li&gt;&lt;a href=&#34;#rdb优缺点&#34;&gt;RDB优缺点&lt;/a&gt;&lt;/li&gt;
        &lt;li&gt;&lt;a href=&#34;#aof优缺点&#34;&gt;AOF优缺点&lt;/a&gt;&lt;/li&gt;
        &lt;li&gt;&lt;a href=&#34;#同步机制&#34;&gt;同步机制&lt;/a&gt;&lt;/li&gt;
      &lt;/ul&gt;
    &lt;/li&gt;
    &lt;li&gt;&lt;a href=&#34;#高可用集群&#34;&gt;高可用/集群&lt;/a&gt;
      &lt;ul&gt;
        &lt;li&gt;&lt;a href=&#34;#gossip协议&#34;&gt;Gossip协议&lt;/a&gt;&lt;/li&gt;
        &lt;li&gt;&lt;a href=&#34;#redis的分片机制&#34;&gt;Redis的分片机制&lt;/a&gt;&lt;/li&gt;
        &lt;li&gt;&lt;a href=&#34;#为什么rediscluster会设计成16384个槽呢&#34;&gt;为什么RedisCluster会设计成16384个槽呢？&lt;/a&gt;&lt;/li&gt;
        &lt;li&gt;&lt;a href=&#34;#redis数据增多了是该加内存还是加实例&#34;&gt;Redis数据增多了，是该加内存还是加实例？&lt;/a&gt;&lt;/li&gt;
        &lt;li&gt;&lt;a href=&#34;#集群脑裂&#34;&gt;集群脑裂&lt;/a&gt;&lt;/li&gt;
      &lt;/ul&gt;
    &lt;/li&gt;
    &lt;li&gt;&lt;a href=&#34;#参考&#34;&gt;参考&lt;/a&gt;&lt;/li&gt;
  &lt;/ul&gt;
&lt;/nav&gt;
&lt;hr&gt;
&lt;br&gt;
&lt;h2 id=&#34;数据结构&#34;&gt;数据结构&lt;/h2&gt;
&lt;p&gt;String Hash List Set SortedSet。&lt;/p&gt;
&lt;h3 id=&#34;redis内部存储结构&#34;&gt;Redis内部存储结构&lt;/h3&gt;
&lt;p&gt;dictEntry&lt;/p&gt;
&lt;p&gt;因为 Redis 是 KV 的数据库，它是通过 hashtable 实现的（我们把这个叫做外层的哈希）。&lt;/p&gt;
&lt;p&gt;所以每个键值对都会有一个 dictEntry，里面指向了 key 和 value 的指针。next 指向下一个 dictEntry。源码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef struct dictEntry {
    void *key;              //关键字
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;                    //val
    struct dictEntry *next; //next
} dictEntry;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;key 是字符串，但是 Redis 没有直接使用 C 的字符数组，而是存储在自定义的 SDS中。&lt;/p&gt;
&lt;p&gt;value 既不是直接作为字符串存储，也不是直接存储在 SDS 中，而是存储在redisObject 中。&lt;/p&gt;
&lt;p&gt;实际上五种常用的数据类型的任何一种，都是通过 redisObject 来存储的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef struct redisObject {
    unsigned type:4; /* 对象的类型， 包括： OBJ_STRING、 OBJ_LIST、 OBJ_HASH、 OBJ_SET、 OBJ_ZSET */
    unsigned encoding:4; /* 具体的数据结构 */
    unsigned lru:LRU_BITS; /* 24 位， 对象最后一次被命令程序访问的时间， 与内存回收有关 */
    int refcount; /* 引用计数。 当 refcount 为 0 的时候， 表示该对象已经不被任何对象引用， 则可以进行垃圾回收了*/
    void *ptr; /* 指向对象实际的数据结构 */
} robj;
&lt;/code&gt;&lt;/pre&gt;








  











&lt;figure &gt;


  &lt;a data-fancybox=&#34;&#34; href=&#34;https://w3xse7en.github.io/media/sql/redis/redis_object.jpg&#34; &gt;


  &lt;img src=&#34;https://w3xse7en.github.io/media/sql/redis/redis_object.jpg&#34; alt=&#34;&#34;  &gt;
&lt;/a&gt;



&lt;/figure&gt;

&lt;h3 id=&#34;string&#34;&gt;String&lt;/h3&gt;
&lt;p&gt;sds是在Redis中被广泛使用的字符串结构，它的全称是Simple Dynamic String。与其它语言环境中出现的字符串相比，它具有如下显著的特点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;可动态扩展内存。sds表示的字符串其内容可以修改，也可以追加。在很多语言中字符串会分为mutable和immutable两种，显然sds属于mutable类型的。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;二进制安全（Binary Safe）。sds能存储任意二进制数据，而不仅仅是可打印字符。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;与传统的C语言字符串类型兼容。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&#34;hash&#34;&gt;Hash&lt;/h3&gt;








  











&lt;figure id=&#34;figure-hash链接法&#34;&gt;


  &lt;a data-fancybox=&#34;&#34; href=&#34;https://w3xse7en.github.io/media/sql/redis/hash_dict_jg.png&#34; data-caption=&#34;Hash链接法&#34;&gt;


  &lt;img src=&#34;https://w3xse7en.github.io/media/sql/redis/hash_dict_jg.png&#34; alt=&#34;&#34;  &gt;
&lt;/a&gt;


  
  
  &lt;figcaption&gt;
    Hash链接法
  &lt;/figcaption&gt;


&lt;/figure&gt;

&lt;h3 id=&#34;rehash&#34;&gt;ReHash&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;为ht[1] 分配空间，这个哈希表的空间大小取决于要执行的操作， 以及ht[0]当前包含的键值对数量 （也即是ht[0].used属性的值）：
&lt;ul&gt;
&lt;li&gt;如果执行的是扩展操作， 那么 ht[1] 的大小为第一个大于等于 ht[0].used * 2 的 2^n （2 的 n 次方幂）；&lt;/li&gt;
&lt;li&gt;如果执行的是收缩操作， 那么 ht[1] 的大小为第一个大于等于 ht[0].used 的 2^n 。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;将rehashidx 初始化为0 ，代表rehash 工作正式开始。&lt;/li&gt;
&lt;li&gt;每次字典进行删除、查找、更新操作时， 会同时在两个hash表上进行（先查找ht[0], 如果没找到，再去查找ht[1]）。 进行添加操作时，会直接添加到ht[1]。&lt;/li&gt;
&lt;li&gt;在进行每次增删改查操作时， 会同时把ht[0] 在rehashidx 索引上的所有键值对都rehash到ht[1]上， 完成后 rehashidx 加1.&lt;/li&gt;
&lt;li&gt;当ht[0] 所有元素都被复制到ht[1]， 设置rehashidx 的值为-1 。&lt;/li&gt;
&lt;li&gt;回收 ht[0]，将 ht[1] 设置为 ht[0] ， 并在 ht[1] 新创建一个空白哈希表， 为下一次 rehash 做准备。。&lt;/li&gt;
&lt;/ol&gt;








  











&lt;figure id=&#34;figure-rehash&#34;&gt;


  &lt;a data-fancybox=&#34;&#34; href=&#34;https://w3xse7en.github.io/media/sql/redis/hash_hashtable_rehash.png&#34; data-caption=&#34;rehash&#34;&gt;


  &lt;img src=&#34;https://w3xse7en.github.io/media/sql/redis/hash_hashtable_rehash.png&#34; alt=&#34;&#34;  &gt;
&lt;/a&gt;


  
  
  &lt;figcaption&gt;
    rehash
  &lt;/figcaption&gt;


&lt;/figure&gt;

&lt;h3 id=&#34;skiplist&#34;&gt;SkipList&lt;/h3&gt;








  











&lt;figure id=&#34;figure-跳表&#34;&gt;


  &lt;a data-fancybox=&#34;&#34; href=&#34;https://w3xse7en.github.io/media/sql/redis/ziplist_yl.png&#34; data-caption=&#34;跳表&#34;&gt;


  &lt;img src=&#34;https://w3xse7en.github.io/media/sql/redis/ziplist_yl.png&#34; alt=&#34;&#34;  &gt;
&lt;/a&gt;


  
  
  &lt;figcaption&gt;
    跳表
  &lt;/figcaption&gt;


&lt;/figure&gt;









  











&lt;figure id=&#34;figure-跳表查找过程&#34;&gt;


  &lt;a data-fancybox=&#34;&#34; href=&#34;https://w3xse7en.github.io/media/sql/redis/skip_list_search.png&#34; data-caption=&#34;跳表查找过程&#34;&gt;


  &lt;img src=&#34;https://w3xse7en.github.io/media/sql/redis/skip_list_search.png&#34; alt=&#34;&#34;  &gt;
&lt;/a&gt;


  
  
  &lt;figcaption&gt;
    跳表查找过程
  &lt;/figcaption&gt;


&lt;/figure&gt;

&lt;p&gt;跳表不要求上下相邻两层链表之间的节点个数有严格的对应关系，而是为每个节点随机出一个层数(level)。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#define ZSKIPLIST_MAXLEVEL 32 
#define ZSKIPLIST_P 0.25 

int zslRandomLevel(void) {
    int level = 1;
    while ((random()&amp;amp;0xFFFF) &amp;lt; (ZSKIPLIST_P * 0xFFFF))
        level += 1;
    return (level&amp;lt;ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行插入操作时计算随机数的过程，是一个很关键的过程，它对skiplist的统计特性有着很重要的影响。&lt;/p&gt;
&lt;p&gt;这并不是一个普通的服从均匀分布的随机数，而是服从一定规则的：&lt;/p&gt;
&lt;p&gt;首先，每个节点肯定都有第1层指针（每个节点都在第1层链表里）。
如果一个节点有第i层(i&amp;gt;=1)指针（即节点已经在第1层到第i层链表中），那么它有第(i+1)层指针的概率为p。
节点最大的层数不允许超过一个最大值，记为MaxLevel（Redis里是32）。
比如，一个节点随机出的层数是3，那么就把它链入到第1层到第3层这三层链表中。&lt;/p&gt;
&lt;p&gt;下图展示了如何通过一步步的插入操作从而形成一个skiplist的过程：&lt;/p&gt;








  











&lt;figure id=&#34;figure-跳表插入过程&#34;&gt;


  &lt;a data-fancybox=&#34;&#34; href=&#34;https://w3xse7en.github.io/media/sql/redis/skip_list_insert.png&#34; data-caption=&#34;跳表插入过程&#34;&gt;


  &lt;img src=&#34;https://w3xse7en.github.io/media/sql/redis/skip_list_insert.png&#34; alt=&#34;&#34;  &gt;
&lt;/a&gt;


  
  
  &lt;figcaption&gt;
    跳表插入过程
  &lt;/figcaption&gt;


&lt;/figure&gt;









  











&lt;figure id=&#34;figure-跳表&#34;&gt;


  &lt;a data-fancybox=&#34;&#34; href=&#34;https://w3xse7en.github.io/media/sql/redis/skiplist.png&#34; data-caption=&#34;跳表&#34;&gt;


  &lt;img src=&#34;https://w3xse7en.github.io/media/sql/redis/skiplist.png&#34; alt=&#34;&#34;  &gt;
&lt;/a&gt;


  
  
  &lt;figcaption&gt;
    跳表
  &lt;/figcaption&gt;


&lt;/figure&gt;

&lt;h4 id=&#34;为什么跳表层数上限是32&#34;&gt;为什么跳表层数上限是32？&lt;/h4&gt;
&lt;p&gt;根据前面的随机算法当level[0]有2的64次方个节点时，才能达到32层，因此层数上限是32完全够用了。&lt;/p&gt;
&lt;h4 id=&#34;为什么采用跳表而不使用哈希表或平衡树实现&#34;&gt;为什么采用跳表，而不使用哈希表或平衡树实现&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;skiplist和各种平衡树（如AVL、红黑树等）的元素是有序排列的，而哈希表不是有序的。因此，在哈希表上只能做单个key的查找，不适宜做范围查找。所谓范围查找，指的是查找那些大小在指定的两个值之间的所有节点。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在做范围查找的时候，平衡树比skiplist操作要复杂。在平衡树上，我们找到指定范围的小值之后，还需要以中序遍历的顺序继续寻找其它不超过大值的节点。如果不对平衡树进行一定的改造，这里的中序遍历并不容易实现。而在skiplist上进行范围查找就非常简单，只需要在找到小值之后，对第1层链表进行若干步的遍历就可以实现。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;平衡树的插入和删除操作可能引发子树的调整，逻辑复杂，而skiplist的插入和删除只需要修改相邻节点的指针，操作简单又快速。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;从内存占用上来说，skiplist比平衡树更灵活一些。一般来说，平衡树每个节点包含2个指针（分别指向左右子树），而skiplist每个节点包含的指针数目平均为1/(1-p)，具体取决于参数p的大小。如果像Redis里的实现一样，取p=1/4，那么平均每个节点包含1.33个指针，比平衡树更有优势。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&#34;高级数据结构&#34;&gt;高级数据结构&lt;/h3&gt;
&lt;p&gt;Bitmaps Hyperloglogs GEO&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;HyperLogLog是用于计算唯一事物的概率数据结构（从技术上讲，这被称为估计集合的基数）。
如果统计唯一项，项目越多，需要的内存就越多。因为需要记住过去已经看过的项，从而避免多次统计这些项。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;GEO可以将用户给定的地理位置（经度和纬度）信息储存起来，并对这些信息进行操作&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;hr&gt;
&lt;h2 id=&#34;缓存&#34;&gt;缓存&lt;/h2&gt;
&lt;h3 id=&#34;缓存穿透击穿&#34;&gt;缓存穿透/击穿&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;查询一个数据库中不存在的数据，请求会越过Redis，直接请求DB。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;做好防高频请求
非正常用户量的请求，10s内发起1000次请求
对于此请求的ip进行验证码校验，或者封禁处理&lt;/p&gt;
&lt;p&gt;接口参数合法性校验
请求id需要&amp;gt;=0,分页每页最多100条等&lt;/p&gt;
&lt;p&gt;将此key对应的value设置为一个默认的值，并设置相对短的失效时间例如30分钟&lt;/p&gt;
&lt;p&gt;使用&lt;a href=&#34;https://w3xse7en.github.io/docs/sql/redis/#布隆过滤器(Bloom Filter)&#34;&gt;布隆过滤器(Bloom Filter)&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&#34;缓存雪崩&#34;&gt;缓存雪崩&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;大量Key同时失效，又有大量请求同时到来，导致请求冲向DB，DB最终卡死。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;处理缓存雪崩，在批量往Redis存数据的时候，把每个Key的失效时间都加个随机值，这样可以保证Key不会在同一时间大面积失效&lt;/p&gt;
&lt;h3 id=&#34;热点缓存&#34;&gt;热点缓存&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;某个Key过热，压力集中到一台Redis上&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;使用多级缓存机制，将过热的Key分散到各个服务器的本地缓存中，降低过热Key所在的Redis节点的压力，其他的Key依旧由分布式Redis集群承担&lt;/p&gt;
&lt;br&gt;
&lt;hr&gt;
&lt;h2 id=&#34;布隆过滤器bloom-filter&#34;&gt;布隆过滤器(Bloom Filter)&lt;/h2&gt;
&lt;h3 id=&#34;概念&#34;&gt;概念&lt;/h3&gt;
&lt;p&gt;布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法，缺点是有一定的误识别率和删除困难。&lt;/p&gt;
&lt;h3 id=&#34;原理&#34;&gt;原理&lt;/h3&gt;
&lt;p&gt;布隆过滤器的原理是，当一个元素被加入集合时，通过K个散列函数将这个元素映射成一个位数组中的K个点，把它们置为1。&lt;/p&gt;
&lt;p&gt;检索时，我们只要看看这些点是不是都是1就（大约）知道集合中有没有它了&lt;/p&gt;
&lt;p&gt;如果这些点有任何一个0，则被检元素一定不在&lt;/p&gt;
&lt;p&gt;如果都是1，则被检元素很可能在。这就是布隆过滤器的基本思想。&lt;/p&gt;
&lt;p&gt;Bloom Filter跟单哈希函数Bit-Map不同之处在于：Bloom Filter使用了k个哈希函数，每个字符串跟k个bit对应。从而降低了冲突的概率。&lt;/p&gt;








  











&lt;figure id=&#34;figure-bloom-filter&#34;&gt;


  &lt;a data-fancybox=&#34;&#34; href=&#34;https://w3xse7en.github.io/media/sql/redis/Bloom-Filter.jpg&#34; data-caption=&#34;Bloom Filter&#34;&gt;


  &lt;img src=&#34;https://w3xse7en.github.io/media/sql/redis/Bloom-Filter.jpg&#34; alt=&#34;&#34;  &gt;
&lt;/a&gt;


  
  
  &lt;figcaption&gt;
    Bloom Filter
  &lt;/figcaption&gt;


&lt;/figure&gt;

&lt;h3 id=&#34;缺点&#34;&gt;缺点&lt;/h3&gt;
&lt;p&gt;bloom filter之所以能做到在时间和空间上的效率比较高，是因为牺牲了判断的准确率、删除的便利性&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;存在误判，可能要查到的元素并没有在容器中，但是hash之后得到的k个位置上值都是1。如果bloom filter中存储的是黑名单，那么可以通过建立一个白名单来存储可能会误判的元素。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;删除困难。一个放入容器的元素映射到bit数组的k个位置上是1，删除的时候不能简单的直接置为0，可能会影响其他元素的判断。可以采用Counting Bloom Filter&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;hr&gt;
&lt;h2 id=&#34;分布式锁&#34;&gt;分布式锁&lt;/h2&gt;
&lt;h3 id=&#34;为什么要用分布式锁&#34;&gt;为什么要用分布式锁&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Efficiency（效率） 在分布式系统中，避免不同节点重复做相同的工作，节约计算机资源。&lt;/li&gt;
&lt;li&gt;Correctness（正确) 避免不同节点并发处理同一段数据时，相互干扰结果。例如对一个订单同时进行不同流程，最终订单状态出现混乱&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;简单的分布式锁实现&#34;&gt;简单的分布式锁实现&lt;/h3&gt;
&lt;p&gt;单节点Redis&lt;/p&gt;
&lt;p&gt;简单实现，可以使用 &lt;code&gt;SET key value PX milliseoncds NX&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;这个方案会引申出两个问题&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;锁从master复制到slave的时候挂了，会出现同一资源被多个client加锁。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;执行时间超过了锁的过期时间。很难保证任务一定能在锁的过期时间内完成。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&#34;redlock&#34;&gt;RedLock&lt;/h3&gt;
&lt;p&gt;Redlock算法是Antirez在单Redis节点基础上引入的高可用模式。&lt;/p&gt;
&lt;p&gt;在Redis的分布式环境中，我们假设有N个完全互相独立的Redis节点，在N个Redis实例上使用与在Redis单实例下相同方法获取锁和释放锁。&lt;/p&gt;
&lt;p&gt;现在假设有5个Redis主节点(大于3的奇数个)，这样基本保证他们不会同时都宕掉。&lt;/p&gt;
&lt;p&gt;获取锁和释放锁的过程中，客户端会执行以下操作:&lt;/p&gt;
&lt;p&gt;1.获取当前Unix时间，以毫秒为单位&lt;/p&gt;
&lt;p&gt;2.依次尝试从5个实例，使用相同的key和具有唯一性的value获取锁当向Redis请求获取锁时，客户端应该设置一个网络连接和响应超时时间，这个超时时间应该小于锁的失效时间，这样可以避免客户端死等&lt;/p&gt;
&lt;p&gt;3.客户端使用当前时间减去开始获取锁时间就得到获取锁使用的时间。当且仅当从半数以上的Redis节点取到锁，并且使用的时间小于锁失效时间时，锁才算获取成功&lt;/p&gt;
&lt;p&gt;4.如果取到了锁，key的真正有效时间等于有效时间减去获取锁所使用的时间，这个很重要&lt;/p&gt;
&lt;p&gt;5.如果因为某些原因，获取锁失败（没有在半数以上实例取到锁或者取锁时间已经超过了有效时间），客户端应该在所有的Redis实例上进行解锁，无论Redis实例是否加锁成功，因为可能服务端响应消息丢失了但是实际成功了，毕竟多释放一次也不会有问题&lt;/p&gt;
&lt;h3 id=&#34;个人想法&#34;&gt;个人想法&lt;/h3&gt;
&lt;p&gt;能不用分布式锁就不用分布式锁，避免引入新的复杂度，对于需要使用锁的场景，优先基于中间件原子性的机制操作。&lt;/p&gt;
&lt;p&gt;MySQL数据库，加上version字段，强制要求所有update语句带上&lt;code&gt;set version=version+1 where version={old_version}&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;可能重复insert的场景，对合理的业务id加上唯一索引，由数据库自有机制保证不会有重复数据插入&lt;/p&gt;
&lt;p&gt;秒杀，统计等场景，使用Redis的incr,decr语句来替代分布式锁操作库存&lt;/p&gt;
&lt;br&gt;
&lt;hr&gt;
&lt;h2 id=&#34;持久化&#34;&gt;持久化&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;RDB 持久化可以在指定的时间间隔内生成数据集的时间点快照（point-in-time snapshot）。（适合冷备）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;AOF 持久化记录服务器执行的所有写操作命令，并在服务器启动时，通过重新执行这些命令来还原数据集。
AOF 文件中的命令全部以 Redis 协议的格式来保存，新命令会被追加到文件的末尾。（适合热备）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Redis 还可以在后台对 AOF 文件进行重写（rewrite），使得 AOF 文件的体积不会超出保存数据集状态所需的实际大小。&lt;/p&gt;
&lt;p&gt;Redis 还可以同时使用 AOF 持久化和 RDB 持久化。
在这种情况下， 当 Redis 重启时， 它会优先使用 AOF 文件来还原数据集， 因为 AOF 文件保存的数据集通常比 RDB 文件所保存的数据集更完整。&lt;/p&gt;
&lt;p&gt;但实际上持久化会对Redis的性能造成非常严重的影响，如果一定需要保存数据，那么数据就不应该依靠缓存来保存，建议使用其他方式如数据库。所以Redis的持久化意义不大。&lt;/p&gt;
&lt;h3 id=&#34;rdb优缺点&#34;&gt;RDB优缺点&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;优点：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;他会生成多个数据文件，每个数据文件分别都代表了某一时刻Redis里面的数据，这种方式，适合做冷备。&lt;/p&gt;
&lt;p&gt;RDB对Redis的性能影响非常小，是因为在同步数据的时候他只是fork了一个子进程去做持久化的，而且他在数据恢复的时候速度比AOF来的快。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;缺点：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;RDB都是快照文件，都是默认五分钟甚至更久的时间才会生成一次，这意味着你这次同步到下次同步这中间五分钟的数据都很可能全部丢失掉。&lt;/p&gt;
&lt;p&gt;RDB在生成数据快照的时候，如果文件很大，客户端可能会暂停几毫秒甚至几秒。&lt;/p&gt;
&lt;h3 id=&#34;aof优缺点&#34;&gt;AOF优缺点&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;优点：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;RDB五分钟一次生成快照，但是AOF是一秒一次去通过一个后台的线程fsync操作，那最多丢这一秒的数据。&lt;/p&gt;
&lt;p&gt;AOF在对日志文件进行操作的时候是以append-only的方式去写的，他只是追加的方式写数据，自然就少了很多磁盘寻址的开销了，写入性能惊人，文件也不容易破损。&lt;/p&gt;
&lt;p&gt;AOF的日志是通过一个叫非常可读的方式记录的，这样的特性就适合做灾难性数据误删除的紧急恢复了，比如公司的实习生通过flushall清空了所有的数据，只要这个时候后台重写还没发生，你马上拷贝一份AOF日志文件，把最后一条flushall命令删了就完事了。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;缺点：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一样的数据，AOF文件比RDB还要大。&lt;/p&gt;
&lt;p&gt;AOF开启后，Redis支持写的QPS会比RDB支持写的要低。&lt;/p&gt;
&lt;h3 id=&#34;同步机制&#34;&gt;同步机制&lt;/h3&gt;
&lt;p&gt;Redis可以使用主从同步，从从同步。&lt;/p&gt;
&lt;p&gt;第一次同步时，主节点做一次bgsave，并同时将后续修改操作记录到内存buffer，待完成后将RDB文件全量同步到复制节点，复制节点接受完成后将RDB镜像加载到内存。&lt;/p&gt;
&lt;p&gt;加载完成后，再通知主节点将期间修改的操作记录同步到复制节点进行重放就完成了同步过程。&lt;/p&gt;
&lt;p&gt;后续的增量数据通过AOF日志同步即可，有点类似数据库的binlog。&lt;/p&gt;
&lt;br&gt;
&lt;hr&gt;
&lt;h2 id=&#34;高可用集群&#34;&gt;高可用/集群&lt;/h2&gt;
&lt;p&gt;Redis Sentinal 着眼于高可用，在master宕机时会自动将slave提升为master，继续提供服务。&lt;/p&gt;
&lt;p&gt;Redis Cluster 着眼于扩展性，在单个redis内存不足时，使用Cluster进行分片存储。&lt;/p&gt;
&lt;h3 id=&#34;gossip协议&#34;&gt;Gossip协议&lt;/h3&gt;
&lt;p&gt;Gossip算法又被称为反熵（Anti-Entropy），熵是物理学上的一个概念，代表杂乱无章，而反熵就是在杂乱无章中寻求一致&lt;/p&gt;
&lt;p&gt;这充分说明了Gossip的特点：在一个有界网络中，每个节点都随机地与其他节点通信，经过一番杂乱无章的通信，最终所有节点的状态都会达成一致。&lt;/p&gt;
&lt;p&gt;每个节点可能知道所有其他节点，也可能仅知道几个邻居节点，只要这些节可以通过网络连通，最终他们的状态都是一致的&lt;/p&gt;
&lt;p&gt;Redis Gossip消息分为消息头和消息体，消息体一共有4类，其中MEET、PING和PONG消息都用clusterMsgDataGossip结构来表示。&lt;/p&gt;
&lt;p&gt;随机周期性发送PING消息&lt;/p&gt;








  











&lt;figure id=&#34;figure-gossip协议下一种可能的消息传播过程&#34;&gt;


  &lt;a data-fancybox=&#34;&#34; href=&#34;https://w3xse7en.github.io/media/sql/redis/gossip.gif&#34; data-caption=&#34;Gossip协议下一种可能的消息传播过程&#34;&gt;


  &lt;img src=&#34;https://w3xse7en.github.io/media/sql/redis/gossip.gif&#34; alt=&#34;&#34;  &gt;
&lt;/a&gt;


  
  
  &lt;figcaption&gt;
    Gossip协议下一种可能的消息传播过程
  &lt;/figcaption&gt;


&lt;/figure&gt;

&lt;h3 id=&#34;redis的分片机制&#34;&gt;Redis的分片机制&lt;/h3&gt;
&lt;p&gt;Redis 集群没有使用一致性hash, 而是引入了哈希槽的概念。&lt;/p&gt;
&lt;p&gt;Redis Cluster 采用虚拟哈希槽分区，所有的键根据哈希函数映射到 0 ~ 16383 整数槽内，每个key通过CRC16校验后对16384取模来决定放置哪个槽(Slot)，每一个节点负责维护一部分槽以及槽所映射的键值数据。&lt;/p&gt;
&lt;p&gt;计算公式：slot = CRC16(key) &amp;amp; 16383。&lt;/p&gt;
&lt;p&gt;这种结构很容易添加或者删除节点，并且无论是添加删除或者修改某一个节点，都不会造成集群不可用的状态。使用哈希槽的好处就在于可以方便的添加或移除节点。&lt;/p&gt;
&lt;p&gt;当需要增加节点时，只需要把其他节点的某些哈希槽挪到新节点就可以了；&lt;/p&gt;
&lt;p&gt;当需要移除节点时，只需要把移除节点上的哈希槽挪到其他节点就行了。&lt;/p&gt;
&lt;h3 id=&#34;为什么rediscluster会设计成16384个槽呢&#34;&gt;为什么RedisCluster会设计成16384个槽呢？&lt;/h3&gt;
&lt;p&gt;1.如果槽位为65536，发送心跳信息的消息头达8k，发送的心跳包过于庞大。&lt;/p&gt;
&lt;p&gt;如上所述，在消息头中，最占空间的是 slots[CLUSTER_SLOTS/8]。 当槽位为65536时，这块的大小是: 65536÷8÷1024=8kb因为每秒钟，redis节点需要发送一定数量的ping消息作为心跳包，如果槽位为65536，这个ping消息的消息头太大了，浪费带宽。&lt;/p&gt;
&lt;p&gt;2.redis的集群主节点数量基本不可能超过1000个。&lt;/p&gt;
&lt;p&gt;如上所述，集群节点越多，心跳包的消息体内携带的数据越多。如果节点过1000个，也会导致网络拥堵。因此redis作者，不建议redis cluster节点数量超过1000个。 那么，对于节点数在1000以内的redis cluster集群，16384个槽位够用了。没有必要拓展到65536个。&lt;/p&gt;
&lt;p&gt;3.槽位越小，节点少的情况下，压缩率高&lt;/p&gt;
&lt;p&gt;Redis主节点的配置信息中，它所负责的哈希槽是通过一张bitmap的形式来保存的，在传输过程中，会对bitmap进行压缩，但是如果bitmap的填充率slots / N很高的话(N表示节点数)，bitmap的压缩率就很低。 如果节点数很少，而哈希槽数量很多的话，bitmap的压缩率就很低。&lt;/p&gt;
&lt;h3 id=&#34;redis数据增多了是该加内存还是加实例&#34;&gt;Redis数据增多了，是该加内存还是加实例？&lt;/h3&gt;
&lt;p&gt;这跟 Redis 的持久化机制有关系。&lt;/p&gt;
&lt;p&gt;在使用 RDB 进行持久化时，Redis 会 fork 子进程来完成，fork 操作的用时和 Redis 的数据量是正相关的，而 fork 在执行时会阻塞主线程。&lt;/p&gt;
&lt;p&gt;数据量越大，fork 操作造成的主线程阻塞的时间越长。所以，在使用 RDB 对 25GB 的数据进行持久化时，数据量较大，后台运行的子进程在 fork 创建时阻塞了主线程，于是就导致 Redis 响应变慢了。&lt;/p&gt;
&lt;h3 id=&#34;集群脑裂&#34;&gt;集群脑裂&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;min-replicas-to-write 3
min-replicas-max-lag 10
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要求至少3个slave节点，且数据复制和同步的延迟不能超过10秒。否则master就拒绝读写，这样发生集群脑裂原先的master节点接收到写入请求就会拒绝&lt;/p&gt;
&lt;h4 id=&#34;raft协议解决脑裂&#34;&gt;Raft协议解决脑裂&lt;/h4&gt;
&lt;p&gt;选举安全性，即在一个任期内最多一个领导人被选出，如果有多余的领导人被选出，则被称为脑裂（brain split），如果出现脑裂会导致数据的丢失或者覆盖。&lt;/p&gt;
&lt;p&gt;Raft通过下面两点保证了不会出现脑裂的情况；&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;一个节点某一任期内最多只能投一票；&lt;/li&gt;
&lt;li&gt;只有获得大多数选票才能成为领导人；&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;通过增加约束避免了脑裂的情况出现，保证了同一时间集群中只有一个领导者。&lt;/p&gt;
&lt;p&gt;但是当一个节点崩溃了一段时间，他的状态机已经落后其他节点很多，突然他重启恢复被选举为领导者，这个时候，客户端发来的请求再经由他复制给其他节点的状态机执行，就会出现集群状态机状态不一致的问题。&lt;/p&gt;
&lt;br/&gt;
&lt;hr&gt;
&lt;h2 id=&#34;参考&#34;&gt;参考&lt;/h2&gt;
&lt;p&gt;&lt;a href=&#34;https://itzones.cn/2020/06/30/Redis%E5%9F%BA%E6%9C%AC%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%E4%B9%8BHash/&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;Redis基本数据类型之Hash&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://juejin.cn/post/6844903446475177998&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;Redis 为什么用跳表而不用平衡树？&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://juejin.cn/post/6844903433716105224&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;Redis 内部数据结构详解 (2)——sds&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://itzones.cn/2020/07/11/Redis%E5%9F%BA%E6%9C%AC%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%E4%B9%8BZSet/&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;Redis基本数据类型之ZSet&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://juejin.cn/post/6844904039218429960&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;基于Redis的分布式锁和Redlock算法&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://github.com/javagrowing/JGrowing/blob/master/%E5%88%86%E5%B8%83%E5%BC%8F/%E5%86%8D%E6%9C%89%E4%BA%BA%E9%97%AE%E4%BD%A0%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%EF%BC%8C%E8%BF%99%E7%AF%87%E6%96%87%E7%AB%A0%E6%89%94%E7%BB%99%E4%BB%96.md&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;再有人问你分布式锁，这篇文章扔给他&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://www.jianshu.com/p/b52336ebfc43&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;漫谈Gossip协议与其在Redis Cluster中的实现&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://zhuanlan.zhihu.com/p/308641354&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;redis集群中的gossip协议&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;http://qyuan.top/2019/07/16/raft-1/&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;分布式一致性协议之Raft(二)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://www.bilibili.com/video/BV1Fi4y147ad&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;动画演示 raft 在脑裂发生之后仍然可以正常工作吗？&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
</description>
    </item>
    
  </channel>
</rss>
