不奢望岁月静好 只希望点滴积累

0%

都说innodb好、还要不要用memory引擎 ?

内存表的数据组织结构

innodb表的数据就放在主键索引树上、主键索引是 B+ 树、主键索引上是有序存储的, 在执行select * 时、会按照叶子节点从左到右扫描、得到的结果 0 出现在第一行

memory引擎的数据和索引是分开的. 主键id的索引里、存放的是每个数据的位置. 主键id是hash索引、索引上的key是乱序的. select * 也是全表扫描、也是顺序扫描数组、0就是最后一个被读到、并放入结果集的数据.

innodb把数据放到主键索引上、其它索引保存的是主键id、是索引组织表(Index Organizied Table)
memory引擎采用的是数据单独存放、是堆组织表(Heap Organizied Table)

两者之间的差异:

  1. innodb的数据总是有序存储的、内存表的数据是按照写入顺序存储的.
  2. 当数据文件有空洞的时候、innodb表在插入新数据时、为了保证数据有序性、只能在固定位置写入新值, 而内存表找到空位置就可以插入新值.
  3. 数据位置发生变化的时候、innodb表只需要修改主键索引、而内存表需要修改所有索引.s
  4. innodb表用主键索引查询时、需要走一次索引查询、普通索引查询时、要走两次索引查找.
    memory表无分别、所有的索引地位都是相同的.
  5. innodb支持变长数据类型、不同记录的长度可能不同、内存表不支持Blob和Text字段、并且即使定义varchar(N)实际也是char(N), 即固定长度存储、所以: 每行数据长度相同.

所以: 内存表每行数据被删除以后、空出的位置可以被接下来要插入的数据复用.
注意: 内存表的索引是hash索引、范围查询 select * from t1 where id<5 是无法用到主键索引的, 会全表扫描.

hash索引 和 B-Tree索引

内存表也可以支持B-Tree索引

1
alter table t1 add index a_btree_index using btree(id);

内存表优势是速度快. 一方面它支持hash索引、另一方面、它的数据都在内存; 那么、为什么不建议生产环境使用内存表呢 ?

  1. 锁粒度问题
  2. 数据持久化问题.

内存表的锁

内存表不支持行锁、只支持表锁、导致并发性太低.

数据持久性问题

数据放在内存中是内存表的优势、也是劣势、因为DB重启时、所有的内存表都会被清空.

M-S 架构场景

1) 业务正常访问s主库
2) 备库硬件升级、备库重启、内存表t1内容被清空
3) 备库重启后、客户端发送一条update语句、修改表t1的数据行、此时备库应用线程就会找不到要更新的数据.

双M架构

MySQL知道重启后、内存表的数据会丢失、所以担心主库重启后、出现主备不一致、在实现上、会在DB重启后写入一行 delete from t1的binlog记录.
备库重启时、备库的binlog里的delete语句就会传到主库、然后把主库内存表的内容删除, 出现主库内存表数据突然被清空的现象.

所以不适合在生产上使用.

  1. 若选择内存表是因为更新量大、那么并发度是重要的参考指标、innodb支持行锁、并发度更好
  2. 若考虑读性能、一个读QPS很高、且数据量不大的表、即使是innodb、数据也都是缓存在buffer pool的、因此innodb表性能也不差.

一个适合用内存表的场景

内存临时表: 不会被其它线程访问、无并发问题; 重启需要删除、清空数据问题不存在; 备库的临时表不影响主库用户线程, 所以刚好可以无视内存表的两个不足.

union执行流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
create table t1(id int primary key, a int, b int, index(a));

delimiter ;;
create procedure idata()
begin
declare i int;
set i=1;
while(i<=1000) do
insert into t1 values(i, i, i);
set i=i+1;
end while;
end;;
delimiter ;
call idata();

然后执行

1
(select 1000 as f) union (select id from t1 order by id desc limit 2);

获取两个子查询结果的并集(非重复).

使用explain分析发现:
在做union的时候、使用了临时表(Using temporary)

执行流程如下:

  1. 创建一个内存临时表、只有一个整型字段f、且f是主键字段
  2. 执行第一个子查询、得到1000这个值、写入临时表
  3. 执行第二个子查询:
    获取id=1000, 插入临时表、由于主键冲突、插入失败、取id=999.
  4. 从临时表按行取出数据、返回结果、并清除临时表、结果中包含的两行数据 1000 和 999.

