redis设计与实现笔记5-事件

redis服务器需要处理的事件可以分为两类:

  1. 文件事件(file event): 套接字的抽象。服务器通过套接字与客户端或其他服务端连接、通信;
  2. 时间事件(time event): 定时任务。

文件事件

事件模型是基于Reactor开发的:

  1. 套接字;
  2. IO多路复用程序;
  3. 事件分派器;(dispatcher)
  4. 事件处理器。

每当一个套接字准备好执行:
连接应答(accept)、
写入、
读取、
关闭
等操作时,就会产生一个事件(可能并发)。

IO多路复用程序:把上游并发的事件组织成一个队列(方便下游单线程地使用)。
当上一个套接字事件被处理完以后,IO多路复用程序才会向事件分派器传送下一个套接字事件。

IO多路复用

具体啥是IO多路复用?就是一个进程处理多个连接。
方案有很多:(详见:http://xiaoyue26.github.io/2017/11/06/2017-11/epoll%E7%9B%B8%E5%85%B3%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86/)

  1. 循环、轮询;
  2. Select;
  3. poll;
  4. epoll(红黑树)/kqueue(哈希表);
  5. libevent库。

事件优先级

先处理可读,再处理可写。

API接口

1
2
3
4
5
6
7
1. createFileEvent(套接字描述符,事件类型,事件处理器):开始监听;
2. deleteFileEvent(套接字描述符,事件类型): 取消监听;
3. getFileEvents(套接字描述符): 返回被监听的事件类型;
4. wait(套接字描述符,事件类型,超时时长(ms)): 等待事件;
5. apiPoll(超时时长): 等待所有被监听事件直至至少一个发生;
6. processEvent():等待事件,然后分派;
7. getApiName: 返回底层使用的IO库(epoll,poll或select等)

时间事件

  1. 定时事件:指定时间后执行1次;
  2. 周期事件:每隔指定时间就执行1次(总N次)。

时间事件属性:

  1. id: 全局唯一,递增;
  2. when: 毫秒,事件到达时间;
  3. timeProc: 函数,到期执行。

定时事件和周期事件区分:

  1. timeProc返回AE_NOMORE: no more事件,不再调用;
  2. timeProc返回30: 周期事件,30ms后再次调用。

TODO: 全局唯一id=>服务器内唯一?

实现

所有时间事件放在一个无序链表中,每次遍历整个链表,查找所有已到达的时间事件。

性能

由于时间事件很少(1,2个),所以虽然实现很naive,性能也还行。

现有时间事件

serverCron:(每100ms)

  1. 更新统计信息:时间、内存、数据库占用;
  2. 清理过期KV;
  3. 关闭清理失效客户端;
  4. AOF\RDB持久化;
  5. 主从同步;
  6. 集群模式:定期同步、连接测试。

事件循环

事件无抢占。
先文件事件后时间事件,因此时间事件一般会滞后一点。

redis设计与实现笔记4-持久化

  1. RDB: 定期(比如五分钟)做一次快照。 (Redis DataBase)压缩的二进制文件;
  2. AOF: 类似于WAL,存每条操作指令日志。(Append only file)

题外:
mmkv: 内存映射文件(MMF),场景是频繁刷盘,对丢失敏感前提下的尽量高性能。

相关命令

  1. SAVE: 直接停服保存;
  2. BGSAVE: 开一个子进程在后台保存。
    检测到RDBAOF文件后,redis进程会自动载入文件。(优先AOF,因为AOF一般更新更频繁)

BGSAVE执行期间,不接受类似指令如SAVE,BGSAVE,BGWRITEAOF
载入文件期间,服务器阻塞不接受任何指令。

自动BGSAVE

1
2
3
save 900 1 # 900秒内至少1次修改
save 300 10 # 300秒内至少10次修改
save 60 10000 # 60秒内至少10000次修改

实现

1
2
3
4
5
6
7
8
struct redisServer{
// 记录自动bgsave的条件
struct saveparam *saveparams;
// 修改计数器:
long long dirty;
// 上一次执行保存的时间(unixtimestamp):
time_t lastsave;
}

有了上述3个变量,可以保存需要的状态;
再加上每100ms执行的serverCron函数,就能完成自动保存的工作了。

RDB持久化

RDB文件结构(第6版RDB格式)

  1. "REDIS": 常量字符;
  2. db_version: 4B,版本号;
  3. database:所有保存的数据库(kv数据),长度不定;
  4. EOF: 1B,表示数据结束;(377)
  5. check_suim: 8B,前4个部分校验和。

database部分结构

  1. SELECTDB: 1B,常量,区别于EOF;
  2. db_number: 1B,2B,5B不等,具体的数据库号;
  3. key_value_pairs: kv数据、过期时间,长度不等。

key_value_pairs部分结构

  1. EXPIRETIME_MS(可选):1B,过期时间标记,常量;
  2. ms(可选): 8B,具体过期时间(毫秒);
  3. TYPE: 1B,类型,标记value的具体类型;
  4. key: 总是字符串编码;
  5. value: 值的编码根据TYPE字段判断。

其中TYPE的取值,一种对象类型或底层编码:

1
2
3
string,list,set,zset,hash
,list_ziplist,set_intset,zset_ziplist
,hash_ziplist

服务器根据这个TYPE字段决定如何解析value字段的编码。

实例

1
2
3
4
5
# 以ascii码查看:
od -c ./home/dump.rdb

0000000 R E D I S 0 0 0 6 377 334 263 C 360 Z 334
0000020 362 V

其中377表示EOF,redis5B,版本4B,EOF1B,校验和8B,总共18B.

1
2
3
4
5
# 以16进制查看:
od -x ./home/dump.rdb

0000000 4552 4944 3053 3030 ff36 b3dc f043 dc5a
0000020 56f2

第11章AOF持久化

三个步骤:

  1. append:命令追加到redis的内存缓冲区;
  2. 文件写入(不刷盘):从redis的内存缓冲区到操作系统的内存页缓存;
  3. fsync: 文件同步(实际刷盘);

    写入和同步

    写入文件:实际上操作系统不会立即刷盘,而且处于性能先放内存缓冲区,后续再一起刷盘;
    同步:调用fsyncfdatasync函数刷盘。

实现

1
2
3
4
struct redisServer{
// AOF缓冲区
sds aof_buf;
}
  1. 执行写命令;
  2. 将写命令追加到aof_buf缓冲区的末尾;
  3. 每次结束一个事件循环前,考虑是否将aof_buf内容写入同步到aof文件;(根据appendfsync选项)

appendfsync选项

appendfsync选项 操作
always 将aof_buf内容写入且同步到AOF文件
everysec(默认值) 将aof_buf内容写入AOF文件,每秒同步一次
no 将aof_buf内容写入AOF文件,同步由操作系统决定

AOF载入还原

用一个伪客户端执行所有aof命令即可。

AOF重写(AOF文件压缩)

由于AOF文件记录的是命令而不是实际数据,可能会非常膨胀。
为了减少空间,同时加快载入恢复速度,可以根据实际数据进行压缩,跳过合并多余的命令。
方法类似于RDB,通过读取实际的数据库状态,生成AOF命令。

性能优化

出于对缓冲区的保护,如果遇到数据库中数据量太多的集合、有序集合、列表、哈希表,会让每条命令插入的数据量<=REDIS_AOF_REWRITE_ITEMS_PER_CMD(默认为64)。

BGREWRITEAOF细节(尽量不停服)

调用子进程进程AOF重写期间,并不停服,为了保证一致性,会把这期间的命令追加到两个缓冲区:AOF缓冲区、AOF重写缓冲区。

重写完成后,stop the world:

  1. AOF重写缓冲区=>AOF重写文件;
  2. AOF重写文件替换旧AOF文件;(利用文件系统的改名原子性)
  3. 恢复服务。

redis设计与实现笔记3-数据库

架构:

server-> db* -> dict*(k/v)

1
2
3
4
5
6
typedef struct redisServer {
// ...
redisDb *db;
int dbnum;
// ...
} redisDb;

数据库键空间

1
2
3
4
5
6
7
8
typedef struct redisDb {
// ...
// 数据库键空间,保存着数据库中的所有键值对
dict *dict;
// 过期时间:
dict *expires;
// ...
} redisDb;

db默认有16,客户端可以通过select 1命令来选择1号数据库。
每个client会保存当前在哪个db的状态变量。

键:Key

key只能为字符串。
整个架构最多三层: db->hashtable->value

key过期

过期删除策略

  1. 定时统一删除 (一般不用,卡太久)
  2. 惰性删除(默认使用,访问时发现过期才删除)
  3. 定期删除(默认使用,类似于增量删除,每次删除最近过期的)

redis持久化策略

  1. RDB: 定期(比如五分钟)做一次快照; (Redis DataBase)
  2. AOF: 类似于WAL,存每条操作指令日志。(Append only file)

RDB和AOF下的过期删除

  1. RDB: 主服务器生成快照时删除过期key; 从库则保留。从库只有收到主库DEL指令时候才会删除过期key。
  2. AOF: 保存DEL指令即可。
1
从库如果没有收到主库的DEL指令,即使已经key已经过期,也会返回给客户端。

主从复制要点:(中心化,一致性)
从库只接受主库指令,自己只执行

通知机制

两个功能:

  1. 订阅某个key的所有操作; (只能知道发生了什么类型的操作,不知道操作数)
  2. 订阅某个库下的所有DEL操作.(或者别的什么操作)
    换句话说就是订阅某个库下key,或者订阅指令。

高性能mysql附录A-E-笔记

高性能mysql附录A-笔记-mysql分支

mysql的三个分支(变种):

  1. Percona Server: 透明、性能、灵活,用XtraDB引擎代替innodb;
  2. MariaDB: mysql原作者。面向客户、补丁插件扩展更多;
  3. Drizzle:sql语法不兼容mysql, 修正bug,最开源。

官方mysql: 最接近于Percona Server。

高性能mysql附录B-笔记-服务器状态

系统变量: show variables like '%xxx%';
只读状态: show status like '%xxx%'; 或infomation_schema.global_status表和information_schama.session_status表;
其他信息:infomation_schema库中

线程和连接统计

1
2
3
connections,max_used_connections,threads_connected
bytes_received,bytes_sent
slow_launch_threads,threads_cached,threads_created,threads_running

二进制日志状态

1
binlog_cache_use,binlog_cache_disk_use

命令计数器

Com_*变量统计每种类型的SQL或C API命令发起过的次数。

1
2
Com_select: select语句的数量;
Com_change_db: 更改默认数据库的次数(use xxx);

临时文件和表

隐式表和文件:

1
show global status like 'Created_tmp%'

显式临时表:

1
show global temporary tables;

查看select查询类型统计

1
2
3
4
5
6
7
8
9
10
show global status like 'Select%';
+------------------------+----------+
| Variable_name | Value |
+------------------------+----------+
| Select_full_join | 677 |
| Select_full_range_join | 0 |
| Select_range | 135124 |
| Select_range_check | 0 |
| Select_scan | 16623726 |
+------------------------+----------+

按预期次数从多到少/开销从少到多的顺序:
Select_range: 在第一个表上扫描一个索引区间的连接数目;
Select_scan: 扫描整个第一个表;
Select_full_range_join: 开销多于Select_scan;
Select_range_check: 开销非常高;
Select_full_join: 开销最高。

表锁

Table_locks_immediate: 立即授权的表锁次数;
Table_locks_waited: 需要等待的表锁次数。

innodb状态

查看innodb相关的状态开销很大,会创建一个全局锁。

1
2
3
1. 通过show engine innodb status;
2. 通过information_schema表;
3. 通过show status,show variables

因此不能频繁查看这些变量。

输出信息包括:

  1. fsync()平均每秒调用次数;
  2. 头部信息: 时间;
  3. Semaphores: 操作系统等待数组,等待互斥量的innodb线程;(如果有等待,可以看出热点是什么)
    1
    2
    3
    waited at buf0buf.ic for 0 second: 等待缓冲区
    waiters flag 0: 0个线程在等待;
    waiting is ending: 等待结束。

innodb_sync_spin_loops变量:
空转多少次后停止spin,挂起进入真正等待。

1
2
3
4
5
6
7
show variables like '%spin%';
+------------------------+-------+
| Variable_name | Value |
+------------------------+-------+
| innodb_spin_wait_delay | 6 |
| innodb_sync_spin_loops | 30 |
+------------------------+-------+

死锁

SHOW ENGINE INNODB STATUS\G的输出还有一部分是上两次死锁的情况。(包括进程id和sql、等待的是什么锁)

死锁类型:

  1. 循环等待;
  2. 等待关系图太深:
    (1)检查超过100W个锁;
    (2)重做超过200个事务。
    错误信息:”TOO DEEP OR LONG SEARCH IN THE LOCK TABLE WAITS-FOR GRAPH”.

减少死锁的TIPS

在事务里更新数据时,先按主键排序,这样扫描索引的顺序就都是一致的;

事务信息

SHOW ENGINE INNODB STATUS\G的输出里还有一部分是事务总结信息和当前活跃事务信息。

事务总结信息

  1. 当前事务ID;
  2. 已经清理MVCC行的事务ID;(可以知道有多少老版本数据没被清理)
  3. 历史记录的长度;
  4. 锁结构的数目(可能包含多个行锁)。

活跃事务信息

  1. 进程id(与show processlist中id通用)
  2. 内部查询号;
  3. 连接信息
  4. 查询的sql。

File I/O

SHOW ENGINE INNODB STATUS\G的输出里还有一部分是IO辅助线程的状态和性能计数器的状态。
其中:

1
2
3
4
insert buffer thread: 插入缓冲合并到表空间;
log thread: 异步刷日志;
read thread: 预读操作;
write thread: 刷脏缓冲。

redo log统计

SHOW ENGINE INNODB STATUS\G的输出里还有一部分是事务日志(redo log)的统计。

  1. sequence number: 当前日志序号;
  2. flushed up to : 当前刷到哪里;
  3. last checkpoint: 上一个检测点的位置。
  4. pending和done的日志操作数量。

缓冲池和内存

SHOW ENGINE INNODB STATUS\G的输出里还有一部分是BufferPool和内存统计。
包括信息:

  1. 分配了多少字节;
  2. pool总共多少页,其中free多少页;
  3. database用了多少页,多少页已经修改;
  4. 命中率等统计信息。
    多个缓冲池的话后头还有各个buffer pool各自的统计信息。

ROW OPERATIONS

SHOW ENGINE INNODB STATUS\G的输出里最后一部分是行操作统计。

1
2
3
4
5
6
7
8
9
--------------
ROW OPERATIONS
--------------
0 queries inside InnoDB, 0 queries in queue
15 read views open inside InnoDB
Main thread process no. 9004, id 139699662354176, state: sleeping
Number of rows inserted 1111442430, updated 2610575313, deleted 42594651, read 8977633484
98.47 inserts/s, 1108.97 updates/s, 0.00 deletes/s, 1671.23 reads/s
----------------------------

包括信息:

  1. 多少线程在innodb内核内,多少在等待队列;
  2. 累积增删改查;
  3. 增删改查的瞬时速度。

主备相关查看命令

1
2
3
4
5
show master status \G
show binary logs;
show binlog events ...
show relaylog events
show slave status

Information_schema中的innodb信息表

innodb_trx,innodb_locks

事务、拥有和等待锁的事务。

performance_schema

高性能mysql附录C-笔记-大文件传输

大概是ssh,tar,gz,rsync命令的运用,略。

高性能mysql附录E-笔记-explain

explain语句:

  1. explain select xxx: 近似的执行计划信息;
  2. 近似的执行计划信息+等效sql:
    1
    2
    explain extended select xxx;
    show warnings; -- 这里得到的sql是从执行计划反向翻译过来的sql
  3. explain partitions select xxx: 显示查询的分区。(仅对分区表有效)

mysql5.6前,explain的时候会执行子查询创建临时表,以便进行外层优化。

explain各列的含义

id

select语句顺序编号,对应在原始语句中的位置;(从外到内)

select的三种类型:

  1. 简单子查询;
  2. 复杂子查询(派生表);
  3. union查询。

其中union查询的id列为null,select_type列为union result

select_type列

取值含义:

1
2
3
4
5
6
simple:  简单查询;
primary: 复杂查询的最外层;
subquery: select列表中的子查询;(不在from子句中);
derived: from子句中的子查询;
union: union语句中第二个和随后的select;
union result: 从匿名临时表检索结果的select.

table列

访问的表或别名。
或者< derivedN>,其中N是子查询的id。
union行中table列出现的< union2,3>其中2,3也是子查询的id。

type列

访问类型:从差到好:

1
2
3
4
5
6
7
all: 扫全表
index: 按索引顺序扫全表(避免排序) -- extra列的using index表示覆盖索引
range: 有范围限制的索引扫描
ref: 索引查找,查找某个索引值;
eq_ref: 唯一索引查找,结果只有一行;
const,system: 可以优化成常量替换;
NULL: 可以优化到无需访问表和索引。

possible_keys列

优化早期尝试的索引,可能无用。

key列

决定采用的索引。

key_len列

采用索引的可能最大长度。

ref列

在索引查找值时使用的列或常量。

rows列

估算大概要读取的行数。

filtered列

explain extended select xxx时有的列。
符合条件行数的悲观估计。

Extra列

包括的值:

1
2
3
4
5
using index: 使用覆盖索引;
using where: 从存储引擎返回后,需要在服务器再过滤一次;(可能暗示查询可以加更多索引)
using temporary: 查询结果排序时会使用临时表;
using filesort: 排序时会使用外部索引排序,而不是按索引顺序扫描;
range checked for each record: 没有理想的索引,对于连接后的每一行重新检查使用哪个索引(很慢)。

可以用Percona Toolkitpt-visual-explain获得树形执行计划。

高性能mysql附录E-笔记-锁的调试

锁的类型:

  1. 表锁;
  2. 全局锁: flush tables with read lock或设置read_only=1
  3. 命名锁:表锁的一种,重命名或者删除表时创建;
  4. 字符锁:可以用get_lock等函数在服务器级别锁住/释放单个字符。

表锁

显式

1
2
3
lock tables film read;
lock tables film write;
unlock tables ...;

隐式

1
2
select sleep(30) from film limit 1
-- 相当于lock tables film read;

可以用mysqladmin debug命令检测锁的持有信息。(输出的末尾)

全局读锁

show processliststatuswaiting for release of readlock时,就是等待全局读锁了。

命名锁

show processliststatuswaiting for table时,就是等待命名锁了。
还可以在show open tables中看到命名锁的影响。

用户锁

1
2
-- 试图获得名为`my_lock`的锁,超时时间100秒。
select get_lock('my_lock',100);

使用information_schema查看锁

https://dev.mysql.com/doc/refman/5.6/en/innodb-information-schema-examples.html
哪个事务在等待锁,哪个事务持有锁:

1
2
3
4
5
6
7
8
9
10
11
12
SELECT
r.trx_id waiting_trx_id,
r.trx_mysql_thread_id waiting_thread,
r.trx_query waiting_query,
b.trx_id blocking_trx_id,
b.trx_mysql_thread_id blocking_thread,
b.trx_query blocking_query
FROM information_schema.innodb_lock_waits w
INNER JOIN information_schema.innodb_trx b
ON b.trx_id = w.blocking_trx_id
INNER JOIN information_schema.innodb_trx r
ON r.trx_id = w.requesting_trx_id;

查看阻塞查询的线程元凶:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
SELECT
CONCAT('thread ', b.trx_mysql_thread_id, ' from ', p.host) AS who_blocks,
IF(p.command = "Sleep", p.time, 0) AS idle_in_trx,
MAX(TIMESTAMPDIFF(SECOND, r.trx_wait_started, CURRENT_TIMESTAMP)) AS max_wait_time,
COUNT(*) AS num_waiters
FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS AS w
INNER JOIN INFORMATION_SCHEMA.INNODB_TRX AS b ON b.trx_id = w.blocking_trx_id
INNER JOIN INFORMATION_SCHEMA.INNODB_TRX AS r ON r.trx_id = w.requesting_trx_id
LEFT JOIN INFORMATION_SCHEMA.PROCESSLIST AS p ON p.id = b.trx_mysql_thread_id
GROUP BY who_blocks ORDER BY num_waiters DESC;

+-------------------------+-------------+---------------+-------------+
| who_blocks | idle_in_trx | max_wait_time | num_waiters |
+-------------------------+-------------+---------------+-------------+
| thread 4 from localhost | 98 | 12 | 1 |
+-------------------------+-------------+---------------+-------------+

结果看线程4空闲了98秒,至少有一个线程等了它12秒,有1个线程在等待它。

高性能mysql第12-16章-笔记

这一章主要讲了一些概念和一些Mysql Cluster变种,略。

高可用概念

5个9: 99.999%,每年允许5分钟宕机时间。

宕机原因

  1. 运行环境: 磁盘耗尽;
  2. 性能问题:糟糕sql,schema索引设计;
  3. 复制问题:主备不一致;
  4. 数据丢失:如误操作。

tips

  1. 禁用查询缓存
  2. 禁用复制过滤器、触发器。

高性能mysql第13章-笔记-云端mysql

IaaS:基础设施虚拟化;
DaaS:数据库虚拟化。

Iaas:

mysql需要的4种资源:

  1. cpu: 云cpu一般较慢;
  2. 内存: 最好能到512GB~1TB;
  3. IO: ms级,虚拟化后一般慢100倍;
  4. 网络: 一般不成为瓶颈。

Daas:

稍微讲了一下亚马逊RDS,略过。

高性能mysql第14章-笔记-应用层优化

思考的角度

  1. 应用和数据库正确分工;
  2. ORM的循环SQL和嵌套查询的性能优劣;
  3. 缓存(redis);
  4. 连接池。

web服务器问题

静态资源: 用nginx代替apache
移除无用的apache模块

缓存问题

不同等级层次的缓存:

  1. 进程缓存;
  2. 本地共享内存缓存;
  3. 分布式内存缓存;(memcached)
  4. 磁盘缓存。

缓存失效策略

  1. TTL;
  2. 显式失效:更新数据时候更新缓存或者标记缓存为脏数据;
  3. 读时失效:读的时候再判断缓存是否已经过期; (加快写,减慢了读)

高性能mysql第15章-笔记-备份与恢复

备份建议:

  1. 使用Percona XtraBackup、Mysql Enterprise Backup;
  2. 生产服务器和备份服务器分开,以免同时挂掉。
  3. 定期进行恢复测试。

逻辑备份

(SQL,mysqldump)
缺点:

  1. 恢复起来慢

物理备份

(文件)
缺点:

  1. 受版本、兼容性影响,bug概率高。
  2. 文件很大。

建议:
0. 全局使用utf-8;

  1. 逻辑:使用mysqldump备份结构;
  2. 物理: 使用select into outfile导出数据为分隔符文件。
  3. 恢复结构:执行1中的sql;
  4. 恢复数据:load data infile
    其中2,4两步对字符集有要求。(不能单独设置某列字符集)

增量备份和全量备份

尽量做全备,增量bug多;

数据一致性

mysqldump --single-transaction
可能会导致非常长的事务,从而失败。

binlog清理

1
purge master logs before current_date - Interval N day

或者设置expire_logs_days

innodb损坏恢复

1. 二级索引损坏

3种办法:

  1. optimize table语句;
  2. 删除重建表;
  3. 表引擎改为myisam,再改回来。

2. 聚簇索引损坏

优先使用备份还原
否则:
(大概率只能修复未受损坏影响的行。)
通过innodb_force_recover选项导出表,如果导出过程崩溃,需要跳过受损行。

3. 损坏系统结构

损坏: innodb事务日志(redo log),表空间撤销日志(undo log),数据字典。

优先使用备份还原
否则:
可能需要做整个数据库的导出和还原。
innodb内部绝大部分工作可能受影响。

上述2,3两种损坏最好从备份还原数据。

高性能mysql第16章-笔记-用户工具

这章主要介绍一些工具:

UI工具:

  1. MysqlWorkbench;
  2. SQLyog: 同workbench,windows专用;

命令行工具:

  1. Percona Toolkit: 管理员必备;
  2. The openark kit:一些管理任务的python脚本。

监控工具

  1. Nagios;
    2-6等其余很多略过。

高性能mysql-第十一章-笔记-可扩展

可扩展

  1. 数据量;
  2. 用户量: 并发量。
  3. 负载。

可扩展性的描述

  • 线性扩展: 加1倍资源=>加1倍性能。(理想最优情况,一般达不到)

  • Amdahl定律:

  1. 存在无法并发的工作;
  2. 存在需要交互的工作;
    因此吞吐量达不到线性增长,会趋于平缓。
  • USL扩展:(通用可扩展定律)
  1. 存在无法并发的工作;
  2. 存在需要交互的工作;
  3. 存在内部系统通信开销。

从3种扩展性的描述看,我们的目标是趋近于线性扩展,因此应该尽量削减USL扩展中的三个方面: 减少串行、减少交互、减少内部通信。

垂直扩展(纵向扩展、向上扩展)

升级硬件。价格更高,性能比低。
mysql利用极限:
CPU: 24
内存: 128
更多的时候提升趋于平缓。

向内扩展

优化查询、索引,压缩数据。

水平扩展(横向扩展,向外扩展)

  1. 复制
  2. 拆分
  3. 分片(sharding)

拆分: 按功能拆分(微服务),划分的表之间不会进行关联操作

数据分片(sharding)

用userid分片。
id+hash。

星形/雪花模型: 容易分片。
网状模型: 难以分片。

因此早期设计应该避免网状模型。

分片方式:

  1. 固定分配,硬编码; 缺点: 无法处理热点。优点: 简单。
  2. 动态分配:缺点:实现复杂。

热点解决方案:随机数+哈希;
扩展性: 预分配32个分片,用DNS单点,把4个库放在一个实例(IP)上。

小猿(CP): 库名_32.表名; DNS作为单点。
网易(CP): 类似codis,引入中间件单点。

全局唯一ID

  1. autoinc: 利用自增幅度和初始偏移;
  2. 创建一个全局表,进行autoinc id分配;
  3. 使用memcached/redis,生成全局自增id;(没有持久性)

其中2,3可以批量生成一批,减少调用次数。

冷热数据分表

归档、冷数据分离到另一张表、或者将冷数据分离到较差的节点。

负载均衡的目的

  1. 可扩展性;(读写分离)
  2. 高效性;
  3. 可用性;
  4. 透明性;
  5. 一致性。

负载均衡算法

  1. 随机
  2. 轮询
  3. 最少连接数: 有缺点,新加入的机器还没有预热缓存,却立即分配了最多的连接;
  4. 最快响应;
  5. 哈希:通过源IP进行哈希(会话局部性);
  6. 权重:结合上述算法,根据底层机器的性能差异进行权重。

具体采用哪个算法,取决于工作负载类型。

高性能mysql第九/十章-笔记-选硬件/主备复制

这章主要讲怎么挑mysql的硬件。

CPU

多核的

磁盘

raid卡,带电,防断电的

iostat和vmstat用法

高性能mysql第十章-笔记-复制

使用场景:

  1. 数据分布;
  2. 负载均衡(从库可以读);
  3. 备份;
  4. 高可用/故障切换;
  5. 升级测试。 主库备库同步流程:
  6. 主库: 生成binlog;(binlog dump线程)
  7. 备库: 读取binlong放入中继日志relay log;(通过TCP/IP协议,IO线程)
  8. 备库: 重放relay log。(单线程,SQL线程)

配置复制

  1. 每台服务器上创建账号;
  2. 配置主库和备库;
  3. 通知备库连接到主库,并从主库复制数据。

第一步创建账号

主库、备库:

1
2
grant replication slave, replication client on *.*
to repl@'192.168.0.%' indentified by 'p4ssword',;

第二步设置主库和备库

主库:
[my.conf]:

1
2
log_bin = mysql-bin
server_id = 10

重启主库。
检查主库完成配置:

1
2
3
4
5
6
7
show master status;

+------------------+-----------+--------------+------------------+-------------------+
| File | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
+------------------+-----------+--------------+------------------+-------------------+
| mysql-bin.000611 | 618598987 | | | |
+------------------+-----------+--------------+------------------+-------------------+

备库配置:
[my.conf]

1
2
3
4
5
log_bin = mysql-bin
server_id = 2
relay_log = /var/lib/mysql/mysql-relay-bin
log_slave_updates = 1 # 备库自身的重放事件也记录到binlog中
read_only = 1

第三步:启动复制

备库执行sql:

1
2
3
4
5
change master to MASTER_HOST= 'server1'
,MASTER_USER='reply'
,MASTER_PASSWORD='p4ssword'
,MASTER_LOG_FILE='mysql-bin.000001'
,MASTER_LOG_POS=0;

检查备库配置:

1
show slave status \G

让备库开始复制:

1
start slave;

binlog配置

主库配置

sync_binlog=1: 事务提交前把binlog存到磁盘上。

复制的原理

binlog_format:

  1. 基于语句的复制(逻辑): 出错机率大,开销小;
  2. 基于行的复制(物理): 几乎不出错,开销一般更大。(5.1以后开始有)

基于语句的复制的缺点:

  1. 不支持触发器或者存储过程:有bug;
  2. 只能串行重放。

基于行的缺点:

  1. 可读性差,无法直接知道执行了什么SQL。
    解决方案:
    1
    2
    3
    4
    -- 1:
    show master logs;
    -- 2:
    show binlog events in 'mysql-bin.000643' from 1073742091;

复制的过滤器

不建议使用。

复制的拓扑

1. 一主多备(星形模型)

2. 双主

不建议使用,两个主库都可写会有很多问题

3. HA模式(类似于双主,但只有一个可写)

两个库的配置基本相同,只是其中一个库是只读的。
如果有Alter table等耗时操作,可以在备库上执行,然后互换两个库。
优点:
故障恢复很简单。

具体设置情况:

  1. 两个库数据相同;
  2. 创建复制账号,选择不同serverid;
  3. 两者启用binlog,互相跟踪;
  4. 备库设置成只读,主库设置成可写。

由于serverid,主库不会重复消费自己的变更。(忽略自己的日志)

确定备库追上了主库

1
2
3
4
-- 主库:
show master status;-- 记录一下文件名和偏移量
-- 备库:
select master_pos_wait(file, pos[, timeout])

4. 环形(>=3的库成环)

不建议使用,太脆弱容易死循环。

5. 分发主库

一主多备对于备的数量有上限,可以用分发主库来进行扩展。
为了避免在分发主库上执行查询,可以将它的表修改为blackhole引擎。
(storage_engine=blackhole)

6. 模拟多主库

备库可以轮流读两个主库,俩主库带blackhole即可。

tips

1
2
3
4
5
insert ...
select ...
-- 转换成:
select into outfile ..
load data infile ...

这样更快(不需要加锁)

高性能mysql第八章-笔记-优化服务器配置

错误配置不如维持默认配置。

优化服务器配置

配置文件

服务端读取mysqld分段
客户端可能会读取客户端分段。

配置级别

全局、连接级、会话级
sort_buffer_size: 每个线程都可以设置
join_buffer_size: 可以为每个关联都设置一个关联缓冲。

tips

用git管理配置文件

最优配置

书上也不知道怎么配出最优配置。

  1. 用benchmark:穷举配置成本太高;
  2. 用比较低的值慢慢试探,较低的内存配置对性能影响不大(百分之几),但较高的内存配置会造成较大的性能抖动,因此尽量以较低的配置。

Buffer pool(Innodb)

缓存内容:

  1. 缓存索引;
  2. 缓存行数据;
  3. 缓存自适应哈希索引;
  4. 插入缓冲;(延迟合并写入)
  5. 锁。

Key caches(MyIsam)

键缓冲(键缓存)。
MyIsam只缓存索引,不缓存数据。

查看MyIsam的索引总大小:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
select sum(index_length) 
from information_schema.tables
where engine='MYISAM';
-- 也可以看看分布:
select engine,sum(index_length)
from information_schema.tables
GROUP BY engine;

+--------------------+-------------------+
| engine | sum(index_length) |
+--------------------+-------------------+
| CSV | 0 |
| InnoDB | 68176035840 |
| MEMORY | 0 |
| MyISAM | 124928 |
| PERFORMANCE_SCHEMA | 0 |
+--------------------+-------------------+

key cache的大小不要超过MYISAM索引总大小。

线程缓存

thread_cache_size: 缓存线程数量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
show variables like '%thread_cache_size%';
show variables like '%thread%';


+-----------------------------------------+---------------------------+
| Variable_name | Value |
+-----------------------------------------+---------------------------+
| innodb_purge_threads | 1 |
| innodb_read_io_threads | 12 |
| innodb_thread_concurrency | 0 |
| innodb_thread_sleep_delay | 10000 |
| innodb_write_io_threads | 12 |
| max_delayed_threads | 20 |
| max_insert_delayed_threads | 20 |
| myisam_repair_threads | 1 |
| performance_schema_max_thread_classes | 50 |
| performance_schema_max_thread_instances | -1 |
| pseudo_thread_id | 4804578 |
| thread_cache_size | 512 |
| thread_concurrency | 4 |
| thread_handling | one-thread-per-connection |
| thread_stack | 524288 |
+-----------------------------------------+---------------------------+

表缓存

对myisam有用,innodb用处不大。
算是一个已经过时的配置。

Buffer Pool相关架构

图中的undo日志是方便事务回滚的。
(redo日志是类似WAL,防断电的,利用顺序IO优化性能)

事务日志(redo log)使用类似环形数组的数据结构,可以环形写入到flush过的头部,节省存储空间。

Log Buffer(Redo log/事务日志)

每次变更: 记录到Log Buffer(Buffer Pool => Log Buffer)

Log Buffer刷盘到磁盘日志文件的时机(或者说触发事件):

  1. 每一秒钟(定时器tick事件);
  2. 或事务提交事件;
  3. 或缓冲区满。

以上是默认的三个事件,调节参数为innodb_flush_log_at_trx_commit,3种取值:
0: 事务提交:不触发;
1: 事务提交:刷盘到磁盘;(默认值,3种事件皆触发刷盘)
2: 事务提交:刷新到操作系统缓存。(能扛mysql进程挂掉,不能扛断电,断电的话会丢一秒数据。)

如果缓冲区满的太频繁(1s满了很多次),可以考虑增大缓冲区大小来减少IO次数。
参数:
innodb_log_buffer_size: 默认1M,推荐1~8M。

调节这个参数需要命令:

1
show engine innodb status

Innodb读写日志、数据文件配置

这是一个风险很高的选项,认真学习以后有必要再修改。

1
2
3
4
5
6
7
show global variables like '%flush_method%';

+---------------------+----------+
| Variable_name | Value |
+---------------------+----------+
| innodb_flush_method | O_DIRECT |
+---------------------+----------+

windows用的选项:
async_unbuffered,unbuffered,normal.

linux:

  • fdatasync: 默认值。用fsync()刷新数据和日志文件。
    fdatasync()函数只刷新文件的数据;
    fsync()函数刷新数据和日志文件。
    配置为fdatasync时其实会调用fsync()函数。默认会合并多个文件的fsync()操作,合并IO操作。可以通过innodb_file_per_table选项为每个文件独立fsync(),但也会增多IO操作。

  • O_DIRECT: 数据文件: 用O_DIRECT标记或directio()函数。
    (不影响日志文件)
    区别: 使用fsync()函数刷盘,但会通知操作系统不要缓存数据,也不要预读。
    相当于关闭了操作系统缓存(读写)。
    特例: 不影响raid卡的预读、写缓存。

  • ALL_O_DIRECT: 影响数据和日志文件。

  • O_DSYNC: 日志文件: 用O_SYNC标记,写同步(日志写到磁盘才返回)
    (不影响数据文件)
    区别:不禁用操作系统的缓存。

配置值 实际调用函数 概述
fdatasync fsync(含义相反) 刷新数据和日志,有操作系统缓存
O_DIRECT 刷新数据,关闭缓存
ALL_O_DIRECT 刷新数据和日志,关闭缓存
O_DSYNC O_SYNC 刷新日志,有操作系统缓存

O_DSYNC和fdatasync的区别:
(实际是O_SYNC和fsync的区别)fsync允许写操作累积在缓存,然后一次性刷新。
O_SYNC是每次都是同步IO,每次都刷盘。

tips

如果使用raid卡带电: O_DIRECT;
否则: O_DIRECT或fdatasync都有可能,看业务需求。

Innodb表空间

位于innodb_data_home_dir目录:

1
表、索引、回滚日志(undo log)、插入缓冲(Insert Buffer)、双写缓冲(DoubleWrite Buffer)。

目录下可以挂载多个设备,但不会自动做负载均衡,只有写满第一个才会写第二个,因此需要底层用raid自己做负载均衡。
挂载多个目录:

1
innodb_data_file_path = /disk1/ibdata1:1G;/disk2/ibdata2:1G;/disk3/ibdata3:1G:autoextended:max:2G

建议使用innodb_file_per_table选项,每个表的表空间一个文件。
缺点:DROP Table慢。
原因:

  1. 删除文件慢;
  2. 移除表空间需要Innodb锁定和扫描缓冲区,查找属于这个表空间的页。

运行时间长的事务

缺点: undo log增长到打爆表空间、磁盘。

双写缓冲(Doublewrite Buffer)

功能: 避免页没写完整导致的数据损坏。
buffer pool => Doublewrite Buffer => 磁盘

写两次,但由于有fsync(),而且这个过程时间顺序上比较紧密,可以一次性刷到磁盘,写两次的性能损耗也就多了百分之记。

binlog IO配置

sync_binlog: binlog配置。
N=0(默认值): 不干预,让操作系统自己决定。
N>0: N次写以后,刷新到磁盘。(一般不会大于1)

1
show global variables like '%log%';

innodb的6种日志:

  1. redo log(物理/WAL): 事务开始后写入缓冲区innodb_log_buffer;(存储引擎层)
  2. undo log(逻辑/版本): 事务开始后为了事务回滚准备的日志,从buffer pool=>表空间;
  3. bin log(逻辑/SQL): 主从复制。或者基于时间点的还原。(类似于存了sql)。(事务提交前产生,数据库层)
  4. slow query log: 慢查询;
  5. general log: 一般日志;
  6. relay log: 中继日志。

undo log(在表空间): 修改前的值/版本号;
undo log可以配置从表空间中分离出来。

redo log(在log buffer): 类似于WAL,记录buffer pool和disk的不同。

回滚事务的crash恢复

  • redo log: 只追加写,提交到磁盘以后标记为可以被新日志覆盖。
  • 基于这个特性,即使事务已经回滚了,它的redo log依然是存在的。
  • 再基于以上这点,db crash以后,可以恢复已经回滚的事务。

回滚事务的crash恢复,有两种策略:

  1. 读redo log,挑已经提交的事务进行恢复;
  2. 读redo log,恢复所有事务,再用undo log解决回滚的事务。

innodb使用的是策略2,也就是先redo,再undo。

使用策略2的难点:

  • 需要redo log相应的undo log是健全的,否则就漏回滚了。
    (1)解决方案1:确保undo log落盘早于redo log;(很麻烦,不用)
    (2)解决方案2:将undo log也视为数据,记录到redo log中。(innodb采用)

  • 总结

    crash恢复: 先读取执行redo log,再执行undo log,这样可以恢复未提交事务和回滚事务。
    undo log和redo log的数据一致性: 把undo log也视为数据,写入redo log。

日志与事务提交速度:

redo log: 不影响事务提交速度。因为是事务进行中就产生、而且每秒刷盘了;
binlog: 影响事务提交速度。因为事务提交前才产生binlog、刷盘,因此如果开启了binlog,长时间的事务提交很慢。

三种重要日志的写顺序:

  1. undo log;
  2. redo log;
  3. binlog.

MYISAM的IO配置

delay_key_write:
OFF: 不延迟。每次写操作后刷新键缓存;
ON: 延迟。对使用该选项创建的表生效;
ALL: 延迟。对所有表生效。
该选项对性能提升不大,缺点:

  1. 索引可能损坏;
  2. 关闭表时间变长;
  3. 占用空间变大。

myisam_use_mmap: 使用内存映射,减少系统调用开销。

Innodb并发

innodb_thread_concurrency:
N=0: 不限制有多少线程进入内核;
N>0: 同时可以有N个线程进入内核。
N的推荐值< 2cpu数disk数

innodb_commit_concurrency:
同时可以有多少个线程提交。

Myisam并发

concurrent_insert:
0: 不允许并发插入;
1: 默认值。只要表中没有空洞,就允许并发插入。
2: 强制并发插入到Myisam表末尾,即使有空洞。(碎片会更多)

基于工作负载的配置

  1. blob,text的表,临时表都会变成磁盘表。
    解决方案: 使用substr。

tmp_table_size,max_heap_table_size: 临时表大小上限。
max_connections: 最大连接数。
thread_cache_size: 缓存线程数。

查看配置和状态:

1
2
show variables like '%thread%';
show status like '%thread%';

innodb_autoinc_lock_mode
0: 每次都锁;
1: 确定需要分配多少自增值的话,可以立即释放锁;
2: 每次都不锁,空洞很多。

两个最重要的配置

innodb_log_file_size: redo log的缓冲区,如果调大了innodb_buffer_pool_size就要相应调大它。
(一种可能的值是51210241024,512M,搭配2G的innodb_buffer_pool_size)
可以通过show engine innodb status\G select sleep(60); show engine innodb status\G,查看Log sequence number差,计算1个小时的redo日志写入量,大致可以设置为这个缓冲区的大小。

innodb_buffer_pool_size的大小设置就比较简单了,按服务器可用内存75%设定即可。

高性能mysql第七章-笔记-高级特性

高级特性

略过: 分区表,合并表,全文索引

视图

视图会影响优化器

存储过程

在线系统: 不推荐使用;
离线系统: 如果代码很简单,为了批量性能可以考虑。

游标

游标使用临时表实现。

因此:

  1. 逐行只读;
  2. 会执行整个查询。

基于2,如果只读一小部分结果,请使用limit。
(也就是不要使用游标来实现limit的效果,直接使用limit语句即可)

prepareStatement

流程

  1. 客户端=>服务端: SQL原型
  2. 服务端: SQL原型=>部分执行计划A;
  3. 服务端=>客户端: A的句柄;
  4. 客户端=>服务端: 变量、A的句柄.

优点:

  1. 服务端: SQL解析只需解析/优化一次;
  2. 通信: 只发送参数+句柄,通信成本降低;
  3. 缓存: 参数可以被缓存;
  4. 安全: 无须转义、由于执行计划定了,不会被SQL注入。

prepareStatement的三类优化:
(和传入参数无关的)

  1. 准备阶段: 根据已知条件,where条件优化;
  2. 第一次执行: 简化嵌套关联,将外关联转化成内关联;
  3. 每次SQL执行:
    (1) 过滤分区;
    (2) 尽量移除count,min,max;
    (3) 移除常量表达式;
    (4) 检测常量表;
    (5) 做必要的等值传播;
    (6) 分析和优化ref,range和索引优化等访问数据的方法;
    (7) 优化关联顺序。

绑定变量

set @xxx:= 123
绑定变量是会话级的。

字符集和校对

字符集(encode): 二进制<=>某类编码字符
校对(collation):某字符集的排序规则。

校对集都是针对某一个字符集的。(类似于弱实体)

相关设置分为两类:

  1. 创建对象时候的默认值
  2. 服务器和客户端通信的设置

1. 创建对象时候的默认值

(1)character_set_server: 服务器创建数据库默认值;
(2)表字符集;
(3)列字符集.
越小范围的优先级越高。

2. 服务器和客户端通信的设置

服务端:

(1)character_set_client: 服务端总是假设客户端按照character_set_client设置的字符集来传输数据和sql语句;
(2)character_set_connection: 服务器收到客户端sql后,转换成character_set_connection类型;
(3)character_result:服务端返回数据时,转换成character_result类型。

客户端:

客户端: jdbc:mysql://localhost:3306/exceltest1?useUnicode=true&characterEncoding=UTF-8

客户端可以使用set names utf-8语句来告诉服务端自己将使用utf-8传输数据。

特殊情况

诡异的character_set_database: 某个数据库的默认字符集。当数据库改变的时候,它也会改变。当没有指定数据库的时候,会按照character_set_server

Load Data Infile:
数据库总是将文件中的字符按照字符集character_set_database来解析。
mysql加载数据的时候,总是以同一个字符集处理所有数据,不管表中的列是否有不同的字符集设定。

Select Into OutFile:
输出文件时不会自动做转码,也不能指定。唯一的方法是手动调用函数convert

查看字符集设置

1
SHOW VARIABLES LIKE 'character%';

查看mysql支持的校对集: show collation

一般校对集是字符集加上三种后缀:
_bin: 二进制比较;
_ci: 忽略大小写(ignore);
_cs: 大小写敏感(sensitive)

大小写敏感比二进制比较更加复杂,会有更多规则。

mysql如何选择字符集

每个字符集有对应的默认校对集;
每个校对集有对应的默认字符集。(一般来说,校对集依赖于字符集)

  1. 如果用户设置了: 那肯定按用户设置的来;
  2. 如果设置了一部分: 根据用户设置找默认的。
  3. 什么都没设置: 根据默认配置设置。

分布式(XA)事务

两阶段提交。
协调者+参与者。

  1. 所有参与者完成准备工作;
  2. 协调者收到所有参与者的回复,提交事务。

两种XA: 内部XA,外部XA。

内部XA

mysql是插件式的,因此每个存储引擎是互相独立,不知道彼此存在的。
他们之间协调需要分布式事务。(内部XA)

  • 跨存储引擎的事务需要内部XA。

如果开启了二进制日志,日志也可以看作一种特别的存储引擎,因此也需要内部XA。

外部XA

mysql对xa协议支持不完整,无法在一个XA中关联多个连接。
外部XA网络开销大,建议引入MQ解决,而不使用外部XA。

查询缓存

  1. 检查sql是不是select开头,如果数据源表没有更改,hash查找缓存,返回结果;
  2. 如果有确定性结果,存储查询语句的结果和hash值到缓存。

由于2(确定性结果),所以使用current_date这样的函数时,sql结果不会被缓存。

查询缓存的注意事项:

  1. 不能有长时间的事务;(会引入全局锁)
  2. 不要占用太大内存(过度依赖缓存),可以直接使用redis。

过度依赖缓存的话,缓存清理以后系统会假死很久。

查询缓存的配置参数

show variables like '%query_cache%'

query_cache_type

三种取值:

  1. OFF(0): 关闭,默认值。
  2. ON(1): 打开
  3. DEMAND(2): sql里写明sql_cachehint的语句才放入缓存。

query_cache_size: 查询缓存占的空间;
query_cache_limit: 缓存结果集最大条数;

清空缓存: reset query cache
整理缓存碎片: flush query cache(服务僵死一段时间)

半小时真正理解记住paxos协议

摘要:

简单浏览一遍流程,然后看半小时视频: https://www.bilibili.com/video/av36134550
就OK了。

paxos是分布式一致性协议,对于CAP的取舍是完全的C,较好的A,较好的P。
(关于CAP: http://xiaoyue26.github.io/2019/02/01/2019-02/简单解释CAP原理

paxos协商流程

两个角色

Proposer
Acceptor

流程

首先大致扫一遍下叙流程,第一遍大概率是完全懵逼看不懂的,花1分钟在脑中留下一点点印象即可:

准备阶段(prepare)Proposer申请epoch

第一阶段A:Proposer选择一个提议编号n,向所有的Acceptor广播Prepare(n)请求。

第一阶段B:若Acceptor接收到Prepare(n)请求,两个选择:
(1)n>接受过的n_old: 接受请求,返回(n_old,value_old)
(2)n<接受过的n_old: 拒绝请求。

接受阶段(提交阶段)

第二阶段A:Proposer得到了Acceptor响应:
1.如果未超过半数accpetor响应,直接转为提议失败;

2.如果超过多数Acceptor的承诺,又分为不同情况:
(1)如果所有Acceptor返回的value都是null,那么向所有的Acceptor发送(n,value_n);
(2)如果存在Acceptor返回的value不为null,那么从所有接受过的值中选择对应的n_old最大的作为提议的值value_old,提议编号仍然为n。但此时Proposer就不能提议自己的值,只能信任Acceptor通过的值,维护一但获得确定性取值就不能更改原则: 向所有Acceptor发送(n,value_old of max n_old)

第二阶段B:Acceptor接收到提议(n,value)后:
(1)n!=本地保存的last_n,拒绝。
(2)n=本地保存的last_n,写入本地值。

看教学视频:

https://www.bilibili.com/video/av36134550

由于之前花1分钟浏览了流程,视频看到15分钟的时候心里就会已经有数了。
看完30分钟就理解记住了。

温习要点

  1. 类似于CAS,仅当,当前值为null时,才能提议值为value。
  2. 类似于jdk里的hashmap扩容,当发现有人捷足先登的时候,放下陈见开始辅助n_old完成value_old的扩散。发送(n,value_old),以自己的名义,但是以别人的值,帮助一致性尽快达成。
  3. Acceptor对于epoch: 喜新厌旧,总是接受最大的epoch,防止Proposor单点故障死等。
  4. Proposor对于value: 喜旧厌新,总是接受更旧的value,尽快达成一致性。

以上就是Basic Paxos。其实依然会有活锁,需要引进leader缓解。leader挂了的时候退化到Basic Paxos,出现活锁几率提升。