首页 文章 精选 留言 我的

精选列表

搜索[团队协作工具],共10000篇文章
优秀的个人博客,低调大师

浅谈Redis - 热点key问题 | 京东云技术团队

热key问题描述 热key问题就是突然有几十万的请求去访问redis上的某个特定key,那么这样会造成流量过于集中,达到物理网卡上限,从而导致这台redis服务器直接宕机。 如何发现热点key 凭借业务经验,进行预估哪些是热key。比如某些商品要做秒杀,则商品key就可以判断为热key,但并非所有业务都能预估出热key。 在客户端进行收集。比如在redis客户端执行redis命令之前,加入一行代码进行命令数据收集,然后通过网络将收集的命令发送出去,缺点是对客户端代码有入侵。 在Proxy层做收集,但是并非所有的redis集群都有proxy。 用redis自带命令,monitor命令可以实时抓取出redis服务器接收到的命令,然后写代码统计出热key是啥,不过高并发条件下,有内存暴增的隐患,影响redis的性能。redis4.0.3提供了客户端热点key发现功能,如果key比较多,执行比较慢。 自己抓包评估,redis客户端使用TCP协议与服务端进行交互,通信协议采用RESP,自己写程序监听端口,按照RESP协议规则解析数据进行分析,不过开发成本较高,不易维护。 如何解决热key 增加二级缓存,发现热key以后,可以把热key数据加载到系统JVM并设置合适的缓存过期时间,针对热key的请求就会直接分散到各业务服务器上,防止所有请求同时访问同一台redis。 备份热key。可以把热点key的数据备份到所有redis的集群节点中,可以通过在热点key后面拼接集群节点编号,然后将这些备份key分散到所有集群节点中,客户端访问热点key的时候也在热点key后面随机拼接集群节点编号,将热点key的请求分散到不同集群节点上。 作者:京东零售曹志飞 来源:京东云开发社区 转载请注明来源

优秀的个人博客,低调大师

React请求机制优化思路 | 京东云技术团队

说起数据加载的机制,有一个绕不开的话题就是前端性能,很多电商门户的首页其实都会做一些垂直的定制优化,比如让请求在页面最早加载,或者在前一个页面就进行预加载等等。随着react18的发布,请求机制这一块也是被不断谈起,并且在后续其实也给出了明确的方向。 假如我们页面中有三个组件C1、C2、C3依次嵌套,每个组件中有对应的请求F1、F2、F3,通常大多数人会使用useeffect和state变量来实现数据流的获取,这样就意味着必须在组件加载时才能发起请求,所有数据获取都发生在组件的生命周期中,如下图所示,我们可以将这种获取数据的方式称作瀑布流渲染,但是这种方式有一个问题是,在这种方法中,组件之间的数据获取是按顺序进行的,这实际上意味着它们彼此阻塞,如果组件的层级嵌套很深的话,就会导致页面的加载时间特别长。 为了阻止组件间数据获取的这种顺序阻塞,另一种方法就是在组件加载前可以使用Promise.all获取所有的请求数据,这样在组件加载时就已经获取到所有的数据了。这种方式解决了组件加载顺序阻塞彼此数据流获取的问题,但是也产生了一个新的问题,在请求完成之前页面都会是空白的。 基于第二种先请求后渲染的方式,还可以使用Suspense将请求和渲染并行化,Suspense 可以使得组件可以“等待”某些操作结束后,再进行渲染。而这种方式如果要用到实际项目中,还需要考虑C1、C2、C3对应的请求写在哪里,如果写在组件中,那么加载还是慢的。如果拆分出来,就需要考虑文件拆分、code splitting等工程化问题。 在认真的分析了以上三种方式以后,发现各有优劣,同时基于上述方案,我也提供一个请求优化的思路,首先将请求的JS单独拆分出来打一个文件request.js,在html头部引入request.js并使用async属性让脚本并行执行(如下代码所示),这样既可以保证我们的请求在最开始就能发出,也能不阻塞后续React代码打包的js文件的执行,相当请求和渲染的代码在并行执行。 // html头部引入request.js <script async src="/js/request.js"></script> 在发送完请求之后,需要将返回的数据注入到React组件中,这部分怎么注入呢?简单的代码示例如下: // request.js 中请求部分 evt是发布订阅模式的一个实例 window.InitData = { data: null, on: (msg, fn) => evt.on(msg, e => fn(e)), emit: (msg, data) => evt.emit(msg, data), }; fetch().then(rs => { InitData.data = rs; InitData.emit("init_data", rs); return rs; }); // index.js react代码逻辑 ……. useEffect(() => { if (InitData.data !== null) { // 这里已经获取到了请求的返回值 doSomething(); } else { InitData.on("init_data", (data) => { // 利用发布订阅模式获取到数据 doSomething(); }); } }, []); ……. 总体思路就是在html中最先加载单独打包出来的请求文件并加入async属性使脚本并行执行,将返回的数据挂载到window下或者利用发布订阅模式将数据注入到react组件中。这样其实类似于边请求边渲染的模式,利用提前请求来减少加载的时间。后续也是希望能用工程化的方式去解决数据的请求机制。 未来的话在开发时肯定是更倾向于边请求边渲染的这种模式,在React的官网中也有说到未来计划让Suspense 处理更多的场景,包括获取数据等等,在新发布的React18中Suspense也是支持了服务端渲染,包括近期开源的remix也都在优化请求机制能够让应用更快的加载。也欢迎有兴趣的小伙伴一起来讨论和实践。 作者:京东零售梁峰峰 来源:京东云开发者社区

优秀的个人博客,低调大师

MySQL 执行计划详解 | 京东物流技术团队