若union改成union all、无需去重、就不需要临时表了.

group by执行流程

1
select id%10 as m, count(*) as c from t1 group by m;

explain的extra字段、可以看到三个信息:
using index, 表示语句使用了覆盖索引、选择了索引a、不需要回表;
uusing temporary, 表示使用了临时表
using filesort, 表示需要排序.

SQL执行流程:

  1. 创建内存临时表、表里有两个字段 m 和 c, 主键是 m;
  2. 扫描表t1的索引a、依次取出叶子节点上id的值、计算 id%10的结果、记为x;
    若临时表没有主键为x的行、就插入一个记录(x,1);
    若表中有主键x的行、就将c的值+1;
  3. 遍历完成后、根据字段m做排序、得到结果集返回给客户端.

若实际需求不需要排序、可以加上 order by null. 这样就会跳过排序阶段.

group by优化方法 – 索引

不论是内存临时表还是磁盘临时表、group by逻辑都需要构造一个带唯一索引的表、执行代价较高.
group by为什么需要临时表呢 ? group by是统计不同值出现的次数, 但每一行的 id%100 的结果是无序的、所以需要一个临时表、记录统计结果. 如果保证 出现的数据是有序的呢 ?
就不需要额外排序了. 索引, 可以通过添加冗余字段、记录 id%10 的值、并在该列 添加索引来优化.

四、group by优化方法 – 直接排序
若碰到无法使用加索引完成group by的逻辑、就只能老老实实排序, 此时 group by如何优化呢 ?
若明知group by 语句的结果集很大、依然要 先存到内存临时表、插入部分数据后、发现临时内存不够再转成磁盘临时表、这样倒不如直接使用磁盘临时表.

在group by中加入 sql_big_result 可以告诉优化器、结果集很大、直接使用磁盘临时表就好. 磁盘临时表使用的是 B+ 树存储、存储效率不如数组. 既然数据量大、那不如使用数组存储.

1
select sql_big_result id%100 as m, count(*) as c from t1 group by m;

执行流程如下:

  1. 初始化 sort_buffer、确定放入一个整型字段m;
  2. 扫描t1的索引a、依次取出id的值、将id%100 的值、存入sort_buffer;
  3. 扫描完成后、对sort_buffer的字段m做排序(若sort_buffer不够用、就利用磁盘临时文件辅助排序)
  4. 排序完成、得到有序数组.

那么、MySQL什么时候会使用内部临时表呢 ?

  1. 若语句执行过程是一边读数据、一边直接得到结果、是不需要额外内存的、否则就需要额外内存存储中间结果.
  2. join_buffer是无序数组、sort_buffer是有序数据、临时表是二维表结构.
  3. 若执行逻辑需要用到二维表特性、就要优先考虑临时表.

思考:

为什么同样是使用 order by null. 内存临时表得到的结果 0在最后一行、而使用磁盘临时表得到的结果、0在第一行 ?

内存表: 指的是使用Memory引擎的表、创建语法是: create table … engine=memory. 这种表的数据都保存在内存里、系统重启的时候会被清空, 但表结构还在. 而、临时表是可以使用各种引擎类型. 若使用的是innodb引擎或者mysiam引擎、写数据的时候是写到磁盘的、当前, 临时表也可以使用memory引擎.

临时表的特性

  1. 创建语法是: create temporary table…

  2. 一个临时表只能被创建它的session访问、对其它线程不可见. (A线程创建的临时表t、B线程不可见). 在session结束时自动删除.

  3. 临时表可以与普通表同名

  4. session A内具有同名的临时表和普通表时、show create 语句、及增删改查语句、访问的是临时表

  5. show tables. 不显示临时表.

临时表的应用

分库分表的跨库查询.

一般分库分表的场景、就是要把一个逻辑上的大表分散到不同的数据库实例上. eg. 将大表ht、按照字段f、拆分成1024个分表、然后分布到32个DB实例

一般有两种查询场景. 1) 可直接定位分表. 2) 无法定位分表、需要多表数据重组.

此时一般有两种思路解决:

1) 在proxy层进行代码排序

优势: 处理速度快、拿到分库的数据后、直接在内存中参与计算. 但:

开发工作量大, eg. group by, join 等的操作、对中间层开发要求高.

对proxy的压力较大、容易出现内存不够用和CPU瓶颈问题.

2) 将各表拿到的数据、汇总到一个MySQL实例的一个表、然后在汇总实例上进行逻辑操作.

