ARTS第六周
ARTS是什么?
Algorithm:每周至少做一个leetcode的算法题;
Review:阅读并点评至少一篇英文技术文章;
Tip/Techni:学习至少一个技术技巧;
Share:分享一篇有观点和思考的技术文章。
Algorithm
题目:141. Linked List Cycle
解题思路
本题是链表相关的一道题,题目给出一个链表,问我们这个链表中是否有环。题目中给出了三个例子来帮助我们分析是否有环,我们可以简单理解为,判断链表中是否有节点被超过两个节点的next指针指向,如果有就存在环,如果没有就无环。
?
第一种方法
我们一次遍历链表中的各个节点,并把遍历过节点存到一个set中,每次遍历下一个节点的时候先判断是否它是否存在于set中,如果存在则表示此链表存在环,如果遍历完整个链表都没有发现重复节点,则表示此链表不存在环。
?
第二种方法
设置两个快慢指针,一个指向第一个节点取名为slow,另一个指向第二个节点,取名为fast,slow指针每次走一步,即slow = slow.next
,fast指针每次走两步,即fast = fast.next.next
,遍历过程中判断slow和fast的值是否相等,如果有环,fast指针会追上slow指针,如果没环,最终fast或fast.next的值会是null。
代码
第一种方法
public boolean hasCycle(ListNode head) {
Set<ListNode> nodes = new HashSet<>();
if (head == null || head.next == null){
return false;
}
while (head.next != null){
if (nodes.contains(head)) {
return true;
}else{
nodes.add(head);
head = head.next;
}
}
return false;
}
?
第二种方法
public boolean hasCycle(ListNode head) {
if (head == null || head.next == null){
return false;
}
ListNode quick = head.next;
ListNode slow = head;
while (quick != slow){
if (quick == null || quick.next == null){
return false;
}
quick = quick.next.next;
slow = slow.next;
}
return true;
}
Review
这周在medium上看了一篇文章How to work optimally with relational databases,意思就是“如何更好地使用关系型数据库”。下面我就对这篇文章中的主要内容进行翻译(如有错误,敬请指正),完整原文请点击前文链接前往阅读。
理解关系型数据库
存储
MySQL是一种关系型数据库,它的数据是由一行行tuple表示,所有tuple组成一个relation,每行tuple用它的属性来表示。关系如下图所示:
假设我们有一个借书的应用,我们需要存储所有图书的借阅记录,为了存储它们,我们用下面的sql语句创建一个简单的关系表:
> CREATE TABLE book_transactions ( id INTEGER NOT NULL AUTO_INCREMENT, book_id INTEGER, borrower_id INTEGER, lender_id INTEGER, return_date DATE, PRIMARY KEY (id));
?
这个表看起来就像这样:
book_transactions
------------------------------------------------
id borrower_id lender_id book_id return_date
?
这里的** id **是主键, borrower_id, lender_id, book_id都是外键,在我们使用我们的应用之后会生成几条记录:
book_transactions
------------------------------------------------
id borrower_id lender_id book_id return_date
------------------------------------------------
1 1 1 1 2018-01-13
2 2 3 2 2018-01-13
3 1 2 1 2018-01-13
获取数据
我们的应用有一个dashboard页允许用户去查看他们借阅图书的记录,那么让我们来获取某个用户的借阅记录:
> SELECT * FROM book_transactions WHERE borrower_id = 1;
book_transactions
------------------------------------------------
id borrower_id lender_id book_id return_date
------------------------------------------------
1 1 1 1 2018-01-13
2 1 2 1 2018-01-13
?
这会顺序扫描关系表并把这个用户的数据返回给我们。这看起来很快,因为我们的关系表数据很少。为了能看到精确的查询时间,我们通过下面的命令把** set profiling **设置成true:
> set profiling=1;
?
一旦设置了profiling参数,再此运行查询语句的时候,使用下面的命令就能看到执行时间:
> show profiles;
?
这将返回我们执行的查询命所花费的时间。
Query_ID | Duration | Query
1 | 0.00254000 | SELECT * FROM book_transactions ...
?
执行速度似乎很快。
慢慢地,book_transactions表数据越来越多,因为会一直产生很多的记录。
问题
这在我们的关系表中增加了tuples的数量,接着,用户获取借阅记录的时间开始变得更长。MySQL需要遍历所有的tuples去找到结果。
为了插入很多数据到这个表,我写了下面的存储过程:
DELIMITER //
CREATE PROCEDURE InsertALot()
BEGIN
DECLARE i INT DEFAULT 1;
WHILE (i <= 100000) DO
INSERT INTO book_transactions (borrower_id, lender_id, book_id, return_date) VALUES ((FLOOR(1 + RAND() * 60)), (FLOOR(1 + RAND() * 60)), (FLOOR(1 + RAND() * 60)), CURDATE());
SET i = i+1;
END WHILE;
END //
DELIMITER ;
* It took around 7 minutes to insert 1.5 million data
?
这个存储过程插入了100000条随机数据到我们的book_transactions表,在运行完这个存储过程以后,分析器显示运行时间有略微增长:
Query_ID | Duration | Query
1 | 0.07151000 | SELECT * FROM book_transactions ...
?
让我们多加一点数据,运行上面的存储过程,然后看看会发生什么。随着加入越来越多的数据,查询时长也在随之增加。当有1.5百万数据插入到这张表的时候,查询的响应时间已经明显加长了。
Query_ID | Duration | Query
1 | 0.36795200 | SELECT * FROM book_transactions ...
?
这只是一个简单的涉及整数字段的查询。
随着更多的复合查询,排序查询和计数查询,执行时间将变得更长。
对于单个查询,这似乎不是一个很长的时间,但当我们每分钟运行数千甚至上百万的查询的时候,就会有很大的不同。
将有更长的等待时间,这会降低整个应用的性能。同样的一个查询,执行时间会从2ms增加到370ms。
加速
索引
MySQL和其他数据库提供了索引,一种有助于更快获取数据的数据结构。
在MySQL中,有不同类型的索引:
- 主键索引 -- 加在主键上的索引。默认情况下,主键都是加了索引的。它确保任意两行数据不会出现同一个主键值。
- 唯一索引 -- 唯一索引可以确保没有两行数据会有同一个值,可以使用唯一索引存储多个NULL值。
- 普通索引 -- 可以加在除主键以外的任何字段上。
- 全文索引 -- 全文索引可以帮助针对字符串数据的查询
索引存储的方式主要有两种:
Hash -- 这种方式主要用于精确匹配,不适用于比较。
B-Tree -- 这是存储上面提到的索引类型最常用的方式。
MySQL适用B-Tree作为它的默认索引格式,数据存储在一颗二叉树中,这样能快速检索数据。
用B-Tree组织数据有助于跳过关系表中所有tuples的全表扫描。
上面图中的B-Tree一共有16个节点,假设我们需要找到数字6,我们总共只需要遍历3次就能找到这个数,这有助于提高搜索性能。
所以要提高book_transactions表的性能,让我们在lender_id字段上添加索引。
> CREATE INDEX lenderId ON book_transactions(lender_id)
----------------------------------------------------
* It took around 6.18sec Adding this index
?
上面的命令在lender_id字段上添加了一个索引。重新运行之前的查询语句,让我们来看看它如何影响我们拥有的1.5百万数据的性能。
> SELECT * FROM book_transactions WHERE lender_id = 1;
Query_ID | Duration | Query
1 | 0.00787600 | SELECT * FROM book_transactions ...
?
哇,又像之前那么快了!
现在和只有3条记录的时候一样快,通过正确的索引添加,我们能看到显著的性能提升。
复合索引和单一索引
刚才我们添加的是单一索引,索引也能加到复合字段上。
如果我们的查询包含多个字段,一个复合索引将帮助我们,我们可以使用下面的命令来添加一个复合索引:
> CREATE INDEX lenderReturnDate ON book_transactions(lender_id, return_date);
索引的其他用法
查询不是索引唯一的用途,它们也能被用在ORDER BY条件上,让我们根据lender_id字段对所有记录排序。
> SELECT * FROM book_transactions ORDER BY lender_id;
1517185 rows in set (4.08 sec)
?
4.08 秒,太长了!出了什么问题吗?我们明明加了索引。让我们借助EXPLAIN语句深入研究看看这个查询语句是怎么被执行的。
使用ExPlain
我们添加一个explain语句来看这个查询在我们当前的数据集中究竟是怎么执行的。
> EXPLAIN SELECT * FROM book_transactions ORDER BY lender_id;
?
输出如下:
explain语句返回了几个字段,让我们看看上图中的表格来找到问题所在。
rows:被扫描的行数
filtered:获取数据时被扫描行所占的百分比
type:如果使用索引则给出,ALL表示没有使用索引
possible_keys, key, key_len 都是NULL,表示没有使用索引
那么为什么这个查询没有用索引?
这是因为我们在查询语句使用了select *
,意味着我们会查询表中的所有字段。
索引只有那些加了索引的字段的信息,不包含其他字段,这意味着MySQL需要去主表再次获取数据。
那么我们应该如何写我们的查询语句?
只查需要的字段
要删除转到主表的查询,我们需要只查索引表中存在的值。让我们修改一下查询语句:
> SELECT lender_id FROM book_transactions ORDER BY lender_id;
?
这将在0.46秒内返回结果,哪个更快?但还是有提高的余地。
当这个查询是在1.5百万条记录上完成的,它需要花费更多的时间去加载数据到内存。
使用Limit
我们或许不需要一次性获取1.5百万条数据。使用LIMIT批量获取数据代替获取全量数据是一种更好的方式。
> SELECT lender_id
FROM book_transactions
ORDER BY lender_id LIMIT 1000;
?
有了LIMIT,响应时间大大提升,只需要0.0025秒。我们能使用OFFSET获取下一批数据。
> SELECT lender_id
FROM book_transactions
ORDER BY lender_id LIMIT 1000 OFFSET 1000;
?
这会获取下一批1000条数据,用这种方式我们可以增加offset和limit获取所有数据。但是有一个陷阱,随着offset的增加,查询性能也会下降。
这是因为MySQL会扫描全表数据去到达offset点,因此最好不要使用很高的offset。
计数查询怎么样?
InnoDB引擎有并发写的能力。这使其具有高扩展性并提高了每秒的吞吐量。
但这是有代价的,InnoDB无法为任意表记录数添加缓存计数器。所以,计数器必须通过遍历所有过滤数据完成实时计数,这使得COUNT查询比较慢。
所以推荐在应用逻辑里为大数据量的数据计算总计数。
为什么不给所有字段都加索引
增加索引能提高性能,但它也有代价。应该有效地使用,给更多的字段增加索引会有下面的问题:
- 占用大量的内存,需要更大的机器
- 当我们做删除操作的时候,会重建索引(CPU密集型操作,慢删除)
- 当我们做插入操作的时候,会重建索引(CPU密集型操作,慢插入)
- 更新不会做全局重建索引,所以更新操作时更快的,cpu效率更高
我们知道添加索引很有帮助,但我们不能在所有数据上都添加索引除非它能提升性能。
分区
当我们构建索引的时候,我们只有索引字段的信息,没有索引中不存在的字段数据。
那么,就如前文说的,MySQL需要回表获取别的字段数据,这会减慢执行时间。
我们可以使用分区来解决这个问题。
分区是一种技术,MySQL将一张表的数据拆分成多张表,但还是当一张表来管理。
在表中执行任何类型的操作时,我们需要指定正在使用的分区。随着数据被分解,MySQL有一个较小的数据集可供查询,根据需要确定正确的分区是高性能的关键。
但如果我们一直用同一台机器,它能扩展吗?
分片
当面对庞大的数据集,将所有数据存储在同一台机器上可能会很麻烦。
特定分区可能很重,需要更多查询,而其他分区则较少。所以一个会影响另一个。它们无法单独扩展。
假设最近三个月的数据是最常用的,而较旧的数据使用较少。也许最近的数据大多是更新/创建的,而旧的数据大多只是被读过。
要解决此问题,我们可以将最近三个月的分区移动到另一台计算机。
分片是一种将大数据集分成较小块的方式,然后转移到单独的RDBMS。换句话说,分片也可以称为“水平分区”。
关系数据库能够随着应用程序的增长而扩展。找出正确的索引并根据需要调整基础架构是必要的。
Tip/Techni
本周分享一个cpu占用100%问题的解决方法,主要用到两个命令top和perf,top用来确认引发 CPU 性能问题的来源;perf用来排查出引起性能问题的具体函数。
具体示例
使用top命令查看当前cpu使用率,按下数字1,切换到每个cpu的使用率:
$ top
...
%Cpu0 : 98.7 us, 1.3 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu1 : 99.3 us, 0.7 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
...
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
21514 daemon 20 0 336696 16384 8712 R 41.9 0.2 0:06.00 php-fpm
21513 daemon 20 0 336696 13244 5572 R 40.2 0.2 0:06.08 php-fpm
21515 daemon 20 0 336696 16384 8712 R 40.2 0.2 0:05.67 php-fpm
21512 daemon 20 0 336696 13244 5572 R 39.9 0.2 0:05.87 php-fpm
21516 daemon 20 0 336696 16384 8712 R 35.9 0.2 0:05.61 php-fpm
?
这里有多个使用率在40%左右的进程,我们找其中一个使用 perf 分析下:
# -g 开启调用关系分析,-p 指定 php-fpm 的进程号 21515
$ perf top -g -p 21515
?
按方向键切换到 php-fpm,再按下回车键展开 php-fpm 的调用关系,调用关系最终到了 sqrt 和 add_function。
然后我们需要去源码中寻找这两个方法的踪迹,问题就藏在使用到这两个方法的地方。
Share
今天分享一则轻松一点的讯息Mozilla 抱怨谷歌偏心! 火狐/Edge 加载 YouTube 缓慢,看到这则新闻的时候我瞬间联想到了最近美国和中国的贸易战,两者何其相似,都是强者无理欺负弱者而弱者只能抱怨的案例,自古以来亘古不变的真理就是强者才有话语权,实力说话,不然就要“挨打”,不仅仅是商场、外交是这样,生活中处处都是这个道理的鲜活例子,作为个人也要努力变强,这样才有话语权,才能有更多的自由做你想做的事,过你想过的生活。
原文地址:https://www.cnblogs.com/muxuanchan/p/10163948.html