1. EXPLAIN详解 本文基于MySQL 8.0编写,理论支持MySQL 5.0及更高版本。 2. EXPLAIN使用 2.1 explain分析SQL的执行计划 {EXPLAIN | DESCRIBE | DESC} tbl_name [col_name | wild] {EXPLAIN | DESCRIBE | DESC} [explain_type] {explainable_stmt | FOR CONNECTION connection_id} {EXPLAIN | DESCRIBE | DESC} ANALYZE select_statement explain_type: { FORMAT = format_name } format_name: { TRADITIONAL | JSON | TREE } explainable_stmt: { SELECT statement | TABLE statement | DELETE statement | INSERT statement | REPLACE statement | UPDATE statement } 2.2 示例 EXPLAIN format = TRADITIONAL json SELECT tt.TicketNumber, tt.TimeIn, tt.ProjectReference, tt.EstimatedShipDate, tt.ActualShipDate, tt.ClientID, tt.ServiceCodes, tt.RepetitiveID, tt.CurrentProcess, tt.CurrentDPPerson, tt.RecordVolume, tt.DPPrinted, et.COUNTRY, et_1.COUNTRY, do.CUSTNAME FROM tt, et, et AS et_1, do WHERE tt.SubmitTime IS NULL AND tt.ActualPC = et.EMPLOYID AND tt.AssignedPC = et_1.EMPLOYID AND tt.ClientID = do.CUSTNMBR; 2.3 结果输出展示 3. 结果解读 id:该语句的唯一标识。如果explain的结果包括多个id值,则数字越大越先执行;而对于相同id的行,则表示从上往下依次执行。 select_type:查询类型,有如下几种取值: table:表示当前这一行正在访问哪张表,如果SQL定义了别名,则展示表的别名 **partitions:**当前查询匹配记录的分区。对于未分区的表,返回null type:连接类型,有如下几种取值,性能从好到坏排序如下: ◦system:该表只有一行(相当于系统表),system是const类型的特例 ◦const:针对主键或唯一索引的等值查询扫描, 最多只返回一行数据. const 查询速度非常快, 因为它仅仅读取一次即可 ◦eq_ref:当使用了索引的全部组成部分,并且索引是PRIMARY KEY或UNIQUE NOT NULL 才会使用该类型,性能仅次于system及const。 -- 多表关联查询,单行匹配 SELECT * FROM ref_table,other_table WHERE ref_table.key_column=other_table.column; -- 多表关联查询,联合索引,多行匹配 SELECT * FROM ref_table,other_table WHERE ref_table.key_column_part1=other_table.column AND ref_table.key_column_part2=1; ◦ref:当满足索引的最左前缀规则,或者索引不是主键也不是唯一索引时才会发生。如果使用的索引只会匹配到少量的行,性能也是不错的。 -- 根据索引(非主键,非唯一索引),匹配到多行 SELECT * FROM ref_table WHERE key_column=expr; -- 多表关联查询,单个索引,多行匹配 SELECT * FROM ref_table,other_table WHERE ref_table.key_column=other_table.column; -- 多表关联查询,联合索引,多行匹配 SELECT * FROM ref_table,other_table WHERE ref_table.key_column_part1=other_table.column AND ref_table.key_column_part2=1; TIPS最左前缀原则 指的是索引按照最左优先的方式匹配索引。比如创建了一个组合索引(column1, column2, column3),那么,如果查询条件是: WHERE column1 = 1、WHERE column1= 1 AND column2 = 2、WHERE column1= 1 AND column2 = 2 AND column3 = 3 都可以使用该索引; WHERE column1 = 2、WHERE column1 = 1 AND column3 = 3就无法匹配该索引。 ◦fulltext:全文索引ref_or_null:该类型类似于ref,但是MySQL会额外搜索哪些行包含了NULL。这种类型常见于解析子查询 SELECT * FROM ref_table WHERE key_column=expr OR key_column IS NULL; ◦index_merge:此类型表示使用了索引合并优化,表示一个查询里面用到了多个索引 ◦unique_subquery:该类型和eq_ref类似,但是使用了IN查询,且子查询是主键或者唯一索引。例如:value IN (SELECT primary_key FROM single_table WHERE some_expr) ◦index_subquery:和unique_subquery类似,只是子查询使用的是非唯一索引。value IN (SELECT key_column FROM single_table WHERE some_expr) ◦range:范围扫描,表示检索了指定范围的行,主要用于有限制的索引扫描。比较常见的范围扫描是带有BETWEEN子句或WHERE子句里有>、>=、<、<=、IS NULL、<=>、BETWEEN、LIKE、IN()等操作符。 SELECT * FROM tbl_name WHERE key_column BETWEEN 10 and 20; SELECT * FROM tbl_name WHERE key_column IN (10,20,30); ◦index:全索引扫描,和ALL类似,只不过index是全盘扫描了索引的数据。当查询仅使用索引中的一部分列时,可使用此类型。有两种场景会触发: ▪如果索引是查询的覆盖索引,并且索引查询的数据就可以满足查询中所需的所有数据,则只扫描索引树。此时,explain的Extra 列的结果是Using index。index通常比ALL快,因为索引的大小通常小于表数据。 ▪按索引的顺序来查找数据行,执行了全表扫描。此时,explain的Extra列的结果不会出现Uses index。 ◦ALL:全表扫描,性能最差。 possible_keys:展示当前查询可以使用哪些索引,这一列的数据是在优化过程的早期创建的,因此有些索引可能对于后续优化过程是没用的。 key:表示MySQL实际选择的索引key_len:索引使用的字节数。由于存储格式,当字段允许为NULL时,key_len比不允许为空时大1字节。 ◦key_len计算公式:https://www.cnblogs.com/gomysql/p/4004244.html ref:表示将哪个字段或常量和key列所使用的字段进行比较。如果ref是一个函数,则使用的值是函数的结果。要想查看是哪个函数,可在EXPLAIN语句之后紧跟一个SHOW WARNING语句。 rows:MySQL估算会扫描的行数,数值越小越好。 filtered:表示符合查询条件的数据百分比,最大100。用rows × filtered可获得和下一张表连接的行数。例如rows = 1000,filtered = 50%,则和下一张表连接的行数是500。 TIPS 在MySQL 5.7之前,想要显示此字段需使用explain extended命令; MySQL.5.7及更高版本,explain默认就会展示filtered Extra:展示有关本次查询的附加信息,取值如下: ◦Child of ‘table’ pushed join@1:此值只会在NDB Cluster下出现。 ◦const row not found:例如查询语句SELECT … FROM tbl_name,而表是空的 ◦Deleting all rows:对于DELETE语句,某些引擎(例如MyISAM)支持以一种简单而快速的方式删除所有的数据,如果使用了这种优化,则显示此值 ◦Distinct:查找distinct值,当找到第一个匹配的行后,将停止为当前行组合搜索更多行 ◦FirstMatch(tbl_name):当前使用了半连接FirstMatch策略,详见 https://mariadb.com/kb/en/firstmatch-strategy/ ,翻译 https://www.cnblogs.com/abclife/p/10895624.html ◦Full scan on NULL key:子查询中的一种优化方式,在无法通过索引访问null值的时候使用 ◦Impossible HAVING:HAVING子句始终为false,不会命中任何行 ◦Impossible WHERE:WHERE子句始终为false,不会命中任何行 ◦Impossible WHERE noticed after reading const tables:MySQL已经读取了所有const(或system)表,并发现WHERE子句始终为false ◦LooseScan(m…n):当前使用了半连接LooseScan策略,详见 https://mariadb.com/kb/en/loosescan-strategy/ ,翻译 http://www.javacoder.cn/?p=39 ◦No matching min/max row:没有任何能满足例如 SELECT MIN(…) FROM … WHERE condition 中的condition的行 ◦no matching row in const table:对于关联查询,存在一个空表,或者没有行能够满足唯一索引条件 ◦No matching rows after partition pruning:对于DELETE或UPDATE语句,优化器在partition pruning(分区修剪)之后,找不到要delete或update的内容 ◦No tables used:当此查询没有FROM子句或拥有FROM DUAL子句时出现。例如:explain select 1 ◦Not exists:MySQL能对LEFT JOIN优化,在找到符合LEFT JOIN的行后,不会为上一行组合中检查此表中的更多行。例如: SELECT * FROM t1 LEFT JOIN t2 ON t1.id=t2.id WHERE t2.id IS NULL; ◦假设t2.id定义成了NOT NULL ,此时,MySQL会扫描t1,并使用t1.id的值查找t2中的行。 如果MySQL在t2中找到一个匹配的行,它会知道t2.id永远不会为NULL,并且不会扫描t2中具有相同id值的其余行。也就是说,对于t1中的每一行,MySQL只需要在t2中只执行一次查找,而不考虑在t2中实际匹配的行数。 ◦在MySQL 8.0.17及更高版本中,如果出现此提示,还可表示形如 NOT IN (subquery) 或 NOT EXISTS (subquery) 的WHERE条件已经在内部转换为反连接。这将删除子查询并将其表放入最顶层的查询计划中,从而改进查询的开销。通过合并半连接和反联接,优化器可以更加自由地对执行计划中的表重新排序,在某些情况下,可让查询提速。你可以通过在EXPLAIN语句后紧跟一个SHOW WARNING语句,并分析结果中的Message列,从而查看何时对该查询执行了反联接转换。 ◦Note:两表关联只返回主表的数据,并且只返回主表与子表没关联上的数据,这种连接就叫反连接Plan isn’t ready yet:使用了EXPLAIN FOR CONNECTION,当优化器尚未完成为在指定连接中为执行的语句创建执行计划时, 就会出现此值。MySQL没有找到合适的索引去使用,但是去检查是否可以使用range或index_merge来检索行时,会出现此提示。index map N索引的编号从1开始,按照与表的SHOW INDEX所示相同的顺序。 索引映射值N是指示哪些索引是候选的位掩码值。 例如0x19(二进制11001)的值意味着将考虑索引1、4和5。 Plan isn’t ready yet:使用了EXPLAIN FOR CONNECTION,当优化器尚未完成为在指定连接中为执行的语句创建执行计划时, 就会出现此值。 ◦MySQL没有找到合适的索引去使用,但是去检查是否可以使用range或index_merge来检索行时,会出现此提示。index map N索引的编号从1开始,按照与表的SHOW INDEX所示相同的顺序。 索引映射值N是指示哪些索引是候选的位掩码值。 例如0x19(二进制11001)的值意味着将考虑索引1、4和5。 ◦示例:下面例子中,name是varchar类型,但是条件给出整数型,涉及到隐式转换。图中t2也没有用到索引,是因为查询之前我将t2中name字段排序规则改为utf8_bin导致的链接字段排序规则不匹配。 explain select a.* from t1 a left join t2 b on t1.name = t2.name where t2.name = 2; 结果: Recursive:出现了递归查询。详见“WITH (Common Table Expressions)” Rematerialize:用得很少,使用类似如下SQL时,会展示Rematerialize SELECT ... FROM t, LATERAL (derived table that refers to t) AS dt ... Scanned N databases:表示在处理INFORMATION_SCHEMA表的查询时,扫描了几个目录,N的取值可以是0,1或者all。详见 “Optimizing INFORMATION_SCHEMA Queries” Select tables optimized away:优化器确定:①最多返回1行;②要产生该行的数据,要读取一组确定的行,时会出现此提示。一般在用某些聚合函数访问存在索引的某个字段时,优化器会通过索引直接一次定位到所需要的数据行完成整个查询时展示,例如下面这条SQL。 explain select min(id) from t1; Skip_open_table, Open_frm_only, Open_full_table:这些值表示适用于INFORMATION_SCHEMA表查询的文件打开优化; ◦Skip_open_table:无需打开表文件,信息已经通过扫描数据字典获得 ◦Open_frm_only:仅需要读取数据字典以获取表信息 ◦Open_full_table:未优化的信息查找。表信息必须从数据字典以及表文件中读取Start temporary, End temporary:表示临时表使用Duplicate Weedout策略,详见 https://mariadb.com/kb/en/duplicateweedout-strategy/ ,翻译 https://www.cnblogs.com/abclife/p/10895531.html unique row not found:对于形如 SELECT … FROM tbl_name 的查询,但没有行能够满足唯一索引或主键查询的条件 Using filesort:当Query 中包含 ORDER BY 操作,而且无法利用索引完成排序操作的时候,MySQL Query Optimizer 不得不选择相应的排序算法来实现。数据较少时从内存排序,否则从磁盘排序。Explain不会显示的告诉客户端用哪种排序。官方解释:“MySQL需要额外的一次传递,以找出如何按排序顺序检索行。通过根据联接类型浏览所有行并为所有匹配WHERE子句的行保存排序关键字和行的指针来完成排序。然后关键字被排序,并按排序顺序检索行”Using index:仅使用索引树中的信息从表中检索列信息,而不必进行其他查找以读取实际行。当查询仅使用属于单个索引的列时,可以使用此策略。例如:explain SELECT id FROM t Using index condition:表示先按条件过滤索引,过滤完索引后找到所有符合索引条件的数据行,随后用 WHERE 子句中的其他条件去过滤这些数据行。通过这种方式,除非有必要,否则索引信息将可以延迟“下推”读取整个行的数据。详见 “Index Condition Pushdown Optimization” 。例如: MySQL分成了Server层和引擎层,下推指的是将请求交给引擎层处理。理解这个功能,可创建所以INDEX (zipcode, lastname, firstname),并分别用如下指令, SET optimizer_switch = 'index_condition_pushdown=off'; SET optimizer_switch = 'index_condition_pushdown=on'; 开或者关闭索引条件下推,并对比: explain SELECT * FROM people WHERE zipcode='95054' AND lastname LIKE '%etrunia%' AND address LIKE '%Main Street%'; 的执行结果。index condition pushdown从MySQL 5.6开始支持,是MySQL针对特定场景的优化机制,感兴趣的可以看下 https://blog.51cto.com/lee90/2060449 Using index for group-by:数据访问和 Using index 一样,所需数据只须要读取索引,当Query 中使用GROUP BY或DISTINCT 子句时,如果分组字段也在索引中,Extra中的信息就会是 Using index for group-by。详见 “GROUP BY Optimization” -- name字段有索引 explain SELECT name FROM t1 group by name Using index for skip scan:表示使用了Skip Scan。详见 Skip Scan Range Access Method Using join buffer (Block Nested Loop), Using join buffer (Batched Key Access):使用Block Nested Loop或Batched Key Access算法提高join的性能。详见 https://www.cnblogs.com/chenpingzhao/p/6720531.html Using MRR:使用了Multi-Range Read优化策略。详见 “Multi-Range Read Optimization”Using sort_union(…), Using union(…), Using intersect(…):这些指示索引扫描如何合并为index_merge连接类型。详见 “Index Merge Optimization” Using temporary:为了解决该查询,MySQL需要创建一个临时表来保存结果。如果查询包含不同列的GROUP BY和 ORDER BY子句,通常会发生这种情况。 -- name无索引 explain SELECT name FROM t1 group by name Using where:如果我们不是读取表的所有数据,或者不是仅仅通过索引就可以获取所有需要的数据,则会出现using where信息 explain SELECT * FROM t1 where id > 5 Using where with pushed condition:仅用于NDB Zero limit:该查询有一个limit 0子句,不能选择任何行explain SELECT name FROM resource_template limit 0 4. 扩展的EXPLAIN EXPLAIN可产生额外的扩展信息,可通过在EXPLAIN语句后紧跟一条SHOW WARNING语句查看扩展信息。 TIPS 在MySQL 8.0.12及更高版本,扩展信息可用于SELECT、DELETE、INSERT、REPLACE、UPDATE语句;在MySQL 8.0.12之前,扩展信息仅适用于SELECT语句; 在MySQL 5.6及更低版本,需使用EXPLAIN EXTENDED xxx语句;而从MySQL 5.7开始,无需添加EXTENDED关键词。 使用示例: mysql> EXPLAIN SELECT t1.a, t1.a IN (SELECT t2.a FROM t2) FROM t1\G *************************** 1. row *************************** id: 1 select_type: PRIMARY table: t1 type: index possible_keys: NULL key: PRIMARY key_len: 4 ref: NULL rows: 4 filtered: 100.00 Extra: Using index *************************** 2. row *************************** id: 2 select_type: SUBQUERY table: t2 type: index possible_keys: a key: a key_len: 5 ref: NULL rows: 3 filtered: 100.00 Extra: Using index 2 rows in set, 1 warning (0.00 sec) mysql> SHOW WARNINGS\G *************************** 1. row *************************** Level: Note Code: 1003 Message: /* select#1 */ select `test`.`t1`.`a` AS `a`, <in_optimizer>(`test`.`t1`.`a`,`test`.`t1`.`a` in ( <materialize> (/* select#2 */ select `test`.`t2`.`a` from `test`.`t2` where 1 having 1 ), <primary_index_lookup>(`test`.`t1`.`a` in <temporary table> on <auto_key> where ((`test`.`t1`.`a` = `materialized-subquery`.`a`))))) AS `t1.a IN (SELECT t2.a FROM t2)` from `test`.`t1` 1 row in set (0.00 sec) 由于SHOW WARNING的结果并不一定是一个有效SQL,也不一定能够执行(因为里面包含了很多特殊标记)。特殊标记取值如下: ◦<auto_key>:自动生成的临时表key ◦<cache>(expr):表达式(例如标量子查询)执行了一次,并且将值保存在了内存中以备以后使用。对于包括多个值的结果,可能会创建临时表,你将会看到 <temporary table> 的字样 ◦<exists>(query fragment):子查询被转换为 EXISTS ◦<in_optimizer>(query fragment):这是一个内部优化器对象,对用户没有任何意义 ◦<index_lookup>(query fragment):使用索引查找来处理查询片段,从而找到合格的行 ◦<if>(condition, expr1, expr2):如果条件是true,则取expr1,否则取expr2 ◦<is_not_null_test>(expr):验证表达式不为NULL的测试 ◦<materialize>(query fragment):使用子查询实现 ◦materialized-subquery.col_name:在内部物化临时表中对col_name的引用,以保存子查询的结果 ◦<primary_index_lookup>(query fragment):使用主键来处理查询片段,从而找到合格的行 ◦<ref_null_helper>(expr):这是一个内部优化器对象,对用户没有任何意义 ◦/* select#N */ select_stmt:SELECT与非扩展的EXPLAIN输出中id=N的那行关联 ◦outer_tables semi join (inner_tables):半连接操作。inner_tables展示未拉出的表。详见 “Optimizing Subqueries, Derived Tables, and View References with Semijoin Transformations” ◦<temporary table>:表示创建了内部临时表而缓存中间结果 当某些表是const或system类型时,这些表中的列所涉及的表达式将由优化器尽早评估,并且不属于所显示语句的一部分。但是,当使用FORMAT=JSON时,某些const表的访问将显示为ref。 5. 估计查询性能 多数情况下,你可以通过计算磁盘的搜索次数来估算查询性能。对于比较小的表,通常可以在一次磁盘搜索中找到行(因为索引可能已经被缓存了),而对于更大的表,你可以使用B-tree索引进行估算:你需要进行多少次查找才能找到行:log(row_count) / log(index_block_length / 3 * 2 / (index_length + data_pointer_length)) + 1 在MySQL中,index_block_length通常是1024字节,数据指针一般是4字节。比方说,有一个500,000的表,key是3字节,那么根据计算公式 log(500,000)/log(1024/3*2/(3+4)) + 1 = 4 次搜索。 该索引将需要500,000 * 7 * 3/2 = 5.2MB的存储空间(假设典型的索引缓存的填充率是2/3),因此你可以在内存中存放更多索引,可能只要一到两个调用就可以找到想要的行了。 但是,对于写操作,你需要四个搜索请求来查找在何处放置新的索引值,然后通常需要2次搜索来更新索引并写入行。 前面的讨论并不意味着你的应用性能会因为log N而缓慢下降。只要内容被OS或MySQL服务器缓存,随着表的变大,只会稍微变慢。在数据量变得太大而无法缓存后,将会变慢很多,直到你的应用程序受到磁盘搜索约束(按照log N增长)。为了避免这种情况,可以根据数据的增长而增加key的。对于MyISAM表,key的缓存大小由名为key_buffer_size的系统变量控制,详见Section 5.1.1, “Configuring the Server” 6. 参考文档 EXPLAIN Output Format EXPLAIN Statement Extended EXPLAIN Output Format Estimating Query Performance MySQL中explain执行计划中额外信息字段(Extra)详解 explain参数详解 最官方的 mysql explain type 字段解读 What does eq_ref and ref types mean in MySQL explain 面试官:不会看 Explain执行计划,简历敢写 SQL 优化? 作者:京东物流 柳宏 来源:京东云开发者社区 自猿其说Tech 转载请注明来源

优秀的个人博客,低调大师

MYSQL中JSON类型介绍 | 京东物流技术团队