join优化的场景

1) 不同session临时表命名是可以重复的、若有多个session同时执行join优化、无需担心表名重复建表失败.

2) 无需担心数据删除问题. 若使用临时表、在执行流程过程中Client异常断开、或者DB异常重启、还需要专门清理中间过程生成的数据表、而临时表会自动回收、不需要这个额外的操作.

为什么临时表可以重名 ?

在执行 create temporary table tmp(id int primary key)engine=innodb; 这个语句时、Mysql会给innodb表创建一个 frm 文件保存表结构定义、还要有地方保存表数据. frm放在临时文件目录下 #sql{进程id}_{线程id}_序列号、 select @@temdir 可以查看临时文件目录.

5.6之前、MySQL会在临时文件目录下创建一个相同前缀、.ibd 为后缀的文件、用来存放数据文件;

5.7开始、MySQL引入临文件表空间、专门存放临时文件数据、不需要创建ibd文件.

其实创建一个叫t1的innodb临时表、MySQL在存储上认为跟普通表t1不同、可以与普通表重名.

MySQL维护数据表、除了物理上要有文件外、内存里也有一套机制区别不同的表、每个表对应一个table_def_key. 普通表的table_def_key是库名 + 表名得到、而临时表的table_def_key是库名 + 表名 + server_id + thread_id, 所以, 临时表不能重名而临时表是可以的.

在实现上、每个线程都维护了自己的临时表链表. 这样每次session内操作表的时候、先遍历链表、检查是否有对应临时表、有: 优先操作、无: 再操作临时表; session结束时、对链表里的每个临时表都执行 drop temporary table + tb_name的操作. 此时会发现: binlog也记录了 drop temporary操作

```

SET TIMESTAMP=1584267384/!/;

SET @@session.pseudo_thread_id=19/!/;

DROP /*!40005 TEMPORARY */ TABLE IF EXISTS x,tmp

```

临时表只有在县城内可以访问、为什么要写binlog呢 ? –这就需要考虑主备复制了.

临时表和主备复制.

既然写binlog, 就意味着备库需要. eg. 下边的语句序列

1
2
3
4
5
6
7
create table t_normal(id int primary key, c int)engine=innodb; // Q1

create temporary table tmp_t like t_normal; // Q2

insert into tmp_t values(1,1); // Q3

insert into t_normal select * from tmp_t; // Q4

若关于临时表的操作不记录、在备库重放binlog时、就会报错: tmp_t不存在

如果把binlog设为row格式能解决吗 ? binlog是row时、记录的是操作的数据 插入一行数据(1,1). 确实如此、若binlog_format=row、那么跟临时表有关的操作不会记录到binlog、

即: 只有binlog_format=statement / mixed时、binlog才会记录临时表的操作.

此时, 创建临时表的语句会传到备库执行、备库同步时也会创建临时表、所以、也需要在主库上记录 drop temporary table、传给备库.

MySQL在记录binlog时、不论是create 还是 alter都会原样不变、但drop确会变成

1
drop table `t_normal` /*generated by server*/

也就是改成了标准格式. 为什么呢 ?

因为 drop是可以一次删除多个表. eg. 若设置 binlog_format=row, 若主库执行 drop table t_normal,tmp_t, 则binlog只记录 drop table t_normal; 因为备库无tmp_t、备库执行会出错、所以, 必须将语句改写.

思考:

备库如何处理不同线程同名临时表

MySQL在记录binlog时、会把线程id写入binlog、这样备库的应用线程就能知道每个语句的主库线程id、并利用这个线程id来构造临时表的table_def_key.

临时表可使用 alter修改表名、不能使用rename ?

因为在rename table时、是按照 db/table.frm 的规则去磁盘找文件、但临时表在磁盘上的frm文件是放在tmpdir目录下的、并且命名规则是: #sql{进程id}_{线程id}_序列号.frm, 所以会找不到.

为什么还要kill不掉的语句 ?

1
2
3
4
Mysql 中有两个kill命令: 
kill query + 线程id; // 终止线程中正在执行的语句;
kill connection + 线程id; // 断开线程的连接、里边若有语句郑州执行、也会停止执行
那么什么情况下、会出现: 使用了kill命令、未断开连接、show processlist显示的是killed状态的现象呢 ?

收到kill以后、线程做什么 ?

