本周做的是 121. Best Time to Buy and Sell Stock。
给定一个数字,元素的顺序为天数,数值表示股票的价格。找出元素之间的最大差值,即为买入卖出最高利润,被减数的位置必须在减数后面,即要先买入才能卖出。
开始想的最粗暴的方式就是从头遍历,然后将遍历到的值依次与其前面的值相减,最终得到最高利润值,代码如下:
class Solution {
public static int maxProfit(int[] prices) {
int length = prices.length;
if (length == 0 || length == 1) {
return 0;
}
int profit = 0;
for (int i = 1; i < length; i ++) {
int num = prices[i];
for (int j = 0; j < i; j ++) {
int currentProfit = num - prices[j];
if (currentProfit > profit) {
profit = currentProfit;
}
}
}
return profit;
}
}
每次外层遍历都要进行 i - 1 次内层遍历,最终时间复杂度为 O(1 + 2 + 3 +... + n),即 O(N!),性能很差,实际执行 beats 3%。
第二种解法是看了 discuss 部分才知道的,简单来说,本题的本质就是找出右边值减左边值的最大差值。那么我从数组的最后一个值开始遍历,找到右边的最大值,并减去比该值索引小的值,得到的 最大值就是结果,代码如下:
class Solution {
public static int maxProfit(int[] prices) {
int length = prices.length;
if (length == 0 || length == 1) {
return 0;
}
int profit = 0;
int currentRightMax = prices[length - 1];
for (int i = length - 2; i >= 0; i --) {
if (currentRightMax < prices[i]) {
currentRightMax = prices[i];
}else {
profit = Math.max(profit, currentRightMax - prices[i]);
}
}
return profit;
}
}
这样只需要一次遍历即可,时间复杂度为 O(N), 并且不需要占用额外空间。最终执行效率为 1ms,beats 99%。Easy 级别的题目,结果本身很简单,就看思考过程能不能想到这一点。
本次读了 Medium 上的一篇文章: What Truly Makes a Senior Developer,作者讲述了自己认为的成为高级工程师该有的素养。
虽然在不同规模的公司、不同的业务领域,对工程师的技术和工作要求各不相同,但是对于高级工程师而言,还是有几个通用的标准去衡量:
- 作为主程的能力
- 能够引导和培养其他的程序员
- 在组织中解决社交、办公室问题的能力
- 根据实际业务选择合适技术的能力
理想情况下,一个高级工程师的上面四种能力都要突出,但不同的环境可能会有不同的要求,对于初创团队,1、2 点可能更为重要,在大公司中,需要和不同的团队合作,每个团队有不同的 KPI ,不同的能力,此时第 3 点能力就显得尤为重要。
除此之外,作者还提到了三点:
技术之外的能力
作为高级工程师,首先当然要有扎实的技术功底,但除此之外还要有足够的经验,在不同的领域中遇到并解决不同的问题,这会使工程师能够快速的过渡任何一个领域,并根据当前条件做技术决策,其清晰的知道当前的技术决策能够解决什么问题,又做出了怎样的妥协,埋下了怎样的隐患。
深度思考技术方案的方方面面,这比单纯的提高写代码的速度更加的珍贵。
作出决定,为之负责
原文标题为 Making Tough Calls and Living with Them,我理解为 作出决定,为之负责。没有技术方案是完美的,当我们做出一个方案时,这个方案也会埋下隐患,在以后的产品迭代中会变得不合时宜,会有问题。作为高级工程师,我们应该时刻把握当前系统中架构、问题甚至某些细节,并根据产品的迭代不断对其优化。
虽然对各种框架、语言的细节都做到了如指掌不太现实,但我们还是要尽可能的去了解更多。
极度开放,博采众长
这里的博采众长是指接纳各种技术,不要对技术抱有偏见。大家都知道程序员界的各种鄙视链,这些都只是刁侃,如果真的当真就有点说不过去了。在高级工程师眼里,语言、框架都是工具,什么最合适就用什么。
当你抱有这种心态时,就会更加主动的学习了解不同的技术,这时候自己也会距离高级工程师更进一步了。
作者最后对初中高级工程师的表现做了一些对比:
- 初级工程师通常根据教程或者学校学到的东西解决问题,并且习惯追求新潮的技术
- 中级工程师了解当前问题所在,并会想着去维护改进代码质量,但依然只看到了其中的一部分
- 高级工程师明白,任何方案都会有相应的问题和风险,其会从整个团队的角度思考方案,使其更加的易于维护,易学和容易调试。
另外很重要的一点就是求知欲,一个人只有能够主动的学习,成长,才有成为高级工程师的可能。
以上就是本周 Review,每次看这种文章都是对自身的一次反思,帮助自己矫正方向、方法,进一步的学习提高。
分享一个在看 《MySQL 技术内幕》时看到的联合索引的用法,创建联合索引如下:
mysql> create table t(
-> a int,
-> b int,
-> primary key (a),
-> key idx_a_b (a,b)
-> ) ENGINE=INNODB;
Query OK, 0 rows affected (0.07 sec)
索引的叶子节点采用字典排序,先根据 a 的值进行排序,在根据 b 的排序。因此其在第一个键值的基础上对第二个键值做了排序,当使用 a 查询然后按照 b 排序时,在查询时可以减少一次排序。
如下方式可以直接通过联合索引获得,而不需要在进行一次排序, 通过这个特性可以进一步提到某些情况下的查询速度。
mysql> select * from t where a = xxx ORDER BY b;
但如果忽略 a 直接用 b 查询则无法使用联合索引,无法根据索引查询并且还会进行一次排序。
mysql> select * from t where b = xxx;
最近按照耗子叔的练级攻略去那些 “硬核知识”了,目前在读的是《MySQL 技术内幕》,本次 Share 分享下 第六章 《锁》的阅读笔记吧。
锁是数据库用于管理共享资源 的并发访问的机制,使用锁的目的是为了支持对共享资源进行并发性访问,保证数据的完整性和一致性。这是数据库区别于文件系统的一个关键特性。
InnoDB 支持行级锁,但数据库内部很多地方也会用到锁机制,比如操作缓冲池中的 LRU 列表。
是一种轻量级锁,其要求锁定时间必须非常短,否则性能会非常差。InnoDB 中可以分为 mutex(互斥量) 和 rwlock(读写锁)。其目的是保证并发线程操作临界资源的的正确性并且通常没有死锁检测机制。
可以通过 show engine innodb mutex
命令查看 latch 相关信息,在 DEBUG 模式下可以看到更多的信息。
mysql> show engine innodb mutex;
+--------+-----------------------------+-----------+
| Type | Name | Status |
+--------+-----------------------------+-----------+
| InnoDB | rwlock: dict0dict.cc:2687 | waits=127 |
| InnoDB | rwlock: dict0dict.cc:2687 | waits=1 |
| InnoDB | rwlock: dict0dict.cc:2687 | waits=3 |
| InnoDB | rwlock: dict0dict.cc:2687 | waits=4 |
| InnoDB | rwlock: dict0dict.cc:2687 | waits=4 |
| InnoDB | rwlock: dict0dict.cc:2687 | waits=6 |
| InnoDB | rwlock: dict0dict.cc:2687 | waits=25 |
| InnoDB | rwlock: dict0dict.cc:1184 | waits=928 |
| InnoDB | rwlock: fil0fil.cc:1381 | waits=21 |
| InnoDB | rwlock: log0log.cc:838 | waits=39 |
| InnoDB | rwlock: btr0sea.cc:195 | waits=2 |
| InnoDB | sum rwlock: buf0buf.cc:1460 | waits=208 |
+--------+-----------------------------+-----------+
12 rows in set (0.22 sec)
lock 就是我们通常提到的锁,其对象是事务,用来锁定表、页、行等数据库中的对象。并且锁定的对象会根据事务的隔离级别进行释放,一般是在事务提交或者回滚后释放,并且有死锁检测机制。
可以通过 show engine innodb status
命令和查看
information_schema 中的表 INNODB_TRX, INNODB_LOCKS, INNODB_LOCK_WAITS 查看锁的信息。
。 | lock | latch |
---|---|---|
对象 | 事务 | 线程 |
保护 | 数据库内容 | 内存数据结构 |
持续时间 | 整个事务过程 | 临界资源 |
模式 | 行锁、表锁、意向锁 | 读写锁,互斥量 |
死锁 | 有死锁检测机制 | 无死锁检测与处理机制 |
存在于 | Lock Manager 的哈希表中 | 每个数据结构对象中 |
InnoDB 实现了两种标准的行级锁:
- 共享锁(S LOCK): 允许事务读一行数据
- 排他锁(X LOCK): 允许事务删除或者更新一行数据
下面是两种锁的兼容性:
. | X | S |
---|---|---|
X | 不兼容 | 不兼容 |
S | 不兼容 | 兼容 |
X 锁和任何锁都不兼容,而 S 锁仅和 S 锁兼容,注意这里的 S 和 X 都是行锁,兼容指的是对同一行记录锁的兼容情况。
如果事务 T1 已经获得了行 r 的 S 锁,那么其他事务也可以获得行 r 的 S 锁,因为读取不会改变行数据,这种情况称为锁兼容。但如果是 X 排他锁,如果其他事务想获得行 r 的排他锁,则必须等待前面的事务完成,这种情况就称为锁不兼容。
意向锁即为表级别的锁,设计的目的是为了在一个事务中揭示下一行将被请求的锁类型。其支持两种意向锁:
- 意向共享锁(IS Lock): 事务想要获得一张表中某几行的共享锁
- 意向排他锁(IX Lock): 事务想要获得一张表中某几行的排他锁
如果要对页上的某一行上 X 锁,则分别需要对数据库、表、页上意向锁 IX,最后对行上 X 锁,必须等待粗粒度上的完成,行才能上 X 锁。
InnoDB 锁的兼容性:
。 | IS | IX | S | X |
---|---|---|---|---|
IS | 兼容 | 兼容 | 兼容 | 不兼容 |
IX | 兼容 | 兼容 | 不兼容 | 不兼容 |
S | 兼容 | 不兼容 | 兼容 | 不兼容 |
X | 不兼容 | 不兼容 | 不兼容 | 不兼容 |
简单来说就是 InnoDB 在对行上锁之前,会对数据库、页、表都上意向锁,在这过程中也会判断意向锁与已有锁的兼容性,如果不兼容必须等待上一个锁释放后在可以加锁,也就会造成事务等待。
可以通过三张表查看完整的事务和锁信息
- innodb_trx: 事务表,记录每个事务的相关信息
- innodb_locks: 锁表,记录的每个锁的相关信息
- innodb_lock_waits: 事务等待情况
mysql> show create table information_schema.INNODB_TRX\G;
*************************** 1. row ***************************
Table: INNODB_TRX
Create Table: CREATE TEMPORARY TABLE `INNODB_TRX` (
`trx_id` varchar(18) NOT NULL DEFAULT '',
`trx_state` varchar(13) NOT NULL DEFAULT '',
`trx_started` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
`trx_requested_lock_id` varchar(81) DEFAULT NULL,
`trx_wait_started` datetime DEFAULT NULL,
`trx_weight` bigint(21) unsigned NOT NULL DEFAULT '0',
`trx_mysql_thread_id` bigint(21) unsigned NOT NULL DEFAULT '0',
`trx_query` varchar(1024) DEFAULT NULL,
`trx_operation_state` varchar(64) DEFAULT NULL,
`trx_tables_in_use` bigint(21) unsigned NOT NULL DEFAULT '0',
`trx_tables_locked` bigint(21) unsigned NOT NULL DEFAULT '0',
`trx_lock_structs` bigint(21) unsigned NOT NULL DEFAULT '0',
`trx_lock_memory_bytes` bigint(21) unsigned NOT NULL DEFAULT '0',
`trx_rows_locked` bigint(21) unsigned NOT NULL DEFAULT '0',
`trx_rows_modified` bigint(21) unsigned NOT NULL DEFAULT '0',
`trx_concurrency_tickets` bigint(21) unsigned NOT NULL DEFAULT '0',
`trx_isolation_level` varchar(16) NOT NULL DEFAULT '',
`trx_unique_checks` int(1) NOT NULL DEFAULT '0',
`trx_foreign_key_checks` int(1) NOT NULL DEFAULT '0',
`trx_last_foreign_key_error` varchar(256) DEFAULT NULL,
`trx_adaptive_hash_latched` int(1) NOT NULL DEFAULT '0',
`trx_adaptive_hash_timeout` bigint(21) unsigned NOT NULL DEFAULT '0',
`trx_is_read_only` int(1) NOT NULL DEFAULT '0',
`trx_autocommit_non_locking` int(1) NOT NULL DEFAULT '0'
) ENGINE=MEMORY DEFAULT CHARSET=utf8
1 row in set (0.00 sec)
mysql> show create table information_schema.INNODB_LOCKS\G;
*************************** 1. row ***************************
Table: INNODB_LOCKS
Create Table: CREATE TEMPORARY TABLE `INNODB_LOCKS` (
`lock_id` varchar(81) NOT NULL DEFAULT '',
`lock_trx_id` varchar(18) NOT NULL DEFAULT '',
`lock_mode` varchar(32) NOT NULL DEFAULT '',
`lock_type` varchar(32) NOT NULL DEFAULT '',
`lock_table` varchar(1024) NOT NULL DEFAULT '',
`lock_index` varchar(1024) DEFAULT NULL,
`lock_space` bigint(21) unsigned DEFAULT NULL,
`lock_page` bigint(21) unsigned DEFAULT NULL,
`lock_rec` bigint(21) unsigned DEFAULT NULL,
`lock_data` varchar(8192) DEFAULT NULL
) ENGINE=MEMORY DEFAULT CHARSET=utf8
1 row in set (0.00 sec)
mysql> show create table information_schema.INNODB_LOCK_WAITS\G;
*************************** 1. row ***************************
Table: INNODB_LOCK_WAITS
Create Table: CREATE TEMPORARY TABLE `INNODB_LOCK_WAITS` (
`requesting_trx_id` varchar(18) NOT NULL DEFAULT '',
`requested_lock_id` varchar(81) NOT NULL DEFAULT '',
`blocking_trx_id` varchar(18) NOT NULL DEFAULT '',
`blocking_lock_id` varchar(81) NOT NULL DEFAULT ''
) ENGINE=MEMORY DEFAULT CHARSET=utf8
1 row in set (0.00 sec)
指 InnoDB 存储引擎通过行多版本控制的方式来读取当前执行时间数据库中的行数据,如果读取的数据正在进行 update 或者 delete 操作,这是读取操作不会等待行锁的释放,而是读行的一个快照。
快照指的是该行之前版本的数据,是通过 undo 段完成的,因此快照数据本身没有任何开销。因此该机制可以极大的提高数据库的并发性,在 InnoDB 中这是默认的读取方式,即读取不会占用和等待表上的锁,但具体的读取方式也会受隔离级别的影响,这个在后面事务一章在详细了解。
在某些情况下要求数据库支持加锁语句,即使是 SELECT 的只读操作,InnoDB 支持两种一致性锁定读的操作:
- SELECT ... FOR UPDATE, 对读取行加 X 锁
- SELECT .. LOCK IN SHARE MODE, 对读取行加 S 锁
两种操作都必须在一个事务中,当事务提交后,锁也就释放了,因此在使用上面两个语句中时,必须加上 BEGIN、START TRANSACTION 或者 SET AUTOCOMMIT=0。
对于自增长的列,在 InnoDB 存储引擎中有一个自增长计数器,对于自增长列的插入操作会依据自增长计数器值加 1 后赋予自增长列。该方式称为 AUTO-INC Locking,其实是一种特殊的表锁机制,锁是在执行完插入操作语句后释放,而不是等待事务结束后释放,可以提升性能。但其对于自增长列的并发插入性能较。
从 MySQL 5.1.22 版本开始提供了一种轻量级互斥量的自增长实现机制,并提供了 innodb_autoinc_lock_mode
参数来控制自增长模式。这样极大提高了自增长值插入的性能。
mysql> show variables like "innodb_autoinc_lock_mode";
+--------------------------+-------+
| Variable_name | Value |
+--------------------------+-------+
| innodb_autoinc_lock_mode | 1 |
+--------------------------+-------+
1 row in set (0.01 sec)
InnoDB 会对外键自动加一个索引。
对于外键的插入或者更新,首先会查询父表的记录,但不是使用的一致性非锁定读的方式,因为这样会发生数据不一致的问题,因此会采用 SELECT ... LOCK IN SHARE MODE 的方式给父表加一个 S 锁,如果父表已经加上了 X 锁,子表上的操作会被阻塞。
- Record Lock: 单个行记录的锁
- Gap Lock: 间隙锁,锁定一个范围,但不包含行记录本身
- Next-key Lock: 相当于 Gap + Record, 锁定一个范围,并且锁定记录本身
对于 Next-Key Lock 算法,其设计的目的是为了解决 Phantom Problem (幻象问题),利用该算法实现的 Next-Key Locking 锁,其锁定的不是单个值,而是一个范围。除了 next-key locking 还有 previous-key locking 技术。
假设有一个索引有 10、11、13、20 四个值,那么其锁定范围如下:
# Next-Key Locking
(- ∞ 10]
(10, 11]
(11, 13]
(13, 20]
(20, + ∞ )
# Previous-Key Locking
(- ∞ 10)
[10, 11)
[11, 13)
[13, 20)
[20, + ∞ )
唯一索引锁降级
当查询的索引含有唯一属性时,比如主键索引或者唯一索引,InnoDB 会将 next-key lock 会优化降级为 record lock,即仅锁住索引本身而不是范围。但注意其仅存于查询所有的唯一列,若唯一索引由多个列组成,对于其中的某一列查询时是 range 查询,而不是 point 查询,仍然是 Next-Key Lock 锁定。
辅助索引查询
对于辅助索引,并不存在锁降级的情况。因此始终会使用 Next-Key Lock 锁住一个范围,并且 InnoDB 还会对辅助索引的下一个键值加上 gap lock (间隙锁)。
示例如下:
# 1. 构建表
mysql> create table z (a int, b int, primary key(a), key(b));
mysql> insert into z select 1,1;
mysql> insert into z select 3,1;
mysql> insert into z select 5,3;
mysql> insert into z select 7,6;
mysql> insert into z select 10,8;
# 2. 读加锁
mysql> SELECT * FROM z WHERE b = 3 FOR UPDATE;
上面的 for update 语句,因为 b 是辅助索引,因此采用 Next-Key Lock 锁机制,加上的锁为 (1, 3],同时还会对下个键值加上间隙锁 (3, 6)。 此时下面语句会阻塞:
# a = 5 的行被加了 X 锁因此阻塞
mysql> SELECT * FROM z WHERE a = 5 LOCK IN SHARE MODE;
# 主键 4 没有问题,b 为 2 在 (1,3] 锁定范围内因此会阻塞
mysql> INSERT INTO z SELECT 4,2;
# 主键 6 没有问题,但是 b 为 5 在间隙锁 (3, 6) 范围内,因此会阻塞
mysql> INSERT INTO z SELECT 6,5;
从上面可以知道,Gap Lock 的作用是防止事务将记录插入到同一范围内,导致 Phantom Problem (幻象问题)。
Phantom Problem (幻象问题) 指的是在同一个事务下,连续执行两次同样的 SQL 可能导致不同的结果,最常见的现象是连续两次相同的查询语句返回了不一样的数据。
发生幻想问题的原因是当前事务在执行时,其他事务发生了数据的提交,从而导致当前事务查询出的数据前后不一致。
InnoDB 默认的事务隔离级别为 REPEATABLE READ,在该隔离级别下采用 Next-Key Locking 方式加锁,以避免幻象问题,示例如下:
mysql> select * from z where a > 2 for update;
该查询语句的加锁范围为 (2, + ∞),对该范围内的值加 X 锁,因此任何对该范围内数据的更改都是不允许的,从而避免幻象问题。
脏读
首先了解下脏数据,脏数据指的是未提交的数据。脏读指的就是在不同事务下,当前事务读到了另外事务的未提交的数据,违反了数据库的隔离性。
当数据库的隔离级别设置为 READ UNCOMMITTED
时会引发该问题,但实际情况并不常见,因为数据库通常不采取该隔离级别。
但READ UNCOMMITTED
也有用武之地,当在主从架构中,slave 节点的查询并不需要特别精确时可以设置为该级别。
不可重复读
其实就是前面提到的幻象问题,指的是当前事务中,多次读到同一个数据集合,此时如果其他事务对该数据集合进行了 DML 操作,则导致当前事务再次读取时会读到不一样的数据。违反了一致性的要求。
一般来说,不可重复读是可以接受的,因为读到的数据是已经提交的数据。InnoDB 引擎默认的事务隔离级别为 READ REPEATABLE,采用 Next-Key Locking 算法可避免不可重复读的问题。
丢失更新
简单来说就是一个事务的更新操作被另一个事务的更新操作覆盖。
一般来说,将事务的操作串行化,每次更新数据前的读取添加 X 排它锁,在数据库层面保证避免该问题。
因为锁的兼容性问题,因此数据库在执行事务时一个锁通常要等待另一个事务中的锁释放资源,这就是阻塞,这样可以确保事务能够并发且正常执行。
有下面几个阻塞相关的参数:
mysql> show variables like "innodb_lock_wait_timeout";
+--------------------------+-------+
| Variable_name | Value |
+--------------------------+-------+
| innodb_lock_wait_timeout | 50 |
+--------------------------+-------+
1 row in set (0.01 sec)
mysql> show variables like "innodb_rollback_on_timeout";
+----------------------------+-------+
| Variable_name | Value |
+----------------------------+-------+
| innodb_rollback_on_timeout | OFF |
+----------------------------+-------+
1 row in set (0.00 sec)
- innodb_lock_wait_timeout: 控制等待时间,默认 50 s,如果发生超时 MySQL 会抛出 1205 错误。该参数是动态的,可以在数据库运行时调整。
- innodb_rollback_on_timeout: 等待超时时是否回滚事务,默认是 OFF 代表不回滚,是静态参数不可以在启动时修改。
由此我们知道当事务失败时 InnoDB 是不回滚的,这是十分危险的行为,我们需要对其进行处理,确定是提交还是回滚。
定义: 死锁是指两个或者两个事务以上在执行的过程中,因争夺锁资源而造成的互相等待情况。
作为程序员对死锁的概念应该不会陌生。解决死锁的方式可以有如下几种:
主动回滚
最简单的方式就是事务不要等待,直接回滚,这样不会造成死锁,但会严重降低并发性能。
超时
当事务互相等待时,对事务等待设置一个超时阈值,对超时的事务进行回滚,这样一个事务回滚后释放资源,另一个事务就可以正常执行了。InnoDB 中通过 innodb_lock_wait_timeout
来设置超时的时间。
超时机制一般会按照 FIFO 的方式进行回滚,如果此时回滚的事务所占权重较大,占用了较多的 undo log,回滚就会导致性能问题。
wait-for graph 等待图
这是当前包括使用 InnoDB 引擎的 MySQL 数据库在内的大多数数据库采用的一种更为主动的死锁检测机制。
等待图包含两种信息:
- 锁的信息链表
- 事务的等待链表
如果某个事务所占的资源需要等待其他事务释放锁,则将该事务指向持有锁的事务,最终会形成一个等待图,如果图中存在回路,则表示存在死锁。
死锁检测,也就是等待图的回路检测,通常是采用 DFS 深度优先的算法实现,从 InnoDB 1.2.1 开始对等待图死锁检测进行了优化,采用了非递归的方式进行,并且 InnoDB 在检测到死锁时,默认回滚 undo 量最小的事务。
死锁概率
P ≈ n²r^4/4R²
- n: 系统的事务的数量,越大则死锁概率越高
- r: 每个事务操作的数量,越多则死锁概率越大
- R: 操作数据的集合,越小则发生死锁概率越大
关于死锁最常见的就是AB-BA 死锁
,即 A 等待 B,B 又在等待 A。另外一种情况,当前事务持有了待插入记录的下一个记录的 X 锁,但是在等待队列中存在一个 S 锁时则可能发生死锁。
之前提到 InnoDB 在进行事务超时时不会回滚。但是当死锁发生抛出 1213 错误时会进行回滚,因此当程序中捕获到了数据库的 1213 错误时并不需要对其进行回滚操作。
锁升级是指将当前锁的粒度降低,比如将 1000 个行锁升级为表锁,或者页锁升级为表锁,这是因为数据库任务锁是一种稀有资源,为了避免锁的开销,因此会进行锁升级。
InnoDB 不存在 锁升级的问题,因为其不是根据每个记录产生行锁的,而是根据每个事务访问的每个页对锁进行管理,采用的是位图的方式,因此不管一个事务锁住页中的一个记录还是多个记录,开销都是一致的。
以上是本章锁的简记,有个疑惑是最后锁升级后的讲解,InnoDB 根据每个事务访问的每个页对锁进行管理,如何管理的还没搞清楚。锁和事务是密切关联的,等看完下一章 《事务》后再来回顾总结吧。