1 json对象的介绍 在mysql未支持json数据类型时,我们通常使用varchar、blob或text的数据类型存储json字符串,对mysql来说,用户插入的数据只是序列化后的一个普通的字符串,不会对JSON文档本身的语法合法性做检查,文档的合法性需要用户自己保证。在使用时需要先将整个json对象从数据库读取出来,在内存中完成解析及相应的计算处理,这种方式增加了数据库的网络开销并降低处理效率。 从 MySQL 5.7.8 开始,MySQL 支持RFC 7159定义的全部json 数据类型,具体的包含四种基本类型(strings, numbers, booleans,and null)和两种结构化类型(objects and arrays)。可以有效地访问 JSON文档中的数据。与将 JSON 格式的字符串存储在字符串列中相比,该数据类型具有以下优势: 自动验证存储在 JSON列中的 JSON 文档。无效的文档会产生错误。 优化的存储格式。存储在列中的 JSON 文档被转换为允许快速读取文档元素的内部格式。当读取 JSON 值时,不需要从文本表示中解析该值,使服务器能够直接通过键或数组索引查找子对象或嵌套值,而无需读取文档中它们之前或之后的所有值。 2 json类型的存储结构 mysql为了提供对json对象的支持,提供了一套将json字符串转为结构化二进制对象的存储方式。json会被转为二进制的doc对象存储于磁盘中(在处理JSON时MySQL使用的utf8mb4字符集,utf8mb4是utf8和ascii的超集)。 doc对象包含两个部分,type和value部分。其中type占1字节,可以表示16种类型:大的和小的json object类型、大的和小的 json array类型、literal类型(true、false、null三个值)、number类型(int6、uint16、int32、uint32、int64、uint64、double类型、utf8mb4 string类型和custom data(mysql自定义类型),具体可以参考源码json_binary.cc和json_binary.h进行学习。 下面进行简单介绍: type ::= 0x00 | // small JSON object 0x01 | // large JSON object 0x02 | // small JSON array 0x03 | // large JSON array 0x04 | // literal (true/false/null) 0x05 | // int16 0x06 | // uint16 0x07 | // int32 0x08 | // uint32 0x09 | // int64 0x0a | // uint64 0x0b | // double 0x0c | // utf8mb4 string 0x0f // custom data (any MySQL data type) value包含 object、array、literal、number、string和custom-data六种类型,与type的16种类型对应。 object表示json对象类型,由6部分组成: object ::= element-count size key-entry value-entry key value 其中: element-count表示对象中包含的成员(key)个数,在array类型中表示数组元素个数。 size表示整个json对象的二进制占用空间大小。小对象用2Bytes空间表示(最大64K),大对象用4Bytes表示(最大4G) key-entry可以理解为一个用于指向真实key值的数组。本身用于二分查找,加速json字段的定位。 key-entry由两个部分组成: key-entry ::= key-offset key-length 其中: key-offset:表示key值存储的偏移量,便于快速定位key的真实值。 key-length:表示key值的长度,用于分割不同key值的边界。长度为2Bytes,这说明,key值的长度最长不能超过64kb. value-entry与key-enter功能类似,不同之处在于,value-entry可能存储真实的value值。 value-entry由两部分组成: value-entry ::= type offset-or-inlined-value 其中: type表示value类型,如上文所示,支持16种基本类型,从而可以表示各种类型的嵌套。 offset-or-inlined-value:有两层含义,如果value值足够小,可以存储于此,那么就存储数据本身,如果数据本身较大,则存储真实值的偏移用于快速定位。 key 表示key值的真实值,类型为:key ::= utf8mb4-data,这里无需指定key值长度,因为key-entry中已经声明了key的存储长度。同时,在同一个json对象中,key值的长度总是一样的。 array表示json数组,array类型主要包含4部分: array ::= element-count size value-entry value 我们来使用示意图更清晰的展示它的结构: 举例说明: 需要注意的是: JSON对象的Key索引(图中橙色部分)都是排序好的,先按长度排序,长度相同的按照code point排序;Value索引(图中黄色部分)根据对应的Key的位置依次排列,最后面真实的数据存储(图中白色部分)也是如此 Key和Value的索引对存储了对象内的偏移和大小,单个索引的大小固定,可以通过简单的算术跳转到距离为N的索引 通过MySQL5.7.16源代码可以看到,在序列化JSON文档时,MySQL会动态检测单个对象的大小,如果小于64KB使用两个字节的偏移量,否则使用四个字节的偏移量,以节省空间。同时,动态检查单个对象是否是大对象,会造成对大对象进行两次解析,源代码中也指出这是以后需要优化的点 现在受索引中偏移量和存储大小四个字节大小的限制,单个JSON文档的大小不能超过4G;单个KEY的大小不能超过两个字节,即64K 索引存储对象内的偏移是为了方便移动,如果某个键值被改动,只用修改受影响对象整体的偏移量 索引的大小现在是冗余信息,因为通过相邻偏移可以简单的得到存储大小,主要是为了应对变长JSON对象值更新,如果长度变小,JSON文档整体都不用移动,只需要当前对象修改大小 现在MySQL对于变长大小的值没有预留额外的空间,也就是说如果该值的长度变大,后面的存储都要受到影响 结合JSON的路径表达式可以知道,JSON的搜索操作只用反序列化路径上涉及到的元素,速度非常快,实现了读操作的高性能 MySQL对于大型文档的变长键值的更新操作可能会变慢,可能并不适合写密集的需求 3 json类型基本操作 3.1 json数据插入 json类型数据插入时有两种方式,一种是基于字符串格式插入,另一种是基于json_object()函数,在使用json_object()函数只需按k-v顺序,以,符号隔开顺序插入即可,MYSQL会自动验证 JSON 文档,无效的文档会产生错误。 mysql> CREATE TABLE t1 (jdoc JSON); Query OK, 0 rows affected (0.20 sec) mysql> INSERT INTO t1 VALUES('{"key1": "value1", "key2": "value2"}'); Query OK, 1 row affected (0.01 sec) mysql> INSERT INTO t1 VALUES('[1, 2,'); ERROR 3140 (22032) at line 2: Invalid JSON text: "Invalid value." at position 6 in value (or column) '[1, 2,'. 当一个字符串被解析并发现是一个有效的 JSON 文档时,它也会被规范化:具有与文档中先前找到的键重复的键的成员被丢弃(即使值不同)。以下第一个sql中通过 JSON_OBJECT()调用生成的对象值不包括第二个key1元素,因为该键名出现在值的前面;第二个sql中只保留了x第一次出现的值: mysql> SELECT JSON_OBJECT('key1', 1, 'key2', 'abc', 'key1', 'def'); +------------------------------------------------------+ | JSON_OBJECT('key1', 1, 'key2', 'abc', 'key1', 'def') | +------------------------------------------------------+ | {"key1": 1, "key2": "abc"} | +------------------------------------------------------+ mysql> INSERT INTO t1 VALUES > ('{"x": 17, "x": "red"}'), > ('{"x": 17, "x": "red", "x": [3, 5, 7]}'); mysql> SELECT c1 FROM t1; +-----------+ | c1 | +-----------+ | {"x": 17} | | {"x": 17} | +-----------+ 3.2 json合并 MySQL 5.7支持JSON_MERGE()的合并算法,多个对象合并时产生一个对象。 可将多个数组合并为一个数组: mysql> SELECT JSON_MERGE('[1, 2]', '["a", "b"]', '[true, false]'); +-----------------------------------------------------+ | JSON_MERGE('[1, 2]', '["a", "b"]', '[true, false]') | +-----------------------------------------------------+ | [1, 2, "a", "b", true, false] | +-----------------------------------------------------+ 当合并数组与对象时,会将对象转换为新数组进行合并: mysql> SELECT JSON_MERGE('[10, 20]', '{"a": "x", "b": "y"}'); +------------------------------------------------+ | JSON_MERGE('[10, 20]', '{"a": "x", "b": "y"}') | +------------------------------------------------+ | [10, 20, {"a": "x", "b": "y"}] | +------------------------------------------------+ 如果多个对象具有相同的键,则生成的合并对象中该键的值是包含键值的数组 mysql> SELECT JSON_MERGE('{"a": 1, "b": 2}', '{"c": 3, "a": 4}'); +----------------------------------------------------+ | JSON_MERGE('{"a": 1, "b": 2}', '{"c": 3, "a": 4}') | +----------------------------------------------------+ | {"a": [1, 4], "b": 2, "c": 3} | +----------------------------------------------------+ MySQL 8.0.3(及更高版本)支持两种合并算法,由函数 JSON_MERGE_PRESERVE()和 JSON_MERGE_PATCH(). 它们在处理重复键的方式上有所不同:JSON_MERGE_PRESERVE()保留重复键的值(与5.7版本的JSON_MERGE()相同),而 JSON_MERGE_PATCH()丢弃除最后一个值之外的所有值。具体的 JSON_MERGE_PRESERVE() 函数接受两个或多个 JSON 文档并返回组合结果。如果参数为两个object,相同的key将会把value合并为array(即使value也相同,也会合并为array),不同的key则直接合并。如果其中一个参数为json array,则另一个json object整体作为一个元素,加入array结果。 JSON_MERGE_PATCH()函数接受两个或多个 JSON 文档并返回组合结果。如果参数为两个object,相同的key的value将会被后面参数的value覆盖,不同的key则直接合并。如果合并的是数组,将按照“最后一个重复键获胜”逻辑仅保留最后一个参数。 mysql> SELECT JSON_MERGE_PRESERVE('{"a":1,"b":2}', '{"a":3,"c":3}'); +-------------------------------------------------------+ | JSON_MERGE_PRESERVE('{"a":1,"b":2}', '{"a":3,"c":3}') | +-------------------------------------------------------+ | {"a": [1, 3], "b": 2, "c": 3} | +-------------------------------------------------------+ 1 row in set (0.01 sec) mysql> SELECT JSON_MERGE_PATCH('{"a":1,"b":2}', '{"a":3,"c":3}'); +----------------------------------------------------+ | JSON_MERGE_PATCH('{"a":1,"b":2}', '{"a":3,"c":3}') | +----------------------------------------------------+ | {"a": 3, "b": 2, "c": 3} | +----------------------------------------------------+ 1 row in set (0.02 sec) mysql> SELECT JSON_MERGE_PRESERVE('["a", 1]', '"a"','{"key": "value"}'); +-----------------------------------------------------------+ | JSON_MERGE_PRESERVE('["a", 1]', '"a"','{"key": "value"}') | +-----------------------------------------------------------+ | ["a", 1, "a", {"key": "value"}] | +-----------------------------------------------------------+ 1 row in set (0.00 sec) mysql> SELECT JSON_MERGE_PATCH('["a", 1]', '"a"','{"key": "value"}') ; +--------------------------------------------------------+ | JSON_MERGE_PATCH('["a", 1]', '"a"','{"key": "value"}') | +--------------------------------------------------------+ | {"key": "value"} | +--------------------------------------------------------+ 1 row in set (0.01 sec) 3.3 json数据查询 MySQL 5.7.7+本身提供了很多原生的函数以及路径表达式来方便用户访问JSON数据。 JSON_EXTRACT()函数用于解析json对象,->符号是就一种JSON_EXTRACT()函数的等价模式。例如查询上面t1表中 jdoc字段中key值为x的值 SELECT jdoc->'$.x' FROM t1; SELECT JSON_EXTRACT(jdoc,'$.x') FROM t1; JSON_EXTRACT返回值会带有” “,如果想获取原本的值可以使用JSON_UNQUOTE mysql> SELECT JSON_EXTRACT('{"id": 14, "name": "Aztalan"}', '$.name'); +---------------------------------------------------------+ | JSON_EXTRACT('{"id": 14, "name": "Aztalan"}', '$.name') | +---------------------------------------------------------+ | "Aztalan" | +---------------------------------------------------------+ mysql> SELECT JSON_UNQUOTE(json_extract('{"id": 14, "name": "Aztalan"}', '$.name'));; +-----------------------------------------------------------------------+ | JSON_UNQUOTE(json_extract('{"id": 14, "name": "Aztalan"}', '$.name')) | +-----------------------------------------------------------------------+ | Aztalan | +-----------------------------------------------------------------------+ json路径的语法: pathExpression: scope[(pathLeg)*] pathLeg: member | arrayLocation | doubleAsterisk member: period ( keyName | asterisk ) arrayLocation: leftBracket ( nonNegativeInteger | asterisk ) rightBracket keyName: ESIdentifier | doubleQuotedString doubleAsterisk: '**' period: '.' asterisk: '*' leftBracket: '[' rightBracket: ']' 以json { “a”: [ [ 3, 2 ], [ { “c” : “d” }, 1 ] ], “b”: { “c” : 6 }, “one potato”: 7, “b.c” : 8 } 为例: $.a[1] 获取的值为 [ { “c” : “d” }, 1 ] $.b.c 获取的值为 6 $.”b.c” 获取的值为 8(因为键名包含不合法的表达式所以需要使用引号) mysql> select json_extract('{ "a": [ [ 3, 2 ], [ { "c" : "d" }, 1 ] ], "b": { "c" : 6 }, "one potato": 7, "b.c" : 8 }','$**.c'); +-------------------------------------------------------------------------------------------------------------------+ | JSON_EXTRACT('{ "a": [ [ 3, 2 ], [ { "c" : "d" }, 1 ] ], "b": { "c" : 6 }, "one potato": 7, "b.c" : 8 }','$**.c') | +-------------------------------------------------------------------------------------------------------------------+ | ["d", 6] | +-------------------------------------------------------------------------------------------------------------------+ $**.c 匹配到了两个路径 : $.a[1].c 获取的值是”d” $.b.c 获取的值为 6 3.4 json数据更新 一些函数采用现有的 JSON 文档,以某种方式对其进行修改,然后返回结果修改后的文档。路径表达式指示在文档中进行更改的位置。例如,JSON_SET()、 JSON_INSERT()和 JSON_REPLACE()函数各自采用现有的 JSON 文档,加上一个或多个路径和值对,来描述修改文档和要更新的值。这些函数在处理文档中现有值和不存在值的方式上有所不同。 具体如下 mysql> SET @j = '["a", {"b": [true, false]}, [10, 20]]'; JSON_SET()替换存在的路径的值并添加不存在的路径的值: mysql> SELECT JSON_SET(@j, '$[1].b[0]', 1, '$[2][2]', 2); +--------------------------------------------+ | JSON_SET(@j, '$[1].b[0]', 1, '$[2][2]', 2) | +--------------------------------------------+ | ["a", {"b": [1, false]}, [10, 20, 2]] | +--------------------------------------------+ 在这种情况下,路径$[1].b[0]选择一个现有值 ( true),该值将替换为路径参数 ( 1) 后面的值。该路径$[2][2]不存在,因此将相应的值 ( 2) 添加到 选择的值中$[2]。 JSON_INSERT()添加新值但不替换现有值: mysql> SELECT JSON_INSERT(@j, '$[1].b[0]', 1, '$[2][2]', 2); +-----------------------------------------------+ | JSON_INSERT(@j, '$[1].b[0]', 1, '$[2][2]', 2) | +-----------------------------------------------+ | ["a", {"b": [true, false]}, [10, 20, 2]] | +-----------------------------------------------+ JSON_REPLACE()替换现有值并忽略新值: mysql> SELECT JSON_REPLACE(@j, '$[1].b[0]', 1, '$[2][2]', 2); +------------------------------------------------+ | JSON_REPLACE(@j, '$[1].b[0]', 1, '$[2][2]', 2) | +------------------------------------------------+ | ["a", {"b": [1, false]}, [10, 20]] | +------------------------------------------------+ JSON_REMOVE()接受一个 JSON 文档和一个或多个路径,这些路径指定要从文档中删除的值。返回值是原始文档减去文档中存在的路径选择的值: mysql> SELECT JSON_REMOVE(@j, '$[2]', '$[1].b[1]', '$[1].b[1]'); +---------------------------------------------------+ | JSON_REMOVE(@j, '$[2]', '$[1].b[1]', '$[1].b[1]') | +---------------------------------------------------+ | ["a", {"b": [true]}] | +---------------------------------------------------+ $[2]匹配[10, 20] 并删除它。 $[1].b[1]匹配 元素false中 的第一个实例b并将其删除。 不匹配的第二个实例$[1].b[1]:该元素已被删除,路径不再存在,并且没有效果。 3.5 json比较与排序 JSON值可以使用=, <, <=, >, >=, <>, !=, <=>等操作符,BETWEEN, IN,GREATEST, LEAST等操作符现在还不支持。JSON值使用的两级排序规则,第一级基于JSON的类型,类型不同的使用每个类型特有的排序规则。 JSON类型按照优先级从高到低为 BLOB BIT OPAQUE DATETIME TIME DATE BOOLEAN ARRAY OBJECT STRING INTEGER, DOUBLE NULL 优先级高的类型大,不用再进行其他的比较操作;如果类型相同,每个类型按自己的规则排序。具体的规则如下: BLOB/BIT/OPAQUE: 比较两个值前N个字节,如果前N个字节相同,短的值小 DATETIME/TIME/DATE: 按照所表示的时间点排序 BOOLEAN: false小于true ARRAY: 两个数组如果长度和在每个位置的值相同时相等,如果不想等,取第一个不相同元素的排序结果,空元素最小。例:[] < [“a”] < [“ab”] < [“ab”, “cd”, “ef”] < [“ab”, “ef”] OBJECT: 如果两个对象有相同的KEY,并且KEY对应的VALUE也都相同,两者相等。否则,两者大小不等,但相对大小未规定。例:{“a”: 1, “b”: 2} = {“b”: 2, “a”: 1} STRING: 取两个STRING较短的那个长度为N,比较两个值utf8mb4编码的前N个字节,较短的小,空值最小。例:”a” < “ab” < “b” < “bc”;此排序等同于使用 collat​​ion 对 SQL 字符串进行排序utf8mb4_bin。因为 utf8mb4_bin是二进制排序规则,所以 JSON 值的比较区分大小写:”A” < “a” INTEGER/DOUBLE: 包括精确值和近似值的比较 4 JSON的索引 现在MySQL不支持对JSON列进行索引,官网文档的说明是: JSON columns cannot be indexed. You can work around this restriction by creating an index on a generated column that extracts a scalar value from the JSON column. 虽然不支持直接在JSON列上建索引,但MySQL规定,可以首先使用路径表达式对JSON文档中的标量值建立虚拟列,然后在虚拟列上建立索引。这样用户可以使用表达式对自己感兴趣的键值建立索引。举个具体的例子来说明: ALTER TABLE features ADD feature_street VARCHAR(30) AS (JSON_UNQUOTE(feature->"$.properties.STREET")); ALTER TABLE features ADD INDEX (feature_street); 两个步骤,可以对feature列中properties键值下的STREET键(feature->”$.properties.STREET”)创建索引。 其中,feature_street列就是新添加的虚拟列。之所以取名虚拟列,是因为与它对应的还有一个存储列(stored column)。它们最大的区别为虚拟列只修改数据库的metadata,并不会存储真实的数据在硬盘上,读取过程也是实时计算的方式;而存储列会把表达式的列存储在硬盘上。两者使用的场景不一样,默认情况下通过表达式生成的列为虚拟列。 这样虚拟列的添加和删除都会非常快,而在虚拟列上建立索引跟传统的建立索引的方式并没有区别,会提高虚拟列读取的性能,减慢整体插入的性能。虚拟列的特性结合JSON的路径表达式,可以方便的为用户提供高效的键值索引功能。 5 总结 JSON类型无须预定义字段,适合拓展信息的存储 单个JSON文档的大小不能超过4G;单个KEY的大小不能超过两个字节,即64K JSON类型适合应用于不常更新的静态数据 对搜索较频繁的数据建议增加虚拟列并建立索引 作者:京东物流 王凤辉 来源:京东云开发者社区 自猿其说Tech

优秀的个人博客,低调大师

缕析条分Scroll属性 | 京东云技术团队

最近有项目需要使用js原生开发滑动组件,频繁要用到dom元素的各种属性,其中以各种类型的height和top属性居多,名字相近,含义也很容易搞混。因此特地总结归纳了一下常用的知识点,在文末我们来挑战实现一个简易的移动端Scroll组件。 要理解height和top,要从盒模型开始说起,首先我们来认识一下css3中定义的盒模型。dom元素在页面上实际占据的面积可以由下面这张图来说明:  我们可以把它想象成一枚鸡蛋,从外到内依次是 橙色区域——外边距margin:鸡蛋壳 黑色区域——边框border:蛋壳膜 绿色区域——内边距padding:蛋白 白色区域——内容content:蛋黄 我们真正关心的部分是内容content,也就是鸡蛋营养最丰富的部分。content外面包裹了这么多层,我们在页面中才会感觉dom结构整体疏密有致,不会被密密麻麻的文字和图片所困扰。为了更好地描述盒模型,web规范中还定义了一些其他接口来描述他们,也就是我们今天要聊到的主角们: 在上面这张图中,我们描述了具有嵌套关系的两层dom元素,注意这里的内容区和前述一张图中有所不同,这里的内容区也是一个独立的dom元素(即它也有自己的margin、border、padding和content!,为了简化起见,子元素不再具体绘制出来),子元素由于整体高度很高,甚至超出了父元素的高度,在父元素中不能完全展示出来(超出的不可见部分用灰色表示),也就是两者构成了滚动关系。下面我们来尝试描述以下属性: 一、clientHeight 只读属性。clientHeight实际上就是垂直滚动条的高度,一般情况下,垂直滚动条都是要紧贴上下border的,因此clientHeight = 上下padding + 内容height。别忘了有个特殊情况,当存在水平方向滚动条时,还需要考虑水平滚动条挤占了垂直滚动条的一部分空间,即:clientHeight = 上下padding + 内容height - 水平滚动条高度。用鸡蛋来比喻的话,clientHeight就是剥了壳的鸡蛋。 二、clientTop 只读属性。描述了顶部border的宽度。顶部border容易让我们联想到头发的厚度,下次在镜子前可以对自己说:你的clientTop变少了,不过你也变强了。 三、offsetHeight 只读属性。offsetHeight和clientHeight比较类似,观察可以发现,比clientHeight多了border这一层:offsetHeight = clientHeight + 上下border。现在我们把时光回退到给鸡蛋剥壳的前一刻,鸡蛋从水里捞出来洗干净呈现的样子——offsetHeight。 四、offsetTop 只读属性。它返回当前元素相对于其offsetParent元素的顶部内边距的距离。这段距离是不包含自身和父元素的border宽度的,实际上:offsetTop=自己的上margin + 父元素的上padding,相当于鸡蛋和鸡蛋盒之间挡板的厚度。 五、scrollHeight 只读属性。描述了元素内容高度的度量,包括由于溢出导致的视图中不可见内容。我们可以hack一下,把父元素中不可见的内容也展示出来,子元素在垂直方向可以分为两部分:外层的margin和内部的offsetHeight: 对于具有滚动关系的父子元素,其scrollHeight具有不同的含义: (1)对于子元素而言,由于子元素本身不包含溢出部分,其scrollHeight和clientHeight具有相同的值 (2)对于父元素而言,由于父元素的内容实际上被子元素“撑起来”了,因此其scrollHeight为把内容区域“展开”后的实际高度:父元素scrollHeight = 子元素的offsetHeight + 子元素上下margin + 自己的上下padding 六、scrollTop 读写属性,可以获取或设置一个元素的内容垂直滚动的像素数。这个是目前为止咱们遇到的第一个可以支持设置的属性。 (1)初始状态时,内容垂直方向未滚动,其值为0 (2)当内容垂直方向滚动到底时,由于内容区实际高度为scrollHeight,可显示高度为clientHeight,多出来的部分就是此时的scrollTop值为scrollHeight - clientHeight 因此可以得出结论:0 <= scrollTop <= scrollHeight - clientHeight 七、实战 学习了以上属性和信息,我们模仿京东小程序的scroll-view组件功能,来实现一个H5版滑动组件的常见功能:列表的下拉刷新和上拉加载。要实现这个功能,大概可以分为3个核心要点: (1)可滚动:需要有一个不可动的外壳和可滑动的内容区。 (2)手势识别:通过移动端的touch属性,我们可以对比touchend和touchstart的手指位置,来简单进行手势识别。 (3)事件触发:根据滑动位置的临界条件,来判断是否应该触发刷新和加载事件。 部分实现代码如下: // scroller.js export default class Scroller { constructor(el, option) { this._el = el this._parent = el.parentNode this._option = option this._pos = 0 this._handleScrollStart = this.handleScrollStart.bind(this) this._handleScroll = this.handleScroll.bind(this) this.init() } init() { this._el.addEventListener('touchstart', this._handleScrollStart) this._el.addEventListener('touchend', this._handleScroll) if(this._option.auto) { this.handleRefresh() } } destroy() { this._el.removeEventListener('touchstart', this._handleScrollStart) this._el.removeEventListener('touchend', this._handleScroll) } handleScrollStart(e) { const touch = e.targetTouches[0] || e.changedTouches[0] this._pos = touch.clientY } handleScroll(e) { const touch = e.targetTouches[0] || e.changedTouches[0] const delta = touch.clientY - this._pos if(delta >= this._option.threhold) { // 手势向下,且下行滚动距离超过判定阈值 if(this._parent.scrollTop == 0) { this.handleRefresh(e) } } else if(this._parent.scrollHeight > this._parent.clientHeight && delta <= -this._option.threhold){ // 内容可滚动,且上行滚动距离超过判定阈值 if(this._parent.scrollTop > this._parent.scrollHeight - this._parent.clientHeight - this._option.threhold){ this.handleLoad(e) } } } handleRefresh(...params) { this._option.onRefresh && this._option.onRefresh.apply(this, params) } handleLoad(...params) { this._option.onRefresh && this._option.onLoad.apply(this, params) } } 调用demo: var inner = document.getElementsById('inner') var list = [] var pageIndex = 1 function getMockData() { const newData = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(i => i+(pageIndex-1)*10) setTimeout(() => { if(pageIndex==1) { list = newData } else if(pageIndex < 5) { list.push(...newData) } else { alert('没有更多内容了') } pageIndex++ inner.innerHTML = list.map(i => `<div class="inner-item">${i}</div>`).join('') }, 300) } function onRefresh() { pageIndex = 1 getMockData() } new Scroller(inner, { threhold: 20, onRefresh: onRefresh, onLoad: getMockData, auto: true }) 大家可以自己试用一下,感觉效果还不错~~~欢迎留言区讨论交流。 作者:京东零售 陈震 来源:京东云开发者社区

优秀的个人博客,低调大师

从TL、ITL到TTL | 京东云技术团队

1、概述 ThreadLocal(TL)是Java中一种线程局部变量实现机制,他为每个线程提供一个单独的变量副本,保证多线程场景下,变量的线程安全。经常用于代替参数的显式传递。 InheritableThreadLocal(ITL)是JDK提供的TL增强版,而TransmittableThreadLocal(TTL)是阿里开源的ITL增强版 这些ThreadLocal在不同场景下有不同用途,我们来分析一下: 2、ThreadLocal ThreadLocal主要的方法有四个:initialValue、set、get、remove 2.1、初始化——initialValule 当线程首次访问该ThreadLocal时(ThreadLocal.get()),会进行初始化赋值。我们常用两种方法初始化ThreadLocal 2.1.1、重写initialValue ThreadLocal<String> threadLocal = new ThreadLocal<String>() { @Override protected String initialValue() { return ""; } }; 2.1.2、调用ThreadLocal.withInitial ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> ""); 他会创建一个SuppliedThreadLocal内部类 public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) { return new SuppliedThreadLocal<>(supplier); } 该类重写了initialValue方法 static final class SuppliedThreadLocal<T> extends ThreadLocal<T> { private final Supplier<? extends T> supplier; SuppliedThreadLocal(Supplier<? extends T> supplier) { this.supplier = Objects.requireNonNull(supplier); } @Override protected T initialValue() { //当该线程首次访问ThreadLocal时,会间接调用lambda表达式初始化 return supplier.get(); } } ⚠️ITL并没有重新实现withInitial,如果使用withInitial则会创建STL,失去自己增强的特性 2.2、赋值——set public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); } 这里出现了一个关键属性ThreadLocalMap,类定义在ThreadLocal中,是Thread的成员变量 ThreadLocalMap getMap(Thread t) { return t.threadLocals; } ThreadLocalMap内部还有一个内部类Entry,是存值的地方 static class ThreadLocalMap { static class Entry extends WeakReference<ThreadLocal<?>> { Object value; Entry(ThreadLocal<?> k, Object v) { //ThreadLocal的引用是“key” super(k); //线程局部变量是value value = v; } } //Entry数组 //value具体放在哪个index下,是由ThreadLocal的hashCode算出来的 private Entry[] table; } 2.3、取值——get public T get() { Thread t = Thread.currentThread(); //1、获取线程的ThreadLocalMap ThreadLocalMap map = getMap(t); if (map != null) { //2、根据ThreadLocal的hashCode,获取对应Entry下的value ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } //3、如果没有赋过值,则初始化 return setInitialValue(); } 2.4、清空——remove public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) //会将对应Entry、包括他的key、value手动置null m.remove(this); } 3、InheritableThreadLocal 3.1、TL在父子线程场景下存在的问题 我们先来看一个例子 public static void main(String[] args) throws InterruptedException { ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "A"); threadLocal.set("B"); Thread thread = new Thread(() -> { System.out.println("子线程ThreadLocal:" + threadLocal.get()); }, "子线程"); thread.start(); thread.join(); } 打印结果如下,可见子线程的ThreadLocal是初始值,并没有使用父线程修改后的值: 子线程ThreadLocal:A 线程的ThreadLocalMap是首次访问时创建的,所以子线程使用ThreadLocal的时候,会初始化一个新的ThreadLocal,线程局部变量为默认值 ⚠️所以,TL不具有遗传性 3.2、ITL的解决方案 为了解决TL子线程遗传性的问题,JDK引入了ITL 他继承ThreadLocal,重写了childValue、getMap、createMap三个方法 public class InheritableThreadLocal<T> extends ThreadLocal<T> { protected T childValue(T parentValue) { return parentValue; } ThreadLocalMap getMap(Thread t) { return t.inheritableThreadLocals; } void createMap(Thread t, T firstValue) { t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue); } } 这里出现了inheritableThreadLocals,他存储的就是从父线程拷贝过来的ThreadLocal,这个值是在父线程首次修改ThreadLocal的时候赋值的,然后在子线程创建时拷贝过来的 //父线程部分: public void set(T value) { Thread t = Thread.currentThread(); //该方法被ITL重写,访问inheritableThreadLocals为null ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else //该方法同样被ITL重写,创建一个ThreadLocalMap赋值给inheritableThreadLocals createMap(t, value); } //子线程部分: public Thread(Runnable target) { init(null, target, "Thread-" + nextThreadNum(), 0); } private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) { //省略一些代码... //获取当前线程(父线程、也就是创建子线程的线程) Thread parent = currentThread(); //1、允许ThreadLocal遗传(这个默认为true) //2、inheritableThreadLocals不为空,因为父线程调用set了 //父线程不调用set,那ThreadLocal就是初始值,那直接初始化就好了,也不用进该分支 if (inheritThreadLocals && parent.inheritableThreadLocals != null) this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); } //createInheritedMap使用该构造函数,根据父线程的inheritableThreadLocals进行深拷贝 private ThreadLocalMap(ThreadLocalMap parentMap) { Entry[] parentTable = parentMap.table; int len = parentTable.length; setThreshold(len); table = new Entry[len]; //深拷贝父线程ThreadLocalMap for (int j = 0; j < len; j++) { Entry e = parentTable[j]; if (e != null) { @SuppressWarnings("unchecked") ThreadLocal<Object> key = (ThreadLocal<Object>) e.get(); if (key != null) { //childValue被ITL重写,返回父线程ThreadLocal的值 Object value = key.childValue(e.value); Entry c = new Entry(key, value); int h = key.threadLocalHashCode & (len - 1); while (table[h] != null) h = nextIndex(h, len); table[h] = c; size++; } } } } 使用ITL的效果 public static void main(String[] args) throws InterruptedException { ThreadLocal<String> threadLocal = new InheritableThreadLocal<String>() { @Override protected String initialValue() { return "A"; } }; threadLocal.set("B"); Thread thread = new Thread(() -> { System.out.println("子线程ThreadLocal:" + threadLocal.get()); }, "子线程"); thread.start(); thread.join(); } 打印结果如下,子线程拷贝了父线程ThreadLocal: 子线程ThreadLocal:B 总结一下,ITL解决父子线程遗传性的核心思路是,将可遗传的ThreadLocal放在父线程新的ThreadLocalMap中,在子线程首次使用时进行拷贝 4.、TransmittableThreadLocal 4.1、ITL在线程复用场景下存在的问题 我们再从一个简单的例子说起 public static void main(String[] args) throws InterruptedException, ExecutionException { ThreadLocal<String> threadLocal = new InheritableThreadLocal<String>() { @Override protected String initialValue() { return "A"; } }; threadLocal.set("B"); ExecutorService executorService = Executors.newFixedThreadPool(1); //1、子线程第一次获取ThreadLocal executorService.submit(() -> System.out.println("子线程ThreadLocal:"+threadLocal.get())).get(); Thread.sleep(1000); //2、父线程修改ThreadLocal threadLocal.set("C"); System.out.println("父线程修改ThreadLocal为"+threadLocal.get()); //3、子线程第二次获取ThreadLocal executorService.submit(() -> System.out.println("子线程ThreadLocal:"+threadLocal.get())).get(); } 打印结果如下,子线程在第二次打印时,并没有拷贝父线程的ThreadLocal,使用的还是首次拷贝的值: 子线程ThreadLocal:B 父线程修改ThreadLocal为C 子线程ThreadLocal:B ⚠️可复用的子线程不会感知父线程ThreadLocal的变化 4.2、TTL的解决方案 4.2.1、TTL的使用 TTL在ITL上做了稍微复杂的封装,我们从使用开始了解 引入依赖 <dependency> <groupId>com.alibaba</groupId> <artifactId>transmittable-thread-local</artifactId> <version>latest</version> </dependency> 在使用TTL时,线程需要经过TTL封装,线程池同理 public static void main(String[] args) throws InterruptedException, ExecutionException { ThreadLocal<String> threadLocal = new TransmittableThreadLocal<String>() { @Override protected String initialValue() { return "A"; } }; threadLocal.set("B"); ExecutorService executorService = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(1)); executorService.submit(() -> System.out.println("子线程ThreadLocal:" + threadLocal.get())).get(); Thread.sleep(1000); threadLocal.set("C"); System.out.println("父线程修改ThreadLocal为" + threadLocal.get()); executorService.submit(() -> System.out.println("子线程ThreadLocal:" + threadLocal.get())).get(); Thread.sleep(1000); executorService.submit(() -> { threadLocal.set("D"); System.out.println("子线程修改ThreadLocal为" + threadLocal.get()); }); Thread.sleep(1000); executorService.submit(() -> System.out.println("子线程ThreadLocal:" + threadLocal.get())); Thread.sleep(1000); } 打印结果如下,子线程每次都会获取父线程的ThreadLocal 子线程ThreadLocal:B 父线程修改ThreadLocal为C 子线程ThreadLocal:C 子线程修改ThreadLocal为D 子线程ThreadLocal:C 从使用上看,TTL要求将任务封装,那我们就从ThreadLocal和ExecutorService两部分入手 4.2.2、TTL对ThreadLocal的封装 下面是TTL的取值和赋值逻辑,都涉及一个关键方法addThisToHolder,对应的属性holder会在线程池执行任务时用到 //TransmittableThreadLocal.addThisToHolder() private void addThisToHolder() { //InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>> holder if (!holder.get().containsKey(this)) { //holder是静态变量,他会把TTL存到当前线程的map中 //value是null,他其实是把Map当Set用 //主线程赋值时,会获取主线程的holderMap,然后把TTL存进去 holder.get().put((TransmittableThreadLocal<Object>) this, null); } } @Override public final void set(T value) { if (!disableIgnoreNullValueSemantics && null == value) { remove(); } else { super.set(value); //当主线程赋值时,会将自己的TTL放到自己的map中 addThisToHolder(); } } @Override public final T get() { T value = super.get(); if (disableIgnoreNullValueSemantics || null != value) addThisToHolder(); return value; } 4.2.3、TTL对任务的封装 //我们通过TtlExecutors.getTtlExecutorService()对线程池进行封装 public static ExecutorService getTtlExecutorService(@Nullable ExecutorService executorService) { if (TtlAgent.isTtlAgentLoaded() || executorService == null || executorService instanceof TtlEnhanced) { return executorService; } //入参是线程池,通过包装类代理线程池的操作 return new ExecutorServiceTtlWrapper(executorService); } //ExecutorServiceTtlWrapper.submit() public Future<?> submit(@NonNull Runnable task) { //将提交的任务进行封装 return executorService.submit(TtlRunnable.get(task)); } 4.2.3.1、任务构建 TtlRunnable构造方法 这里都是主线程在操作,因为任务是主线程提交的 private TtlRunnable(@NonNull Runnable runnable, boolean releaseTtlValueReferenceAfterRun) { this.capturedRef = new AtomicReference<Object>(capture()); this.runnable = runnable; this.releaseTtlValueReferenceAfterRun = releaseTtlValueReferenceAfterRun; } 这里有一个关键属性capturedRef,他是一个原子引用,存了TTL //TrasmitableThreadLocal.Transmitter public static Object capture() { //获取ttl的值构建快照 return new Snapshot(captureTtlValues(), captureThreadLocalValues()); } private static HashMap<TransmittableThreadLocal<Object>, Object> captureTtlValues() { HashMap<TransmittableThreadLocal<Object>, Object> ttl2Value = new HashMap<TransmittableThreadLocal<Object>, Object>(); for (TransmittableThreadLocal<Object> threadLocal : holder.get().keySet()) { //将主线程TTL的值存到当前任务中 ttl2Value.put(threadLocal, threadLocal.copyValue()); } return ttl2Value; } 4.2.3.2、任务执行 任务执行的代码如下,在任务执行前回放ThreadLocal,在任务执行后恢复ThreadLocal: 这里都是子线程在操作,因为任务都是子线程执行的 @Override public void run() { Object captured = capturedRef.get(); if (captured == null || releaseTtlValueReferenceAfterRun && !capturedRef.compareAndSet(captured, null)) { throw new IllegalStateException("TTL value reference is released after run!"); } //1、备份子线程ThreadLocal //2、使用主线程提交任务时构建的ThreadLocal副本,将子线程ThreadLocal覆盖 Object backup = replay(captured); try { //3、任务执行 runnable.run(); } finally { //3、使用之前备份的子线程ThreadLocal进行恢复 restore(backup); } } 总结一下,TTL让子线程感知父线程变化的核心思路是,主线程在任务提交时构建ThreadLocal副本,在子线程执行任务时供其使用 ⚠️提交和执行任务会对TTL进行若干操作,理论上对性能有一点点影响,官方性能测试结论说损耗可忽略 TTL官方性能测试 作者:京东物流 刘朝永 来源:京东云开发者 自猿其说

优秀的个人博客,低调大师

Mybatis-SQL分析组件 | 京东物流技术团队

背景 大促备战,最大的隐患项之一就是慢sql,带来的破坏性最大,也是日常工作中经常带来整个应用抖动的最大隐患,而且对sql好坏的评估有一定的技术要求,有一些缺乏经验或者因为不够仔细造成一个坏的sql成功走到了线上,等发现的时候要么是造成了线上影响、报警、或者后置的慢sql采集发现,这时候一般无法快速止损,需要修改代码上线、或者调整数据库索引。 核心痛点: 1、无法提前发现慢sql,可能恶化为慢sql的语句 2、线上出现慢sql后,无法快速止损 解决思路 1、把问题解决在上线之前,最好的办法就是在测试阶段,甚至在开发阶段就发现一个sql的好坏 2、线上发现慢sql后除了改代码上线、调整数据库表索引的方式外,支持热更新的方式替换sql语句。 部门内部,目前大部分数据库框架采用的mybatis,然后基于mybatis本身的实现机制中,开发一个mybatis组件,可以自动对运行的sql进行提取和分析,定制一套默认的分析规则,让sql在开发环境和测试环境执行的时候,就能够做初步的评估,把有问题的慢sql在这个阶段暴露出来;同时具备sql替换功能,在线上出现问题sql的时候,可以通过ducc配置快速完成对一个sql的在线替换,大大降低线上问题的止损时间。 开源方案调研 目前,主流的sql分析组件,核心功能主要放在了两个方向:1、慢sql的分析和优化建议 2、sql的优化重写功能,而且主要偏运维的辅助功能无法做到无侵入的和应用代码进行集成。也就无法实现我们的核心痛点,慢sql提前分析预警和动态sql替换。 设计方案 核心功能:SQL分析预警能力、SQL替换能力 详细设计 主要分为8个功能模块 模块一:core 主要负责组件的接入到mybatis,以及其它模块的编排调用 模块二:config 主要负责组件配置信息的初始化 模块三:extrat 主要通过解析mybatis 相关对象,提取完整的待执行sql 模块四:analysis 主要拼接分析语句,执行explain分析语句并获取分析结果 模块五:rule sql分析规则的加载和初始化,支持自定义规则 目前默认规则(持续扩展): 1、查询未匹配索引 2、匹配索引过滤效果较差 3、返回行数过多 4、使用了文件排序 模块六:score 基于分析结果和配置的评分规则进行匹配打分,优化建议组装 模块七:out 输出模块,对于输出结果进行输出,目前已error日志、MQ两种输出方式 模块八:replace替换模块,可以对sql语句基于ducc配置进行动态替换 使用方法 1、引入依赖jar包 <dependency> <groupId>com.jd.sql.analysis</groupId> <artifactId>sql-analysis</artifactId> <version>1.2-SNAPSHOT</version> </dependency> 2、配置组件xml <configuration> <plugins> <plugin interceptor="com.jd.sql.analysis.core.SqlAnalysisAspect" > <!-- 开启sql分析功能最简配置 --> <property name="analysisSwitch" value="true"/> <!-- 开启sql替换功能最简配置 --> <property name="sqlReplaceModelSwitch" value="true"/> <property name="duccAppName" value="workbench-backend"/> <property name="duccUri" value="ucc://workbench-backend:2d6991cb865f4e6bac6c3e1cf7794cdf@test.ducc.jd.local/v1/namespace/workbench_backend/config/default/profiles/test?longPolling=60000&necessary=false"/> <property name="duccMonitorKey" value="refundBugFlag"/> </plugin> </plugins> </configuration> 3、核心配置项 属性 用途 是否必填 默认值 备注 analysisSwitch 是否开启分析功能 是 false onlyCheckOnce 是否对一个sqlid只分析一次 非 true checkInterval 每个sqlid分析间隔 非 300000毫秒 onlyCheckOnce 为false才生效 exceptSqlIds 需要过滤不分析的sqlid 非 sqlType 分析的sql类型 非 默认select、update 支持 scoreRuleLoadClass 评分规则加载器,用于扩展自定义规则 非 outModel 默认输出方式 非 默认值:LOG 支持LOG、MQ两种方式 outputClass 评分结果输出类,用于扩展自定义结果输出方式 非 sqlReplaceModelSwitch sql替换模块是否开启 非 默认 false duccAppName ducc配置的应用名称(jdos) 非 duccUri ducc uri配置 非 duccMonitorKey sql替换配置文件对应的key 非 4、默认分析效果展示 4.1、慢sql分析效果 4.2、sql动态替换效果 5、实践使用方案 5.1、慢sql分析-日志输出+关键词告警 <configuration> <plugins> <plugin interceptor="com.jd.sql.analysis.core.SqlAnalysisAspect" > <property name="analysisSwitch" value="true"/> </plugin> </plugins> </configuration> 5.2、慢sql分析-日志输出+mq输出+es存储+Kibana分析 <configuration> <plugins> <plugin interceptor="com.jd.sql.analysis.core.SqlAnalysisAspect" > <property name="appName" value="workbench-backend"/> <property name="analysisSwitch" value="true"/> <property name="outputModel" value="mq"/> <property name="mqApp" value="qlstation"/> <property name="mqUser" value="qlstation"/> <property name="mqPassword" value="D1BCC547"/> <property name="mqAddress" value="jmq-testcluster.jd.local:50088"/> <property name="mqTopic" value="jdl_kds_key_node_log"/> </plugin> </plugins> </configuration> 最终效果 5.3、慢sql替换-ducc配置动态更新sql语句 <configuration> <plugins> <plugin interceptor="com.jd.sql.analysis.core.SqlAnalysisAspect" > <property name="sqlReplaceModelSwitch" value="true"/> <property name="duccAppName" value="workbench-backend"/> <property name="duccUri" value="ucc://workbench-backend:2d6991cb865f4e6bac6c3e1cf7794cdf@test.ducc.jd.local/v1/namespace/workbench_backend/config/default/profiles/test?longPolling=60000&necessary=false"/> <property name="duccMonitorKey" value="sqlReplaceConfig"/> </plugin> </plugins> </configuration> 发现慢sql ducc配置 线上sql被动态替换 注意:功能正式修复后,需去掉该配置,该功能仅供应急处理线上问题,不建议作为功能长期使用 性能测试 测试环境千次普通sql查询,每种场景进行了5次测试 未启用插件耗时:11108ms,10237ms,9482ms,7938ms,8196ms 开启sql分析耗时:16619ms,17333ms,16321ms,19057ms,18164ms 实际配置,只有首次执行或者间隔时间执行,单次影响10ms左右) 开启sql替换耗时:10642ms,8803ms,8353ms,8830ms,9170ms 基本无影响 适用场景 1、慢sql预防 2、线上问题止损 优势 1、核心优势:执行时分析sql,区别于传统的依赖sql执行耗时来评估慢sql,直接基于语法和索引进行前置分析,不仅能预防某些坏sql在上线后发现是慢sql,还能给出sql优化建议,可以大限度的避免线上产生慢sql。支持动态对线上sql进行替换,可以对线上问题快速止损。 2、性能:基于性能和不同的使用场景考虑,支持定制化配置,每个sql是否仅进行一次检查、或者按某个时间间隔进行配置。sql替换几乎无损耗。 3、扩展:基于后续sql评分规则的扩展、以及分析结果以不同的方式输出的考虑,支持评分规则、输出方式的自定义扩展。 4、成本:接入成本低,无代码侵入。 作者:京东物流 扈海涛 来源:京东云开发者社区自猿其说Tech

优秀的个人博客,低调大师

文盘Rust -- FFI 浅尝 | 京东云技术团队

rust FFI 是rust与其他语言互调的桥梁,通过FFI rust 可以有效继承 C 语言的历史资产。本期通过几个例子来聊聊rust与C 语言交互的具体步骤。 场景一 调用C代码 创建工程 cargo new --bin ffi_sample Cargo.toml 配置 [package] name = "ffi_sample" version = "0.1.0" edition = "2021" build = "build.rs" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [build-dependencies] cc = "1.0.79" [dependencies] libc = "0.2.146" libloading = "0.8.0" 编写一个简单的c程序sample.c int add(int a,int b){ return a+b; } main.rs use std::os::raw::c_int; #[link(name = "sample")] extern "C" { fn add(a: c_int, b: c_int) -> c_int; } fn main() { let r = unsafe { add(2, 18) }; println!("{:?}", r); } build.rs fn main() { cc::Build::new().file("sample.c").compile("sample"); } 代码目录树 . ├── Cargo.lock ├── Cargo.toml ├── build.rs ├── sample.c └── src └── main.rs cargo run 场景二 使用bindgen 通过头文件绑定c语言动态链接库 修改Cargo.toml,新增bindgen依赖 [package] name = "ffi_sample" version = "0.1.0" edition = "2021" build = "build.rs" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [build-dependencies] cc = "1.0.79" bindgen = "0.65.1" [dependencies] libc = "0.2.146" libloading = "0.8.0" 新增 sample.h 头文件 #ifndef ADD_H #define ADD_H int add(int a, int b); #endif 新增 wrapper.h 头文件 wrapper.h 文件将包括所有各种头文件,这些头文件包含我们想要绑定的结构和函数的声明 #include "sample.h"; 改写build.rs 编译 sample.c 生成动态链接库sample.so;通过bindgen生成rust binding c 的代码并输出到 bindings 目录 use std::path::PathBuf; fn main() { // 参考cc 文档 println!("cargo:rerun-if-changed=sample.c"); cc::Build::new() .file("sample.c") .shared_flag(true) .compile("sample.so"); // 参考 https://doc.rust-lang.org/cargo/reference/build-scripts.html println!("cargo:rustc-link-lib=sample.so"); println!("cargo:rerun-if-changed=sample.h"); let bindings = bindgen::Builder::default() .header("wrapper.h") .parse_callbacks(Box::new(bindgen::CargoCallbacks)) .generate() .expect("Unable to generate bindings"); let out_path = PathBuf::from("bindings"); bindings .write_to_file(out_path.join("sample_bindings.rs")) .expect("Couldn't write bindings!"); } 修改main.rs include 宏引入sample 动态链接库的binding。以前我们自己手写的C函数绑定就不需要了,看看bindings/sample_bindings.rs 的内容与我们手写的函数绑定是等效的 include!("../bindings/sample_bindings.rs"); // #[link(name = "sample")] // extern "C" { // fn add(a: c_int, b: c_int) -> c_int; // } fn main() { let r = unsafe { add(2, 18) }; println!("{:?}", r); } 代码目录树 . ├── Cargo.lock ├── Cargo.toml ├── bindings │ └── sample_bindings.rs ├── build.rs ├── sample.c ├── sample.h ├── src │ └── main.rs └── wrapper.h ffi_sample 工程的完整代码位置,读者可以clone https://github.com/jiashiwen/wenpanrust,直接运行即可 cargo run -p ffi_sample 场景三 封装一个c编写的库 secp256k1是一个椭圆曲线计算的 clib,这玩意儿在密码学和隐私计算方面的常用算法,下面我们从工程方面看看封装secp256k1如何操作 cargo new --lib wrapper_secp256k1 cargo.toml [package] name = "wrapper_secp256k1" version = "0.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [build-dependencies] cc = "1.0.79" bindgen = "0.65.1" [dependencies] git 引入 submodule cd wrapper_secp256k1 git submodule add https://github.com/bitcoin-core/secp256k1 wrapper_secp256k1/secp256k1_sys 工程下新建bindings目录用来存放绑定文件,该目录与src平级 wrapper.h #include "secp256k1_sys/secp256k1/include/secp256k1.h" build.rs use std::path::PathBuf; fn main() { println!("cargo:rustc-link-lib=secp256k1"); println!("cargo:rerun-if-changed=wrapper.h"); let bindings = bindgen::Builder::default() .header("wrapper.h") .parse_callbacks(Box::new(bindgen::CargoCallbacks)) .generate() .expect("Unable to generate bindings"); let out_path = PathBuf::from("bindings"); bindings .write_to_file(out_path.join("bindings.rs")) .expect("Couldn't write bindings!"); } cargo build 通过 编写测试 lib.rs include!("../bindings/secp256k1.rs"); #[cfg(test)] mod tests { use super::*; #[test] fn test_create_pubkey() { // secp256k1返回公钥 let mut pubkey: secp256k1_pubkey = secp256k1_pubkey { data: [0; 64] }; let prikey: u8 = 1; unsafe { let context = secp256k1_context_create(SECP256K1_CONTEXT_SIGN); assert!(!context.is_null()); let ret = secp256k1_ec_pubkey_create(&*context, &mut pubkey, &prikey); assert_eq!(ret, 1); } } } 运行测试 cargo test 报错 warning: `wrapper_secp256k1` (lib) generated 5 warnings error: linking with `cc` failed: exit status: 1 | = note: LC_ALL="C" PATH="/usr/local/Cellar/rust/1.70.0/lib/rustlib/x86_64-apple-darwin/bin:/Users/jiashiwen/local/Cellar/rust/1.70.0/lib/rustlib/x86_64-apple-darwin/lib/libobject-6d1da0e5d7930106.rlib" "/usr/local/Cellar/rust/1.70.0/lib/rustlib/x86_64-apple-darwin/lib/libmemchr-d6d74858e37ed726.rlib" "/usr/local/Cellar/rust/1.70.0/lib/rustlib/x86_64-apple-darwin/lib/libaddr2line-d75e66c6c1b76fdd.rlib" "/usr/local/Cellar/rust/1.70.0/lib/rustlib/x86_64-apple-darwin/lib/libgimli-546ea342344e3761.rlib" "/usr/local/Cellar/rust/1.70.0/lib/rustlib/x86_64-apple-darwin/lib/librustc_demangle-8ad10e36ca13f067.rlib" "/usr/local/Cellar/rust/1.70.0/lib/rustlib/x86_64-apple-darwin/lib/libstd_detect-0543b8486ac00cf6.rlib" "/usr/local/Cellar/rust/1.70.0/lib/rustlib/x86_64-apple-darwin/lib/libhashbrown-7f0d42017ce08763.rlib" "/usr/local/Cellar/rust/1.70.0/lib/rustlib/x86_64-apple-darwin/lib/libminiz_oxide-65e6b9c4725e3b7f.rlib" "/usr/local/Cellar/rust/1.70.0/lib/rustlib/x86_64-apple-darwin/lib/libadler-131157f72607aea7.rlib" "/usr/local/Cellar/rust/1.70.0/lib/rustlib/x86_64-apple-darwin/lib/librustc_std_workspace_alloc-f7d15060b16c135d.rlib" "/usr/local/Cellar/rust/1.70.0/lib/rustlib/x86_64-apple-darwin/lib/libunwind-a52bfac5ae872be2.rlib" "/usr/local/Cellar/rust/1.70.0/lib/rustlib/x86_64-apple-darwin/lib/libcfg_if-1762d9ac100ea3e7.rlib" "/usr/local/Cellar/rust/1.70.0/lib/rustlib/x86_64-apple-darwin/lib/liblibc-f8e0e4708f61f3f4.rlib" "/usr/local/Cellar/rust/1.70.0/lib/rustlib/x86_64-apple-darwin/lib/liballoc-af9a608dd9cb26b2.rlib" "/usr/local/Cellar/rust/1.70.0/lib/rustlib/x86_64-apple-darwin/lib/librustc_std_workspace_core-9777023438fd3d6a.rlib" "/usr/local/Cellar/rust/1.70.0/lib/rustlib/x86_64-apple-darwin/lib/libcore-83ca6d61eb70e9b8.rlib" "/usr/local/Cellar/rust/1.70.0/lib/rustlib/x86_64-apple-darwin/lib/libcompiler_builtins-ea2ca6e1df0449b8.rlib" "-lSystem" "-lc" "-lm" "-L" "/usr/local/Cellar/rust/1.70.0/lib/rustlib/x86_64-apple-darwin/lib" "-o" "/Users/jiashiwen/rustproject/wrapper_secp256k1/target/debug/deps/wrapper_secp256k1-4bf30c62ecfdf2a7" "-Wl,-dead_strip" "-nodefaultlibs" = note: ld: library not found for -lsecp256k1 clang: error: linker command failed with exit code 1 (use -v to see invocation) warning: `wrapper_secp256k1` (lib test) generated 5 warnings (5 duplicates) error: could not compile `wrapper_secp256k1` (lib test) due to previous error; 5 warnings emitted 报错显示找不到编译 secp256k1 相对应的库。 手动编译secp256k1 cd secp256k1_sys ./autogen.sh ./configure make make install 编译完成后,测试通过 其实 secp256k1 有对应的 rust wrapper,我们这里只是展示一下封装的过程。 wrapper_secp256k1 工程的完整代码位置,有兴趣的朋友可以clone https://github.com/jiashiwen/wenpanrust。通过以下操作查看运行结果: clone 项目 git clone https://github.com/jiashiwen/wenpanrust cd wenpanrust update submodule git submodule init git submodule update 编译 secp256k1 cd wrapper_secp256k1/secp256k1_sys ./autogen.sh ./configure make make install run test cargo test -p wrapper_secp256k1 参考资料 Rust FFI (C vs Rust)学习杂记.pdfbindgen官方文档Rust FFI 编程 - bindgen 使用示例 作者:京东科技 贾世闻 来源:京东云开发者社区

优秀的个人博客,低调大师

限速神器RateLimiter源码解析 | 京东云技术团队

作者:京东科技李玉亮 目录指引 限流场景 软件系统中一般有两种场景会用到限流: •场景一、高并发的用户端场景。 尤其是C端系统,经常面对海量用户请求,如不做限流,遇到瞬间高并发的场景,则可能压垮系统。 •场景二、内部交易处理场景。 如某类交易任务处理时有速率要求,再如上下游调用时下游对上游有速率要求。 •无论哪种场景,都需要对请求处理的速率进行限制,或者单个请求处理的速率相对固定,或者批量请求的处理速率相对固定,见下图: 常用的限流算法有如下几种: •算法一、信号量算法。 维护最大的并发请求数(如连接数),当并发请求数达到阈值时报错或等待,如线程池。 •算法二、漏桶算法。 模拟一个按固定速率漏出的桶,当流入的请求量大于桶的容量时溢出。 •算法三、令牌桶算法。 以固定速率向桶内发放令牌。请求处理时,先从桶里获取令牌,只服务有令牌的请求。 本次要介绍的RateLimiter使用的是令牌桶算法。RateLimiter是google的guava包中的一个轻巧限流组件,它主要有两个java类文件,RateLimiter.java和SmoothRateLimiter.java。两个类文件共有java代码301行、注释420行,注释比java代码还要多,写的非常详细,后面的介绍也有相关内容是翻译自其注释,有些描述英文原版更加准确清晰,有兴趣的也可以结合原版注释进行更详细的了解。 使用介绍 RateLimiter使用时只需引入guava jar便可,最新的版本是31.1-jre, 本文介绍的源码也是此版本。 <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>31.1-jre</version> </dependency> 源码中提供了两个直观的使用示例。 示例一、有一系列任务列表要提交执行,控制提交速率不超过每秒2个。 final RateLimiter rateLimiter = RateLimiter.create(2.0); // 创建一个每秒2个许可的RateLimiter对象. void submitTasks(List<Runnable> tasks, Executor executor) { for (Runnable task : tasks) { rateLimiter.acquire(); // 此处可能有等待 executor.execute(task); } } 示例二、以不超过5kb/s的速率产生数据流。 final RateLimiter rateLimiter = RateLimiter.create(5000.0); // 创建一个每秒5k个许可的RateLimiter对象 void submitPacket(byte[] packet) { rateLimiter.acquire(packet.length); networkService.send(packet); } 可以看出RateLimiter的使用非常简单,只需要构造限速器,调用获取许可方法便可,不需要释放许可. 算法介绍 在介绍之前,先说一下RateLimiter中的几个名词: •许可( permit ): 代表一个令牌,获取到许可的请求才能放行。 •资源利用不足( underunilization ): 许可的发放一般是匀速的,但请求未必是匀速的,有时会有无请求(资源利用不足)的场景,令牌桶会有贮存机制。 •贮存许可( storedPermit ): 令牌桶支持对空闲资源进行许可贮存,许可请求时优先使用贮存许可。 •新鲜许可( freshPermit ): 当贮存许可为空时,采用透支方式,下发新鲜许可,同时设置下次许可生效时间为本次新鲜许可的结束时间。 •如下为一个许可发放示例,矩形代表整个令牌桶,许可产生速度为1个/秒,令牌桶里有一个贮存桶,容量为2。 以上示例中,在T1贮存容量为0,许可请求时直接返回1个新鲜许可,贮存容量随着时间推移,增长至最大值2,在T2时收到3个许可的请求,此时会先从贮存桶中取出2个,然后再产生1个新鲜许可,0.5s后在T3时刻又来了1个许可请求,由于最近的许可0.5s后才会下发,因此先sleep0.5s再下发。 RateLimiter的核心功能是限速,我们首先想到的限速方案是记住最后一次下发令牌许可(permit)时间,下次许可请求时,如果与最后一次下发许可时间的间隔小于1/QPS,则进行sleep至1/QPS,否则直接发放,但该方法不能感知到资源利用不足的场景。一方面,隔了很长一段再来请求许可,则可能系统此时相对空闲,可下发更多的许可以充分利用资源;另一方面,隔了很长一段时间再来请求许可,也可能意味着处理请求的资源变冷(如缓存失效),处理效率会下降。因此在RateLimiter中,增加了资源利用不足(underutilization)的管理,在代码中体现为贮存许可(storedPermits),贮存许可值最开始为0,随着时间的增加,一直增长为最大贮存许可数。许可获取时,首先从贮存许可中获取,然后再根据下次新鲜许可获取时间来进行新鲜许可获取。这里要说的是RateLimiter是记住了下次令牌发放的时间,类似于透支的功能,当前许可获取时立刻返回,同时记录下次获取许可的时间。 代码结构和主体流程 代码结构 整体类图如下: RateLimiter类 RateLimiter类是顶级类,也是唯一暴露给使用者的类,它提供了工厂方法来创建RateLimiter方法。 create(double permitsPerSecond) 方法创建的是突发限速器,create(double permitsPerSecond, Duration warmupPeriod)方法创建的是预热限速器。同时它提供了acquire方法用于获取令牌,提供了tryAcquire方法用于尝试获取令牌。该类的内部实现上,一方面有一个SleepingStopWatch 用于sleep操作,另一方面有一个mutexDoNotUseDirectly变量和mutex()方法进行互斥加锁。 SmoothRateLimiter类 该类继承了RateLimiter类,是一个抽象类,含义为平滑限速器,限制速率是平滑的,maxPermits和storedPermits维护了最大存储许可数量和当前存储许可数量;stableIntervalMicros指规定的稳定许可发放间隔,nextFreeTicketMicros指下一个空闲许可时间。 SmoothBursty类 平滑突发限速器,该类继承了SmoothRateLimiter,它存储许可的发放频率同设置的stableIntervalMicros,有一个成员变量maxBurstSeconds,代表最多存储多长时间的令牌许可。 SmoothWarmingUp类 平滑预热限速器,继承了SmoothRateLimiter,与SmoothBursty平级,它的预热算法需要一定的理解成本。 主体流程 获取许可的主体流程如下: 主体流程主要是对贮存许可数量和新鲜许可数量进行计算和更新,得到当前许可请求的等待时间。SmoothBursty算法和SmoothWarmingUp算法共用这一套主体流程,差异主要是贮存许可的管理策略,两种算法的不同策略在两个子类中各自实现,SmoothBursty算法相对简单一些,下面先介绍该算法,然后再介绍SmoothWarmingUp算法。 SmoothBursty算法 限速器创建 采用的是工厂模式创建,源码如下: public static RateLimiter create(double permitsPerSecond) { // permitsPerSecond指每秒允许的许可数. 该方法调用了下面的方法 return create(permitsPerSecond, SleepingStopwatch.createFromSystemTimer()); } // 创建SmoothBursty(固定贮存1s的贮存许可), 然后设置速率 static RateLimiter create(double permitsPerSecond, SleepingStopwatch stopwatch) { RateLimiter rateLimiter = new SmoothBursty(stopwatch, 1.0 /* maxBurstSeconds */); rateLimiter.setRate(permitsPerSecond); return rateLimiter; } 1、SmoothBursty的构造方法相对简单: SmoothBursty(SleepingStopwatch stopwatch, double maxBurstSeconds) { super(stopwatch); this.maxBurstSeconds = maxBurstSeconds; } 2、rateLimiter.setRate的定义在父类RateLimiter中 public final void setRate(double permitsPerSecond) { checkArgument( permitsPerSecond > 0.0 && !Double.isNaN(permitsPerSecond), "rate must be positive"); synchronized (mutex()) { doSetRate(permitsPerSecond, stopwatch.readMicros()); } } 该方法使用synchronized(mutex())方法对互斥锁进行同步,以保证多线程调用的安全,然后调用子类的doSetRate方法。 第二个参数nowMicros传的值是调用了stopwatch的方法,将限速器创建的时间定义为0,然后计算了当前时间和创建时间的时间差,因此采用的是相对时间。 2.1 mutex方法的实现如下: // Can't be initialized in the constructor because mocks don't call the constructor. // 从上行注释可看出,这是因为mock才用了懒加载, 实际上即时加载代码更简洁 @CheckForNull private volatile Object mutexDoNotUseDirectly; // 双重检查锁的懒加载模式 private Object mutex() { Object mutex = mutexDoNotUseDirectly; if (mutex == null) { synchronized (this) { mutex = mutexDoNotUseDirectly; if (mutex == null) { mutexDoNotUseDirectly = mutex = new Object(); } } } return mutex; } 该方法使用了双重检查锁来对锁对象mutexDoNotUseDirectly进行懒加载,另外该方法通过mutex临时变量来解决了双重检查锁失效的问题。 2.2 doSetRate方法的主体实现在SmoothRateLimiter类中: final void doSetRate(double permitsPerSecond, long nowMicros) { // 同步贮存许可和时间 resync(nowMicros); double stableIntervalMicros = SECONDS.toMicros(1L) / permitsPerSecond; this.stableIntervalMicros = stableIntervalMicros; doSetRate(permitsPerSecond, stableIntervalMicros); } 该方法在限速器创建时会调用,创建后调用限速器的setRate重置速率时也会调用。 2.2.1 resync方法用于基于当前时间刷新计算最新的storedPermis和nextFreeTicketMicros. /** Updates {@code storedPermits} and {@code nextFreeTicketMicros} based on the current time. */ void resync(long nowMicros) { // if nextFreeTicket is in the past, resync to now if (nowMicros > nextFreeTicketMicros) { double newPermits = (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros(); storedPermits = min(maxPermits, storedPermits + newPermits); nextFreeTicketMicros = nowMicros; } } 该方法从现实场景上讲,代表的是随着时间的流逝,贮存许可不断增加,但从技术实现的角度,并不是真正的持续刷新,而是仅在需要时调用刷新。该方法如果当前时间小于等于下次许可时间,则贮存许可数量和下次许可时间不需要刷新;否则通过 (当前时间-下次许可时间)/贮存许可的发放间隔计算出的值域最大贮存数量取小,则为已贮存的许可数量,需要注意的是贮存许可数量是double类型的。 限速器使用 限速器常用的方法主要有accquire和tryAccquire。 先说一下accquire方法, 共有两个共有方法,一个是无参的,每次获取1个许可,再一个是整数参数的,每次调用获取多个许可。 // 获取1个许可 public double acquire() { return acquire(1); } // 获取多个许可 public double acquire(int permits) { // 留出permits个许可,得到需要sleep的微秒数. long microsToWait = reserve(permits); // 该方法如果小于等于零则直接返回,否则sleep stopwatch.sleepMicrosUninterruptibly(microsToWait); // 返回休眠的秒数. return 1.0 * microsToWait / SECONDS.toMicros(1L); } 从以上源码可看出,获取许可的逻辑很简单:留出permits个许可,根据返回值决定是否sleep等待。留出许可的方法实现如下: // 预留出permits个许可 final long reserve(int permits) { checkPermits(permits); synchronized (mutex()) { return reserveAndGetWaitLength(permits, stopwatch.readMicros()); } } // 预留出permits个需求,得到需要等待的时间 final long reserveAndGetWaitLength(int permits, long nowMicros) { long momentAvailable = reserveEarliestAvailable(permits, nowMicros); return max(momentAvailable - nowMicros, 0); } abstract long reserveEarliestAvailable(int permits, long nowMicros); reserveEarliestAvailable为抽象方法,实现在SmoothRateLimiter类中,该方法是核心主链路方法,该方法先从贮存许可中获取,如果数量足够则直接返回,否则先将全部贮存许可取出,再计算还需要的等待时间,逻辑如下: final long reserveEarliestAvailable(int requiredPermits, long nowMicros) { // 刷新贮存许可和下个令牌时间 resync(nowMicros); // 返回值为当前的下次空闲时间 long returnValue = nextFreeTicketMicros; // 要消耗的贮存数量为需要的贮存数量 double storedPermitsToSpend = min(requiredPermits, this.storedPermits); // 新鲜许可数=需要的许可数-使用的贮存许可 double freshPermits = requiredPermits - storedPermitsToSpend; // 等待时间=贮存许可等待时间(实现方决定)+新鲜许可等待时间(数量*固定速率) long waitMicros = storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend) + (long) (freshPermits * stableIntervalMicros); // 透支后的下次许可可用时间=当前时间(nextFreeTicketMicros)+等待时间(waitMicros) this.nextFreeTicketMicros = LongMath.saturatedAdd(nextFreeTicketMicros, waitMicros); // 贮存许可数量减少 this.storedPermits -= storedPermitsToSpend; return returnValue; } 该方法有两点说明:1、returnValue为之前计算的下次空闲时间(前面有说RateLimiter采用预支的模式,本次直接返回,同时计算下次的最早空闲时间) 2、贮存许可的等待时间不同的实现方逻辑不同,SmoothBursty算法认为贮存许可直接可用,所以返回0, 后面的SmoothWarmingUp算法认为贮存许可需要消耗比正常速率更多的预热时间,有一定算法逻辑. 至此整个accquire方法的调用链路分析结束,下面再看tryAccquire方法就比较简单了,tryAccquire比accquire差异的逻辑在于tryAccquire方法会判断下次许可时间-当前时间是否大于超时时间,如果是则直接返回false,否则进行sleep并返回true. 方法源码如下: public boolean tryAcquire(Duration timeout) { return tryAcquire(1, toNanosSaturated(timeout), TimeUnit.NANOSECONDS); } public boolean tryAcquire(long timeout, TimeUnit unit) { return tryAcquire(1, timeout, unit); } public boolean tryAcquire(int permits) { return tryAcquire(permits, 0, MICROSECONDS); } public boolean tryAcquire() { return tryAcquire(1, 0, MICROSECONDS); } public boolean tryAcquire(int permits, Duration timeout) { return tryAcquire(permits, toNanosSaturated(timeout), TimeUnit.NANOSECONDS); } public boolean tryAcquire(int permits, long timeout, TimeUnit unit) { long timeoutMicros = max(unit.toMicros(timeout), 0); checkPermits(permits); long microsToWait; synchronized (mutex()) { long nowMicros = stopwatch.readMicros(); // 判断超时微秒数是否可等到下个许可时间 if (!canAcquire(nowMicros, timeoutMicros)) { return false; } else { microsToWait = reserveAndGetWaitLength(permits, nowMicros); } } // 休眠等待 stopwatch.sleepMicrosUninterruptibly(microsToWait); return true; } // 下次许可时间-超时时间<=当前时间 private boolean canAcquire(long nowMicros, long timeoutMicros) { return queryEarliestAvailable(nowMicros) - timeoutMicros <= nowMicros; } SmoothWarmingUp算法 SmoothWarmingUp算法的主体处理流程同SmoothBurstry算法,主要在贮存许可时间计算上的两个方法进行了新实现,该算法不像SmoothBurstry算法那么直观好理解,需要先了解算法逻辑,再看源码。 算法说明 该算法在源码注释中已经描述的比较清晰了,主要思想是限流器的初始贮存许可数量便是最大贮存许可值, 贮存许可执行时按一定算法由慢到快的产生,直至设定的固定速率,以此来达到预热过程。该算法涉及到一些数学知识,如果不是很感兴趣,则了解其主要思想便可。下面详细说一下该算法。 说到该算法前,我们再回头看一下SmoothRateLimiter的贮存许可,贮存许可有当前数量和最大数量,另外还有两个算法逻辑,一个是贮存许可生产的速率控制,再一个是贮存许可消费速率的控制,在Bursty算法中,生产的速率同设定的固定速率,而消费的速率为无穷大(立刻消费,不占用时间);在WarmingUp算法中,需对照下图进行分析: 该图可这样理解,每个贮存许可的消费耗时为右侧梯形面积,梯形面积=(上边长+下边长)/2 * 高. 可以看到每个贮存许可的面积越来越小,直到固定速率的长方形面积。 在限速器初始化时,输入的变量有固定速率和预热时间,另外冷却因子是固定值3;在作者算法中,首先计算的是阈值许可数 = 0.5 * 预热周期 / 固定速率. 然后计算的是最大许可数,我们知道了梯形的面积、上边(大速率)、下边(小速率),便能推到出高,最大许可=阀值许可数 + 高。 void doSetRate(double permitsPerSecond, double stableIntervalMicros) { double oldMaxPermits = maxPermits; double coldIntervalMicros = stableIntervalMicros * coldFactor; thresholdPermits = 0.5 * warmupPeriodMicros / stableIntervalMicros; maxPermits = thresholdPermits + 2.0 * warmupPeriodMicros / (stableIntervalMicros + coldIntervalMicros); slope = (coldIntervalMicros - stableIntervalMicros) / (maxPermits - thresholdPermits); if (oldMaxPermits == Double.POSITIVE_INFINITY) { // if we don't special-case this, we would get storedPermits == NaN, below storedPermits = 0.0; } else { storedPermits = (oldMaxPermits == 0.0) ? maxPermits // initial state is cold : storedPermits * maxPermits / oldMaxPermits; } } 在具体使用中,一个是生产的速率,固定为预热时间/最大许可数,源码如下: double coolDownIntervalMicros() { return warmupPeriodMicros / maxPermits; } 再一个是消费的速率,按如上曲线从右至左的面积=梯形面积+长方形面积,梯形面积=(上边+下边) /2 * 高 ,源码如下: long storedPermitsToWaitTime(double storedPermits, double permitsToTake) { double availablePermitsAboveThreshold = storedPermits - thresholdPermits; long micros = 0; // measuring the integral on the right part of the function (the climbing line) if (availablePermitsAboveThreshold > 0.0) { double permitsAboveThresholdToTake = min(availablePermitsAboveThreshold, permitsToTake); // TODO(cpovirk): Figure out a good name for this variable. double length = permitsToTime(availablePermitsAboveThreshold) + permitsToTime(availablePermitsAboveThreshold - permitsAboveThresholdToTake); micros = (long) (permitsAboveThresholdToTake * length / 2.0); permitsToTake -= permitsAboveThresholdToTake; } // measuring the integral on the left part of the function (the horizontal line) micros += (long) (stableIntervalMicros * permitsToTake); return micros; } 源码分析 了解了以上算法后,再看下面的源码就相对简单了。 static final class SmoothWarmingUp extends SmoothRateLimiter { // 预热时间 private final long warmupPeriodMicros; //斜率 private double slope; //阈值许可 private double thresholdPermits; //冷却因子 private double coldFactor; SmoothWarmingUp( SleepingStopwatch stopwatch, long warmupPeriod, TimeUnit timeUnit, double coldFactor) { super(stopwatch); this.warmupPeriodMicros = timeUnit.toMicros(warmupPeriod); this.coldFactor = coldFactor; } // 参数初始化 @Override void doSetRate(double permitsPerSecond, double stableIntervalMicros) { double oldMaxPermits = maxPermits; double coldIntervalMicros = stableIntervalMicros * coldFactor; thresholdPermits = 0.5 * warmupPeriodMicros / stableIntervalMicros; maxPermits = thresholdPermits + 2.0 * warmupPeriodMicros / (stableIntervalMicros + coldIntervalMicros); slope = (coldIntervalMicros - stableIntervalMicros) / (maxPermits - thresholdPermits); if (oldMaxPermits == Double.POSITIVE_INFINITY) { // if we don't special-case this, we would get storedPermits == NaN, below storedPermits = 0.0; } else { storedPermits = (oldMaxPermits == 0.0) ? maxPermits // initial state is cold : storedPermits * maxPermits / oldMaxPermits; } } // 有storedPermits个贮存许可,要使用permitsToTake个时的等待时间计算 @Override long storedPermitsToWaitTime(double storedPermits, double permitsToTake) { double availablePermitsAboveThreshold = storedPermits - thresholdPermits; long micros = 0; // measuring the integral on the right part of the function (the climbing line) if (availablePermitsAboveThreshold > 0.0) { double permitsAboveThresholdToTake = min(availablePermitsAboveThreshold, permitsToTake); // TODO(cpovirk): Figure out a good name for this variable. double length = permitsToTime(availablePermitsAboveThreshold) + permitsToTime(availablePermitsAboveThreshold - permitsAboveThresholdToTake); micros = (long) (permitsAboveThresholdToTake * length / 2.0); permitsToTake -= permitsAboveThresholdToTake; } // measuring the integral on the left part of the function (the horizontal line) micros += (long) (stableIntervalMicros * permitsToTake); return micros; } // 许可耗时=固定速率+许可值*斜率 private double permitsToTime(double permits) { return stableIntervalMicros + permits * slope; } // 冷却间隔固定为预热时间/最大许可数. @Override double coolDownIntervalMicros() { return warmupPeriodMicros / maxPermits; } } 思考总结 sleep说明和相对时间 RateLimiter内部使用类StopWatch进行了一个相对时间的度量,RateLimiter创建时,时间为0,然后向后累计,sleep时不受interrupt异常影响。 double浮点数 RateLimiter暴露的API的许可数量入参为整数类型,但内部计算时实际是浮点double类型,支持小数许可数量,一方面浮点存在丢失精度,另一方面也不便于理解;是否可以使用整数值得考虑。 只支持单机 RateLimiter的这几种算法只支持单机限流,如要支持集群限流,一种方式是先根据负载均衡的权重计算出单机的限速值,再进行单节点限速;另一种方式是参考该组件使用redis等中心化数量管理的中间件,但性能和稳定性会降低一些。 扩展性 RateLimiter提供了有限的扩展能力,自带的SmoothBursty和SmoothWarmingUp类不是公开类,不能直接创建或调整参数,如关闭贮存功能或调整预热系数等。这种场景需要继承SmoothRateLimiter进行重写,贮存许可的生产和消费算法是容易变化和重写的点,将整个源码拷贝出来进行二次修改也是一种方案。

优秀的个人博客,低调大师

如何优雅的处理异常 | 京东云技术团队

作者:京东零售 秦浩然 一、什么是异常 Java 语言按照错误严重性,从 throwale 根类衍生出 Error 和 Exception 两大派系。 Error(错误): 程序在执行过程中所遇到的硬件或操作系统的错误。错误对程序而言是致命的,将导致程序无法运行。常见的错误有内存溢出,jvm 虚拟机自身的非正常运行,calss 文件没有主方法。程序本生是不能处理错误的,只能依靠外界干预。Error 是系统内部的错误,由 jvm 抛出,交给系统来处理。 Exception(异常): 程序正常运行中,可以预料的意外情况。比如数据库连接中断,空指针,数组下标越界。异常出现可以导致程序非正常终止,也可以预先检测,被捕获处理掉,使程序继续运行。Exception(异常)按照性质,又分为编译异常(受检异常)和运行时异常(非受检异常)。 ◦ 编译异常: 又叫可检查异常,通常时由语法错和环境因素(外部资源)造成的异常。比如输入输出异常 IOException,数据库操作 SQLException。其特点是,Java 语言强制要求捕获和处理所有非运行时异常。通过行为规范,强化程序的健壮性和安全性。 ◦ 运行时异常: 又叫不检查异常 RuntimeException,这些异常一般是由程序逻辑错误引起的,即语义错。比如算术异常,空指针异常 NullPointerException,下标越界 IndexOutOfBoundsException。运行时异常应该在程序测试期间被暴露出来,由程序员去调试,而避免捕获。 二、处理异常方式 代码中,我们最常见到的处理异常的方式就是:try-catch try { // 业务逻辑 } catch (Exception e) { // 捕获到异常的逻辑 } 或者是再进一步区分下异常类型: try { // 业务逻辑 } catch (IOException ie) { // 捕获到IO异常的逻辑 } catch (Exception e) { // 捕获到其他异常的逻辑 } 三、如何抛出异常 我们通常可以用抛出异常的方式来控制代码流程,然后在网关处统一catch异常来返回错误code。这在一定程度上可以简化代码流程控制,如下所示: @Override public UserVO queryUser(Long id) { UserDO userDO = userMapper.queryUserById(id); if (Objects.isNull(userDO)) { throw new RuntimeException("用户不存在"); //用户不存在抛出异常 } return userDO.toVo(); } 上面这种抛出异常的方式,虽然简化了代码流程,但是在存在多种错误场景时,没有办法细分具体的错误类型。如:用户不存在的错误、用户没有权限的错误; 聪明如你,一定想到了自定义异常,如下: @Override public UserVO queryUser(Long id) { UserDO userDO = userMapper.queryUserById(id); if (Objects.isNull(userDO)) { throw new UserNotFoundException(); //用户不存在抛出对应异常 } if(!checkLicence(userDO)) { throw new BadLicenceException(); //用户无权限抛出对应异常 } return userDO.toVo(); } 确实,自定义异常可以解决错误场景细分的问题。进一步的,我们可以对系统流程不同阶段、不同业务类型分别自定义异常,但这需要自定义大量的异常; 四、如何优雅的抛出异常 上面的方式,可以区分出错误场景了,但是还存在一些缺点。如:可读性差、需要定义大量的自定义异常; 那我们下面就去优化上面的问题; 用断言增加代码的可读性; @Override public UserVO queryUser(Long id) { UserDO userDO = userMapper.queryUserById(id); Assert.notNull(userDO, "用户不存在"); //用断言进行参数的非空校验 return userDO.toVo(); } 断言虽然代码简洁、可读性好,但是缺乏像上述自定义异常一样可以明确区分错误场景,这就引出我们的究极方案:自定义断言; 自定义断言; 我们用自定义断言的方式,综合上面自定义异常和断言的优点,在断言失败后,抛出我们制定好的异常。代码如下: • 自定义异常基本类 @Getter @Setter public class BaseException extends RuntimeException { // 响应码 private IResponseEnum responseEnum; // 参数信息 private Object[] objs; public BaseException(String message, IResponseEnum responseEnum, Object[] objs) { super(message); this.responseEnum = responseEnum; this.objs = objs; } public BaseException(String message, Throwable cause, IResponseEnum responseEnum, Object[] objs) { super(message, cause); this.responseEnum = responseEnum; this.objs = objs; } } • 自定义断言接口 public interface MyAssert { /** * 创建自定义异常 * * @param objs 参数信息 * @return 自定义异常 */ BaseException newException(Object... objs); /** * 创建自定义异常 * * @param msg 描述信息 * @param objs 参数信息 * @return 自定义异常 */ BaseException newException(String msg, Object... objs); /** * 创建自定义异常 * * @param t 接收验证异常 * @param msg 描述信息 * @param objs 参数信息 * @return 自定义异常 */ BaseException newException(Throwable t, String msg, Object... objs); /** * 校验非空 * * @param obj 被验证对象 */ default void assertNotNull(Object obj, Object... objs) { if (obj == null) { throw newException(objs); } } /** * 校验非空 * * @param obj 被验证对象 */ default void assertNotNull(Object obj, String msg, Object... objs) { if (obj == null) { throw newException(msg, objs); } } } 上述代码我们可以看出基本设计,就是在我们自定义断言失败后抛出我们自定义异常。 下面是具体的实现案例: • 自定义业务异常类,继承自异常基本类 public class BusinessException extends BaseException { public BusinessException(IResponseEnum responseEnum, Object[] args, String msg) { super(msg, responseEnum, args); } public BusinessException(IResponseEnum responseEnum, Object[] args, String msg, Throwable t) { super(msg, t, responseEnum, args); } } • 响应code枚举接口定义 public interface IResponseEnum { /** * 返回code码 * * @return code码 */ String getCode(); /** * 返回描述信息 * * @return 描述信息 */ String getMsg(); } • 自定义业务异常类断言定义,实现自定义断言失败后对应的自定义异常的定义; public interface BusinessExceptionAssert extends IResponseEnum, MyAssert { @Override default BaseException newException(Object... args) { return new BusinessException(this, args, this.getMsg()); //断言失败后,抛出自定义异常 } @Override default BaseException newException(String msg, Object... args) { return new BusinessException(this, args, msg); //断言失败后,抛出自定义异常 } @Override default BaseException newException(Throwable t, String msg, Object... args) { return new BusinessException(this, args, msg, t); //断言失败后,抛出自定义异常 } } • 用枚举的方式,代替BadLicenceException、UserNotFoundException自定义异常。 public enum ResponseEnum implements IResponseEnum, BusinessExceptionAssert { BAD_LICENCE("0001", "无权访问"), USER_NOT_FOUND("1001", "用户不存在"), ; private final String code, msg; ResponseEnum(String code, String msg) { this.code = code; this.msg = msg; } @Override public String getCode() { return code; } @Override public String getMsg() { return msg; } } 使用实例 自定义断言失败抛出自定义异常 @Override public UserVO queryUser(Long id) { UserDO userDO = userMapper.queryUserById(id); ResponseEnum.USER_NOT_FOUND.assertNotNull(userDO); //自定义断言失败抛出自定义异常 return userDO.toVo(); } 网关处统一catch异常,识别异常场景 public static void main(String[] args) { UserService userService = new UserServiceImpl(new UserMapperImpl()); UserController userController = new UserController(userService); try { UserVO vo = userController.queryUser(2L); //执行业务逻辑 } catch (BusinessException e) { System.out.println(e.getResponseEnum().getCode()); //出现异常,错误code:1001 System.out.println(e.getMessage()); //出现异常,错误msg:用户不存在 } } 五、如何优雅的处理异常 网关处统一处理异常,这属于常规操作,这里不再赘述,简单举例如下: @ControllerAdvice public class BusinessExceptionHandler { @ExceptionHandler(value = BusinessException.class) @ResponseBody public Response handBusinessException(BaseException e) { return new Response(e.getResponseEnum().getCode(), e.getResponseEnum().getMsg()); //统一处理异常 } } 综上,我们采用自定义断言的方式,结合了断言的可读性高的优势和自定义异常区分错误场景的优势。并且,有新增的错误场景,我们只需要在错误码枚举中新增对应枚举即可。

资源下载

更多资源
腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

用户登录
用户注册