kill 若直接终止、退出、会导致原来占有的锁无法释放、所以: kill 不会马上停止、而是告诉线程可以执行停止的逻辑了. 实际上执行kull query thread_id_B时、处理了两件事:

  1. 将session B的运行状态改成 THD::KILL_QUERY(将killed赋值为THD::KILL_QUERY);

  2. 给session B的执行线程发一个信号.

kill不掉的场景

线程并发数不够

innodb_thread_concurrency 不够用、线程未执行到判断线程逻辑的状态.

此时command列会显示killed, 但实际上服务端该语句还在执行中. 为什么不会直接退出呢 ?

1) 执行kill query时、

在实现上、等行锁、使用的是pthread_cond_timedwait函数、这个等待状态可以被唤醒. 虽然线程状态已经被置为kill_query, 但在这个等待进入innodb的循环过程中、并未判断线程状态、所以不会进入终止逻辑

2) kill connection

将线程状态置为 kill_connection、关掉线程的网络连接、所以被kill的session收到断开连接的提示. 为什么command列会显示为killed呢 ?

show processlist 有一个特别逻辑: 若线程状态为 kill_connection、就显示为 killed.

所以、即使Client退出了、线程状态仍然是等待中、直到满足进入innodb的条件后 session C才继续执行、然后才有可能判断到线程状态已经变成了kill_query 或者 kil_connection状态、再进入终止逻辑.

终止逻辑耗时长.

此时从processlist结果列上来看也是Killed、需要等终止逻辑完成、才算真正完成. 场景:

1) 超大事务执行期间被Kill、此时回滚事务需要对事务执行期间生成的所有数据版本做回收操作、耗时很长

2) 大查询回滚、若查询过程中生成了较大的临时文件、加上此时文件系统压力大、删除临时文件可能需要等待IO资源、导致耗时较长

3) DDL命令执行到最后阶段、若被Kill、需要删除中间过程的临时文件、可能也要受到IO资源耗时久的影响.

直接在客户端Ctrl+C是否可以终止线程呢 ?

不可以. 客户端只能操作客户端线程、Client和Server通过网络交互、不可能操作Server线程.

而由于MySQL是停等协议、所以、线程执行的语句还未返回的时候、再往连接发命令也是无效的. 实际上、Ctrl+C时、Mysql Client是另外启动一个连接、发送 kill query 命令.

两个关于Client的误解

若库里包含的表特别多、连接就会很慢.

实际上、每个Client和Server建立连接时、需要做的事情就是 TCP握手、用户校验、获取权限、与库里表的数量无关. 但: 使用默认参数连接时、Mysql Client会提供本地库名和表名补全功能. 为了实现这个功能、Client在连接成功后、需要多一些操作:

1) show databases;

2) 切到db1、执行show tables;

3) 将两个命令的结果构建一个本地hash表.

最耗时的是操作(3). 所以、我们感知到的连接过程慢、其实不是连接慢、也不是Server慢、而是Client慢. 若不使用 或者很少使用 自动补全的话、建议默认加 -A

其实-quick(-q) 也可以跳过这个阶段. 但这个参数可能会降低Server性能. Mysql 发送请求后、接收Sever返回结果的方式有两种:

1) 本地缓存: 在本地开一片内存、将结果缓存、若使用API开发、对应:mysql_store_result.

2) 不缓存: 读一个处理一个、若使用API、对应: mysql_use_result.

MySQL默认采用本地缓存、若加上-quick会采用第二种. 此时、若本地处理的较慢、就会导致服务端发送结果被阻塞、让服务端变慢.

为什么叫 -quick呢 ?

1) 可以跳过表名自动补全

2) mysql_store_result需要申请本地内存来缓存查询结果、若结果太大、会耗费较多本地内存、影响Client本机性能.

3) 不会把执行命令结果记录到本地的命令历史文件.

所以: -quick 是让Client变得更快.

思考:

若碰到一个被Killed的事务一直处于回滚状态、应该把Mysql进程强制重启、还是让它执行完呢 ?

因为重启后依然会继续回滚操作、所以 从恢复速度的角度来说、应该让它自己执行结束.

若这个语句可能会占用别的锁、或者由于占用IO资源过多、影响到别的语句执行、就需要做主备切换、切到新的主库提供服务. 切换之后别的线程都断开了连接、自动停止执行, 接下来还是等它自己执行完成(减少系统压力、加速终止逻辑).

误删数据库后除了跑路、还能怎么办 ?

使用delete误删数据行

可以使用Flashback工具把数据恢复. 原理是: 修改binlog的内容、拿回原库重放. 前提是: 确保 binlog_format=row 和 binlog_row_image=FULL.

单行事务处理:

insert: binlog event的类型 Write_rows event -> Delete_rows event

delete: 反之

update: 对调binlog位置即可

多行事务:

(A)delete … (B)insert … (C)update …

写回的顺序应该是:

(revert C)update … (revert B)delete … (revert A)insert …

但: 以上方案只能作为万一的紧急预案、平时应该尽可能的做到提前预防、建议:

把sql_safe_updates 参数设置为 on、这样一来、若忘记在delete或者update时写where条件、或者where条件中不包含索引字段的话、SQL执行会报错

代码上线前、必须经过SQL审计

注意:

\1. delete 全表是很慢的、需要生成undo log、写 redo log、binlog、从性能角度来看、应该优先考虑 truncate table或者drop table.

\2. truncate/drop table, drop database,即时配置binlog_format=row、执行这3个命令、记录的还是statement

使用drop table或者truncate table误删数据表

这种情况就要使用全量备份 + 增量日志的方式了, 必须要有定期的全量备份, 并实时备份binlog才行. 两者都具备时、恢复流程:

  1. 取最近一次全量备份、假设是一天一备、上次备份是当天凌晨

  2. 用备份恢复出一个临时库

  3. 从日志备份里、取出0点之后的日志

  4. 把这些日志、除了误删数据的语句之外、全部应用到临时库.

注意:

  1. 为了加快数据恢复、若临时库有多个数据库、可在使用mysqlbinlog时、加上-database指定库.

  2. 应用日志时、需要跳过误操作语句的binlog、

    非gtid: –stop-position执行到误操作之前的日志、–start-position再从它之后开始操作

    gtid: 假设误操作的gtid是gtid1, set gtid_next=gtid1;begin;commit; 将gtid1先加入gtid集合、之后按顺序执行binlog时、会自动跳过误操作的语句.

不过、即使这样、mysqlbinlog方法恢复数据还是不够快、因为:

  1. mysqlbinlog不能只解析其中一个表的日志

  2. mysqlbinlog解析出日志、应用日志的过程只能是单线程.

一种加速方法是: 在用备份恢复出临时实例之后、将这个临时库设为线上备库的从库、这样:

  1. 在start slave之前、先通过执行 change replication filter replication_do_table = tb、就可以让临时表只同步误操作的表

  2. 也可以用上并行复制、加速数据恢复

使用drop database误删数据库

搭建延迟备库:

虽然可以通过并行复制来加速恢复数据、但恢复时间依然不可控, 若一个库的备份特别大、或者距离上一个全量备份的时间较长、恢复时间就特别长、若核心业务是无法等待这么久的、可以考虑搭建延迟复制的备库(MySQL 5.6引入)

change master to master_delay=n, 让备库与主库有Ns的延迟.

eg. 设为3600s、代表若主库数据被删、且在1h内发现、其它备库已同时被删除时、可到延迟复制备库执行 stop slave、跳过误删除目录、恢复出需要的数据.

预防误删库的方法:
  1. 账号分离、避免写错命令

  2. 指定操作规范. 避免写错要删除的表名. 先重命名待删除表、业务无影响再操作删除

使用rm命令、误删数据库实例

对一个有HA机制的Mysql集群、只要不恶意删除整个集群、只删除单节点的话、HA系统会开始工作、选出一个新的主库、保证集群正常工作. 这时、需要把节点数据恢复、再接入整个集群.

建议: 尽量把备份跨机房、最好跨城市保存.

导出markdown文件

首先登陆简书后台, 下载所有源文件

1
我 -> 设置 -> 账号管理 -> 下载所有文章

将源文件替换成我们需要的格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/bin/bash

# 简书文件存放路径
src=/Users/xx/Downloads/jianshu
dsc=/Users/xx/Downloads/jianshu-m
date=$(date "+%Y-%m-%d")

for f in $src/*/*; do
base=$(basename $f .md)
catgories=$(basename $(dirname $f) $PWD)
insert="---\ntitle: ${base}\ndate: $date\ncategories:\n - ${catgories}\ntags:\n - ${catgories}\n---\n"
echo "${insert}$(cat $f)" > $f # 将hexo需要的头部内容插入
lower=$(echo $catgories|tr '[A-Z]' '[a-z]') # 可根据个人需要、这里是为了生成指定分类开头的文件名
srcFile=$dsc"/"$lower"_"$base".md"
mv $f $srcFile # 将源文件放到一个文件夹下、统一copy到 _post 文件夹下即可
done

临时解决简书图片链接无法加载

1
2
3
4
5
6
7
8
找到主题文件夹下 -> layout文件夹下 -> 有 <head> 标记的文件、
eg. hexo-theme-next主题为:
themes/hexo-theme-next/layout/_layout.swig

添加meta:
<meta name="referrer" content="no-referrer"/>

^.^仅为权宜之计、不想盗链...., 有好的替换方式后更新....

本地新增文件图片链接

1
2
3
4
利用电脑上的各种工具、剪切的图片直接粘贴到 Typora上、会有 copy至 选项, 直接copy到我们的资源目录(你存放image的地方)即可
eg. 如果使用默认配置、应该是 source/images/
若开启 post_asset_folder: true 则每篇文章的图片会单独存放在一个与文件同名的文件夹内
![1.jpg](/images/image-20200320205834941.png)

在线安装

这种方式相对比较简单, idea -> Preference -> Plugins, 在跳出界面 Marketplace 选项卡下输入leetcode, 就可以看到 leetcode editor这个插件, 点击 Install 安装后重启即可

离线安装

https://plugins.jetbrains.com/plugin/12132-leetcode-editor

下载插件安装包、idea -> Preference -> Plugins, 设置(齿轮型⚙)的图标, 点击选择 Install from Disk, 找到刚刚下载的插件安装, 然后重启即可

(PS: 我一般喜欢把插件包放在统一目录下、防止文件清理时误删、放哪里不影响使用)

使用

https://plugins.jetbrains.com/plugin/12132-leetcode-editor 可以参考官方说明.

0.0 需要先注册leetcode账户, 将idea与leetcode关联起来. 然后、在右侧导航那里就可以看到 leetcode的小图标了.

image.png

点击图中1处: 展开leetcode设置面板,
点击图中2处: 设置leetcode账户关联

账户设置.png
其中:

1) URL要选择你注册的地址 leetcode-cn.com 是中文leetcode网站、
leetcode.com 是英文版的网站、与注册要一致、不然会登录失败
2) Code Type 是编程语言
3) LoginName 和 Password 就是leetcode的账号、密码了~

点击图中3处: 登录leetcode账户
点击图中4处: 拉取题目列表

代码模版设置.png

代码模版设置:

1
2
3
4
5
6
7
8
9
10
${question.content}

public class P${question.frontendQuestionId}$!velocityTool.camelCaseName(${question.titleSlug}) {
public static void main(String[] args) {
Solution solution = new P${question.frontendQuestionId}$!velocityTool.camelCaseName(${question.titleSlug});
// todo
}

${question.code}
}

这样就可以愉快的用起来了~

Java中元注解有4个:@Retention, @Target, @Document, @Inherited

@Retention注解的保留位置:
@Retention(RetentionPolicy.SOURCE) 注解仅存在于源码中、在class字节码文件中不存在
@Retention(RetentionPolicy.CLASS) 默认保留策略、注解会在class文件中存在、但、运行时无法获得
@Retention(RetentionPolicy.RUNTIME) 注解会在class文件中存在、并且可以通过反射得到

@Target 注解的作用目标
@Target(ElementType.TYPE) 接口、类、枚举、注解
@Target(ElementType.FIELD) 字段、枚举的常量
@Target(ElementType.METHOD) 方法
@Target(ElementType.PARAMETER) 方法参数
@Target(ElementType.CONSTRUCTOR) 构造函数
@Target(ElementType.LOCAL_VARIABLE) 局部变量
@Target(ElementType.ANNTATION_TYPE) 注解
@Target(ElementType.PACKAGE)

@Document 说明该注解将会被包含在javadoc中

@Inherited 说明子类可以继承父类中的该注解

一、实现HystrixConcurrencyStrategy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class MdcHystrixConcurrencyStrategy extends HystrixConcurrencyStrategy {

@Override
public <T> Callable<T> wrapCallable(Callable<T> callable) {
return new MdcAwareCallable(callable, MDC.getCopyOfContextMap());
}

private class MdcAwareCallable<T> implements Callable<T> {
private final Callable<T> delegate;

private final Map<String, String> contextMap;

public MdcAwareCallable(Callable<T> callable, Map<String, String> contextMap) {
this.delegate = callable;
this.contextMap = contextMap != null ? contextMap : new HashMap();
}

@Override
public T call() throws Exception {
try {
MDC.setContextMap(contextMap);
return delegate.call();
} finally {
MDC.clear();
}
}
}
}

二、注册一个插件

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class HystrixConfig {
//用来拦截处理HystrixCommand注解
@Bean
public HystrixCommandAspect hystrixAspect() {
return new HystrixCommandAspect();
}

@PostConstruct
public void init() {
HystrixPlugins.getInstance().registerConcurrencyStrategy(new MdcHystrixConcurrencyStrategy());
}
}

三、mq producer 和 consumer之间的trace跟踪

正常情况下、用msgId就可以了、有些数据分析需要trace来全局跟踪

  1. 在生产者的消息属性中加入traceId

    1
    2
    3
    SendResult result = null;
    Message msg = new Message(topic, tag, key, val.getBytes());
    msg.putUserProperty("traceId", MDC.get(TraceConst.TRACE_KEY));
  2. 在消费者中取出traceId、放入MDC

    1
    2
    3
    4
    5
    MessageExt msgExt = msgs.get(i);
    String msgId = msgExt.getMsgId();
    String msgBody = new String(msgExt.getBody());
    MDC.put(TraceConst.TRACE_KEY, msgExt.getUserProperty("traceId"));
    logger.info("msgs-size={}, msgId={}, msgBody={}", msgs.size(), msgId, msgBody);
  3. 部分源码
    image.png

image.png

文章参考
https://my.oschina.net/u/3748347/blog/1823392
https://wanghaolistening.github.io/2019/01/26/Hystrix-%E5%9C%A8%E9%A1%B9%E7%9B%AE%E4%B8%AD%E7%9A%84%E5%BA%94%E7%94%A8/

CPU 的术语介绍

内存屏障: memory barriers 一组处理器指令、用于实现对内存操作的顺序限制

缓冲行: cache line cpu高速缓冲中可以分配的最小存储单位
处理器填写缓冲行时、会加载整个缓存行、现代CPU需要执行几百次CPU指令

原子操作: atomic operations 不可中断的一个或者一系列操作

缓冲行填充: cache line fill当处理器识别到从内存中读取的操作数是可缓存的,
处理器读取整个高速缓存行到适当的缓存(L1,L2,L3或者所有)

缓存命中: cache hit如果进行高速缓存行填充操作的内存位置仍然是下次处理器访问的
地址时、处理器从缓存行读取而不是从内存读取

写命中: write hit 当处理器将操作数写回到内存缓存时、会先检查这个缓存的内存地址
是否在缓存行中、如果存在一个有效的缓存行、处理器会将这个操作数歇会到缓存、而不是内存

写缺失: write misses the cache 一个有效的缓存行被写入到不存在的内存区域

比较并交换: Compare and Swap cas操作需要两个值, old and new、在操作期间会比较这
两个值、如果发生变化则不交换、未发生变化才交换

CPU流水线: CPU pipeline在CPU中由5、6个不同的电路单元组成一条指令处理流水线
然后一条X86指令分成5~6步后再由这些电路单元分别执行、就能实现在一个CPU周期内完成
一条指令、提高CPU的运算速率

内存顺序冲突: memory order violation一般是由假共享引起的、是指多个CPU同时修改
同一个缓存行的不同部分而引起其中一个CPU的操作无效、当出现这个内存顺序冲突时、CPU必须清空流水线

Lock前缀的指令

1
2
3
lock 前缀的指令、在多核处理器下会引发两件事:
1. 将当期处理器的缓存行数据写回内存
2. 这个写回内存操作会使在其它CPU里缓存了该地址的数据无效

volatile的实现

1
2
3
4
5
6
7
8
9
为了提高速度、CPU不直接和内存通信、而是先将系统内存的数据加载到内存缓存后再操作
但: 操作完全不知道何时会被写回内存

volatile声明的变量进行读写、JVM会向处理器发送一条Lock前缀的指令、精讲变量所在的
缓存行数据写回到内存

在多核处理器下、实现了缓存一致性协议、每个处理器通过嗅探在总线上传播的数据来检查
自己的缓存是否已过期、当处理器发现自己缓存行对应的内存地址呗修改、会将当期处理器的
缓存行状态设为无效、重新从系统内存读取