开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

Innodb到底是怎么加锁的

发表于 2021-11-09

学完本文后:妈妈再也不用担心我不知道InnoDB是怎么加锁的了!

流传较广,但是错误的一个观点

不知道从什么时候开始,下边这个错误的观点开始被广泛的流传:

在使用加锁读的方式读取使用InnoDB存储引擎的表时,当在执行查询时没有使用到索引时,行锁会被转换为表锁。

这里强调一点,对于任何INSERT、DELETE、UPDATE、SELECT ... LOCK IN SHARE MODE、SELECT ... FOR UPDATE语句来说,InnoDB存储引擎都不会加表级别的S锁或者X锁(我们这里不讨论表级意向锁的添加),只会加行级锁。所以即使对于全表扫描的加锁读语句来说,也只会对表中的记录进行加锁,而不是直接加一个表锁。

另外,很多小伙伴都会问:“这个语句加什么锁”,其实这是一个伪命题,因为一个语句需要加什么锁受到很多方面的影响,如果有人问你某某语句会加什么锁,那你可以直接回怼:真不专业!

我们稍后给大家详细分析一下影响加锁的因素都有哪些,以及从源码的角度看一下InnoDB到底是如何加锁的,希望小伙伴看完后会惊呼:真tm的简单!

不过在进行讨论前我们需要申明一下,我们讨论的只是InnoDB加的事务锁,即为了避免脏写、脏读、不可重复读、幻读这些现象带来的一致性问题而加的锁,并不是为了在多线程访问共享内存区域时而加的锁(比方说两个不同事务所在的线程想读写同一个页面时,需要进行加锁保护),也不包括server层添加的MDL锁。

本文所参考的源码版本为5.7.22。

事务锁到底是什么

锁是一个内存结构,InnoDB中用lock_t这个结构来定义:

image_1fjv1ksadeen1lj6mhb1erl1qc158.png-68kB

不论是行锁,还是表锁都用这个结构来表示。我们给大家画个图:

image_1fjv1nvdh1lr611l41rdp9nmgor5l.png-199.5kB

其中的type_mode是用于区分这个锁结构到底是行锁还是表锁,如果是表锁的话是意向锁、直接对表加锁、还是AUTO-INC锁,如果是行锁的话,具体是正经记录锁、gap锁还是next-key锁。

小贴士:

在InnoDB的实现中,InnoDB的行锁是与记录一一对应的。即使是对于gap锁来说,在实现上也是为某条记录生成一个锁结构,然后该锁结构的类型是gap锁而已,并不是专门为某个区间生成一个锁结构。该gap锁的功能就是每当有别的事务插入记录时,会检查一下待插入记录的下一条记录上是否已经有一个gap锁的锁结构,如果有的话就进入阻塞状态。

我们平时所说的加锁就是在内存中生成这样的一个锁结构(除了生成锁结构,还有一种称作隐式锁的加锁方式,不用生成锁结构)。当然,如果为1条记录加锁就要生成一个锁结构,那岂不是太浪费了!设计InnoDB的大叔提出了一种优化方案,即同一个事务,在同一个页面上加的相同类型的锁都放在同一个锁结构里。

各种类型的锁是如果通过type_mode区分、各种锁都有什么作用,以及如何减少生成锁结构的细节我们这里就不展开了,那又要花费超长的篇幅,大家可以到《MySQL是怎样运行的:从根儿上理解MySQL》书籍中查看,我们下边来看具体的加锁细节。

准备工作

为了故事的顺利发展,我们先创建一个表hero:

1
2
3
4
5
6
7
sql复制代码CREATE TABLE hero (
number INT,
name VARCHAR(100),
country varchar(100),
PRIMARY KEY (number),
KEY idx_name (name)
) Engine=InnoDB CHARSET=utf8;

然后向这个表里插入几条记录:

1
2
3
4
5
6
sql复制代码INSERT INTO hero VALUES
(1, 'l刘备', '蜀'),
(3, 'z诸葛亮', '蜀'),
(8, 'c曹操', '魏'),
(15, 'x荀彧', '魏'),
(20, 's孙权', '吴');

然后现在hero表就有了两个索引(一个二级索引,一个聚簇索引),示意图如下:

image_1fjv5sad81rbm18lvqb6mkq9pb7f.png-58.3kB

加锁受哪些因素影响

一条语句加什么锁受多种因素影响,如果你不能确认下边这些因素的时候,最好不要抢先发言说”XXX语句对XXX记录加了什么锁”:

  • 事务的隔离级别
  • 语句执行时使用的索引类型(比如聚簇索引、唯一二级索引、普通二级索引)
  • 是否是精确匹配
  • 是否是唯一性搜索
  • 具体执行的语句类型(SELECT、INSERT、DELETE、UPDATE)
  • 是否开启innodb_locks_unsafe_for_binlog系统变量
  • 记录是否被标记删除

这里边有几个概念大家可能不是很清楚,我们先解释一下。

扫描区间

比方说下边这个查询:

1
sql复制代码SELECT * FROM hero WHERE name <=  'l刘备' AND country = '魏';

MySQL可以使用下边两种方式来执行上述查询:

  • 使用二级索引idx_name执行上述查询,那么就需要扫描name值在(-∞, 'l刘备']这个区间中的所有二级索引记录,针对获取到的每一条二级索引记录,都需要执行回表操作来获取相应的聚簇索引记录。
  • 直接扫描所有的聚簇索引记录,即进行全表扫描。此时相当于扫描number值在(-∞, +∞)这个区间中的所有聚簇索引记录。

优化器会计算上述二种方式哪个成本更低,选用成本更低的那种来执行查询。

当优化器使用二级索引执行查询时,我们把(-∞, 'l刘备']称作扫描区间,意味着需要扫描name列值在这个区间中的所有二级索引记录,我们也可以把形成这个扫描区间的条件name <= 'l刘备'称作是形成这个扫描区间的边界条件;当优化器使用全表扫描执行查询时,我们把(-∞, +∞)称作扫描区间,意味着需要扫描number值在这个区间中的所有聚簇索引记录。

在执行一个查询的过程中,可能会用到多个扫描区间,如下所示:

1
sql复制代码SELECT * FROM hero WHERE name < 'l刘备' OR name > 'x荀彧';

如果优化器采用二级索引idx_name执行上述查询时,那么对应的扫描区间就是(-∞, l刘备)以及('x荀彧', +∞),即需要扫描name值在上述两个扫描区间中的记录。

每当InnoDB需要扫描一个扫描区间中的记录时,都需要分两步:

  • 先通过索引对应的B+树,从根页面开始一路向下定位,直到定位到叶子节点中在扫描区间中的第一条记录。
  • 之后就可以不需要继续从根节点定位了,而是通过记录的next_record属性直接找到扫描区间的下一条记录即可(页面之间通过双向链表连接,找完一个页面中的记录后,可以顺着双向链表再去下一个页面中去找属于同一个扫描区间的记录)。

也就是说在扫描某个扫描区间的记录时,只有定位第1条记录的时候稍微麻烦点儿,其他记录只需要顺着链表(单个页面中的记录连成一个单向链表,不同的页面之间是双向链表)扫描即可。

精确匹配

对于形成扫描区间的边界条件来说,如果是等值匹配的条件,我们就把对这个扫描区间的匹配模式称作精确匹配。比方说:

1
ini复制代码SELECT * FROM hero WHERE name = 'l刘备' AND country = '魏';

如果使用二级索引idx_name执行上述查询时,扫描区间就是['l刘备', 'l刘备'],形成这个扫描区间的边界条件就是name = 'l刘备'。我们就把在使用二级索引idx_name执行上述查询时的匹配模式称作精确匹配。

而对于下边这个查询来说

1
sql复制代码SELECT * FROM hero WHERE name <=  'l刘备' AND country = '魏';

显然就不是精确匹配了。

唯一性搜索

如果在扫描某个扫描区间的记录前,就能事先确定该扫描区间最多只包含1条记录的话,那么就把这种情况称作唯一性搜索。我们看一下代码中判定扫描某个扫描区间的记录是否是唯一性搜索的代码是怎么写的:

image_1fjv7fa4h1kf010u1175bdtp1cko9s.png-46.3kB

其中:

  1. 匹配模式是精确匹配
  2. 使用的索引是聚簇索引或唯一二级索引
  3. 如果索引中包含多个列,则每个列在生成扫描区间时都应该被用到
  4. 如果使用的索引是唯一二级索引,那么在搜索时不能搜索某个索引列为NULL的记录(因为对于唯一二级索引来说,是可以存储多个值为NULL的记录的)。

上边几点都比较好理解,我们稍微解释一下第3点。比方说我们为某个表的a、b两列建立了一个唯一二级索引uk_a_b(a, b),那么对于搜索条件a=1形成的扫描区间来说,不能保证该扫描区间最多只包含一条记录;对于搜索条件a=1 AND b= 1形成的扫描区间来说,才可以保证该扫描区间中仅包含1条记录(不包括记录的delete_flag=1的记录)。

row_search_mvcc

我们知道MySQL其实是分成server层和存储引擎层两部分,每当执行一个查询时,server层负责生成执行计划,即选取即将使用的索引以及对应的扫描区间。我们这里以InnoDB为例,针对每一个扫描区间,都会:

  • server层向InnoDB要扫描区间的第1条记录
  • InnoDB通过B+树定位到扫描区间的第1条记录(如果定位的是二级索引记录并有回表需求则回表获取完整的聚簇索引记录),然后返回给server层
  • server层判断记录是否符合搜索条件,如果符合则发送给客户端,不符合则跳过。继续向InnoDB要下一条记录。

小贴士:

此处将记录发送给客户端其实是发送到本地的网络缓冲区,缓冲区大小由net_buffer_length控制,默认是16KB大小。等缓冲区满了才真正发送网络包到客户端。

  • InnoDB根据记录的单向链表以及页面之间的双向链表找到下一条记录(如果定位的是二级索引记录并有回表需求则回表获取完整的聚簇索引记录),返回给server层。
  • server层处理该记录,并向InnoDB要下一条记录
  • … 不停执行上述过程,直到InnoDB读到一条不符合边界条件的记录为止

可见一般情况下,server层和存储引擎层是以记录为单位进行通信的,而InnoDB读取一条记录最重要的函数就是row_search_mvcc:

image_1fjv8c0d68rn1nclh1l2ds1acba9.png-187.5kB

可以看到这个函数长到吓人,有一千多行。

小贴士:

不知道你们公司有没有在一个函数中把业务逻辑写到一千多行的同事,如果有的话你想不想打他。

在row_search_mvcc里,对一条记录进行诸如多版本的可见性判断,要不要对记录进行加锁的判断,要是加锁的话加什么锁的选择,完成记录从InnoDB的存储格式到server层存储格式的转换等等等等十分繁杂的工作。

其实对于UPDATE、DELETE语句来说,执行它们前都需要先在B+树中定位到相应的记录,所以它们也会调用row_search_mvcc。

InnoDB对记录的加锁操作主要是在row_search_mvcc中的,像SELECT ... LOCK IN SHARE MODE、SELECT ... FOR UPDATE、UPDATE、DELETE这样的语句都会调用row_search_mvcc完成加锁操作。SELECT ... LOCK IN SHARE MODE会为记录添加S型锁,SELECT ... FOR UPDATE、UPDATE、DELETE会为记录添加X型锁。

InnoDB每当读取一条记录时,都会调用一次row_search_mvcc,在做了足够长的铺垫之后,我们终于可以看一下在row_search_mvcc函数中是怎么对某条记录进行加锁的。

语句到底是怎么加锁的

首先看一个十分重要的变量:

image_1fjv8p7ff1hvldnb4ft1tuh1r9dc9.png-24.9kB

set_also_gap_locks表示是否要给记录添加gap锁(next-key锁可以看成是正经记录锁和gap锁的组合),它的默认值是TRUE,表示默认会给记录添加gap锁。

set_also_gap_locks可能会在下边这个地方发生变化:

image_1fjumo9r61mnkhj719jvlvf15nd16.png-29.9kB

即如果当前执行的是SELECT … LOCK IN SHARE MODE或者SELECT … FOR UPDATE这样的加锁读语句(非DELETE或UPDATE语句),并且隔离级别不大于READ COMMITTED 时,将set_also_gap_locks设置为FALSE。

其中prebuilt->select_lock_type表示加锁的类型,LOCK_NONE表示不加锁,LOCK_S表示加S锁(比方说执行SELECT … LOCK IN SHARE MODE时),LOCK_X表示加X锁(比方说执行SELECT … FOR UPDATE、DELETE、UPDATE时)。

对普通的SELECT的处理和意向锁的添加

再往后看:

image_1fjv9o27j2kar9t188rl1sqod3.png-110.8kB

其中:

  • 标号1的箭头是对普通的SELECT的处理,在查询开启前需要生成ReadView。

小贴士:

具体的讲就是对于Repeatable Read隔离级别来说,只在首次执行SELECT语句时生成Readview,之后的SELECT语句都复用这个ReadView;对于Read Committed隔离级别来说,每次执行SELECT语句时都会生成一个ReadView。这一点并不是在上边截图中的代码里实现的。

  • 标号2的箭头是对加锁读的语句的处理,在首次读取记录(prebuilt->sql_stat_start表示是否是首次读取)前,需要添加表级别的意向锁(IS或IX锁)。

对于ORDER BY … DESC的处理

下边开始通过B+树定位某个扫描区间中的第一条记录了:

image_1fjvbj3go1tr71r2eq02vevb3eeq.png-21.7kB

其中btr_pcur_open_with_no_init是用于定位扫描区间中的第一条记录的函数。

在B+树的每层节点中,记录是按照键值从小到大的方式进行排序的。对于某个扫描区间来说,InnoDB通常是定位到扫描区间中键值最小的那条记录,然后沿着从左往右的方式向后扫描。

但是对于下边这个查询来说:

1
sql复制代码SELECT * FROM hero WHERE name < 's孙权' AND country = '魏' ORDER BY name DESC FOR UPDATE ;

如果优化器决定使用二级索引idx_name执行上述查询的话,那么对应的扫描区间就是(-∞, 's孙权')。由于上述查询要求记录是按照从大到小的顺序返回给用户,所以InnoDB需要先定位到该扫描区间中最右边的一条记录,我们看一下idx_name二级索引示意图:

image_1fjvbfnm9hl41fjf11se13el16pued.png-18.9kB

很显然,name值为'l刘备'的二级索引记录是扫描区间(-∞, 's孙权')中最右边的记录。

下边的代码用于处理从右向左扫描扫描区间中的记录的情况:

image_1fjvbr7eouovvmc1avk1181q3ufn.png-81.8kB

其中sel_set_rec_lock就是对一条记录进行加锁的函数。

可以看到,对于加锁读来说,在隔离级别不小于REPEATABLE READ并且也没有开启innodb_locks_unsafe_for_binlog系统变量的情况下,会对扫描区间中最右边的那条记录的下一条记录加一个类型为LOCK_ORDINARY的锁,这个类型为LOCK_ORDINARY的锁其实就是next-key锁。

在本例中,假设事务的隔离级别是REPATABLE READ。扫描区间(-∞, 's孙权')中最右边的那条记录就是name值为'l刘备'的二级索引记录,接下来就应该为该记录的下一条记录,也就是name值为's孙权'的二级索引记录加一个next-key锁。

小贴士:

大家可以读一下上述代码的注释,其实这样加锁主要是为了阻止幻读。

真正的加锁流程才开始——对Infimum和Supremum记录的处理

image_1fjvqk57u1e7l532nd10021lia9.png-136.8kB

从上边的代码中可以看出,如果当前读取的记录是Infimum记录,则啥也不做,直接去读下一条记录。

如果当前读取的记录是Supremum记录,则在下边这些条件成立的时候就会为记录添加一个类型为LOCK_ORDINARY的锁,其实也就是next-key锁:

  • set_also_gap_locks是TRUE(这个变量只在前边设置过,当隔离级别不大于READ COMMITTED的SELECT语句的加锁读会设置为FALSE,否则为TRUE)
  • 未开启innodb_locks_unsafe_for_binlog系统变量并且事务的隔离级别不小于REPEATABLE READ。
  • 本次读取属于加锁读
  • 所使用的不是空间索引。

其实由于Supremum记录本身是一条伪记录,别的事务并不会更新或删除它,所以给它添加next-key锁起到的效果和给它添加gap锁是一样的。

小贴士:

Infimum记录和Supremum记录是InnoDB自动为B+树中的每个页面都添加的两条虚拟记录,也可以被称作伪记录。Infimum记录和Supremum记录分别占用13字节的存储空间,被放置在页面中固定的位置。其中Infimum记录被看作最小的记录,Supremum记录被看作最大的记录,Infimum记录属于页面中的记录单向链表的头节点,Supremum记录属于页面中的记录单向链表的尾节点。更多关于页面结构的内容小伙伴们可以参考《MySQL是怎样运行的:从根儿上理解MySQL》书籍哈~

真正的加锁流程才开始——对精确匹配的特殊处理

很抱歉,上边唠叨的都是真正对本次row_search_mvcc读取的记录进行加锁的前奏,下边展开真正的加锁过程。

首先看一下对精确匹配的一个特殊处理。

image_1fjvcr1e114g21l5u1j1s1altgr6gh.png-120.8kB

可以看到,对于匹配模式是精确匹配的扫描区间来说,如果执行本次row_search_mvcc获取到的记录不在扫描区间中(0 != cmp_dtuple_rec(search_tuple, rec, offsets)),则需要进行一些特殊处理,即:

对于加锁读来说,如果事务的隔离级别不小于Repeatable Read并且未开启innodb_locks_unsafe_for_binlog系统变量,那么就对该记录加一个gap锁,并且直接返回(代码中直接跳转到normal_return处),就不进行后续的加锁操作了。

我们举一个例子,比方说当前事务的隔离级别为Repeatable Read,执行如下语句:

1
sql复制代码SELECT * FROM hero WHERE name = 's孙权' FOR UPDATE;

如果使用二级索引idx_name执行上述查询,那么对应的扫描区间就是['s孙权', 's孙权']。该语句会首先对name值是's孙权'的记录进行加锁,不过该记录是在扫描区间中的,上述代码并不处理这种正常情况,关于正常情况的加锁我们稍后分析。

当读取完's孙权'的记录后,InnoDB会根据记录的next_record属性找到下一条二级索引记录,即name值为'x荀彧'的二级索引记录,该记录不在扫描区间['s孙权', 's孙权']中,即符合 0 != cmp_dtuple_rec(search_tuple, rec, offsets)条件,那么就执行上述代码的加锁流程 —— 对name值为'x荀彧'的二级索引记录加一个gap锁,然后结束本次扫描区间的查询。

真正的加锁流程才开始——这回真的开始了

image_1fjvdssvq18j17vp1l33104c1fklh1.png-146.4kB

我们在代码中画了2个红框,这两个红框是对记录是不对记录加gap锁的场景。我们具体看一下。

对于1号红框来说:

  • set_also_gap_locks是FALSE(这个变量只在前边设置过,当隔离级别不大于READ COMMITTED的SELECT语句的加锁读会设置为FALSE,否则为TRUE)
  • 开启innodb_locks_unsafe_for_binlog系统变量
  • 事务的隔离级别不大于READ COMMITTED
  • 唯一性搜索并且该记录的delete_flag不为1
  • 该索引是空间索引

也就是说只要上边任意一个条件成立,该记录就不应该被加gap锁,而应该添加正经记录锁。其余情况就应该加next-key锁(gap锁和正经记录锁的合体)了。

紧接着2号红框就又叙述了一个不加gap锁的场景:

对于>= 主键的这种边界条件来说,如果当前记录恰好是开始边界,就仅需对该记录加正经记录锁,而不需添加gap锁。

1号红框的内容比较好理解,我们举个例子看一下2号红框是在说什么。比方说下边这个查询:

1
sql复制代码SELCT * FROM hero WHERE number >= 8 FOR UPDATE;

我们假设这个语句在隔离级别为REPEATABLE READ。

很显然,优化器会扫描[8, +∞)的聚簇索引记录。首先要通过B+树定位到扫描区间[8, +∞)的第一条记录,也就是number值为8的聚簇索引记录,这条记录就是扫描区间[8, +∞)的开始边界记录。按理说在REPEATABLE READ隔离级别下应该添加next-key锁,但由于2号红框中代码的存在,仅会给number值为8的聚簇索引记录添加正经记录锁。

小贴士:

2号方框的优化主要是基于“主键值是唯一的”这条约束,在一个事务执行了上述查询之后,其他事务是不能插入number值为8的记录的,这也用不着gap锁了。

除了1号方框和2号方框的场景,其余场景都给记录加next-key锁就好喽~

回表对记录加锁

如果row_search_mvcc读取的是二级索引记录,则还需进行回表,找到相应的聚簇索引记录后需对该聚簇索引记录加一个正经记录锁:

image_1fjvfh2sr1971ur81fk7ksqf4mhe.png-75.1kB

其中,row_sel_get_clust_rec_for_mysql便是用于回表的函数,对聚簇索引进行加锁的逻辑在该函数中实现,我们这里就不展开了。

需要注意的是,即使是对于覆盖索引的场景下,如果我们想对记录加X型锁(也就是使用SELECT … FOR UPDATE、DELETE、UPDATE语句时)时,也需要对二级索引记录执行回表操作,并给相应的聚簇索引记录添加正经记录锁。

image_1fjvfsdoo1nkdc4k156hqsoelfhr.png-80.4kB

还有一些释放锁的场景

忽然发现已经写了好多好多了,释放锁的场景就先不唠叨了。

总结一下

其实大家再回头看row_search_mvcc里的关于加锁的代码就会发现,其实流程还是很简单的:

  • 普通的SELECT语句是不加锁的
  • 在对记录加锁之前先需要对表添加意向锁
  • 如果扫描区间是从右到左扫描,那么需要给扫描区间最右边的记录的下一条记录添加一个gap锁(在隔离级别不小于REPEATABLE READ并且也没有开启innodb_locks_unsafe_for_binlog系统变量的情况下)。
  • 对于Infimum记录是不加锁的,对于Supremum记录加next-key锁(在隔离级别不小于REPEATABLE READ并且也没有开启innodb_locks_unsafe_for_binlog系统变量的情况下)。
  • 对于精确匹配的扫描区间来说,当扫描区间中的记录都被读完后,需对扫描区间后的第一条记录加一个gap锁即可结束本扫描区间的查询(在隔离级别不小于REPEATABLE READ并且也没有开启innodb_locks_unsafe_for_binlog系统变量的情况下)。
  • 事务的隔离级别不大于READ COMMITTED,开启innodb_locks_unsafe_for_binlog系统变量,唯一性搜索并且该记录的delete_flag不为1,对于>= 主键的这种边界条件来说,当前记录恰好是开始边界记录,则对记录加正经记录锁,否则添加next-key锁。
  • 如果对二级索引记录进行加锁,还需要对相应的聚簇索引记录加正经记录锁。

给小孩子的书打个广告

  • 《MySQL是怎样运行的:从根儿上理解MySQL》小册:juejin.cn/book/684473…
  • 《MySQL是怎样使用的:从零蛋开始学习MySQL》:juejin.cn/book/684473…
  • 《计算机是怎样运行的:从根儿上理解计算机》:juejin.cn/book/684473…
  • 《MySQL是怎样运行的:从根儿上理解MySQL》纸质书:

京东链接:u.jd.com/1Mldwv9

当当链接:u.dangdang.com/qRPRW

十分感谢大家伙支持,才不至于让小孩子饿死街头。

我们都是小青蛙,呱呱呱呱呱~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

『Naocs 2x』(七) Nacos 配置中心是如何与

发表于 2021-11-09

前言

之前写了部分 Nacos 注册中心的源码分析文章,现在继续接着看 Nacos 配置中心的源码了。
本次目标有两个:

  • 了解 Spring Boot 项目启动时,如何从 Nacos 配置中心获取初始化配置。
  • Nacos 配置中心配置变更时,Spring Boot 项目如何更新。

当然实践至上,当弄明白上述两个问题后,也需要写一个丐版注册中心来加强认知。

丐版注册中心需要实现两个功能:

  • Spring Boot 项目启动时,可从丐版注册中心获取初始配置。
  • 丐版注册中心配置变更时,Spring Boot 项目能更新配置。

Spring Boot 初始化器

我们在《 『深入学习 Spring Boot』(二) 系统初始化器 》中曾学习到:

官方描述:系统初始化器是Spring容器刷新之前执行的一个回调函数。

作用是向Spring Boot容器中注册属性,需要实现ApplicationContextInitializer接口。

Spring Cloud 配置中心就是依靠ApplicationContextInitializer去调用远程接口,获取配置信息。

org.springframework.cloud.bootstrap.config包下,有此类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
ini复制代码public class PropertySourceBootstrapConfiguration implements
       ApplicationContextInitializer<ConfigurableApplicationContext>, Ordered {

 @Autowired(required = false)
   private List<PropertySourceLocator> propertySourceLocators = new ArrayList<>();

  // ......
 @Override
   public void initialize(ConfigurableApplicationContext applicationContext) {
       List<PropertySource<?>> composite = new ArrayList<>();
       AnnotationAwareOrderComparator.sort(this.propertySourceLocators);
       boolean empty = true;
   // 从应用上下文中,取出环境类
       ConfigurableEnvironment environment = applicationContext.getEnvironment();
       for (PropertySourceLocator locator : this.propertySourceLocators) {
     // 执行 locateCollection 方法,获取具体配置。
     // 注意本方法执行后,配置已经被从远程加载进来了。
           Collection<PropertySource<?>> source = locator.locateCollection(environment);
           // ....
           List<PropertySource<?>> sourceList = new ArrayList<>();
           for (PropertySource<?> p : source) {
               if (p instanceof EnumerablePropertySource) {
                   EnumerablePropertySource<?> enumerable = (EnumerablePropertySource<?>) p;
                   sourceList.add(new BootstrapPropertySource<>(enumerable));
              }
               // ....
          }
           composite.addAll(sourceList);
           empty = false;
      }
       if (!empty) {
           MutablePropertySources propertySources = environment.getPropertySources();
           // ....
     // 把 composite(远程的属性) 设置到 环境类中
           insertPropertySources(propertySources, composite);
           // ....
      }
  }

 // .....
}

这里还涉及到了,Spring 上下文中的Environment类。

如果觉得陌生,可先阅读 《『深入学习 Spring Boot』(十) Environment 》、《『深入学习 Spring Boot』(十一) profiles 》我的两篇博客。

此类就是 Spring Boot 与 Nacos 之间的 God Class了。

PropertySourceLocator 属性加载器

这是一个接口定义,其实现类将真正读取属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
scss复制代码public interface PropertySourceLocator {

   PropertySource<?> locate(Environment environment);

   default Collection<PropertySource<?>> locateCollection(Environment environment) {
       return locateCollection(this, environment);
  }
 // 注意,上文 PropertySourceBootstrapConfiguration # initialize,调用了此方法。
 // 也就是调用了 locate 方法。
   static Collection<PropertySource<?>> locateCollection(PropertySourceLocator locator,
           Environment environment) {
       PropertySource<?> propertySource = locator.locate(environment);
   // ....
  }

}

查看此接口的关系:

image-20211107225427546

好巧不巧,有个 Nacos 相关的实现。这下子可以串起来了。

Spring Boot 项目启动时,将执行 PropertySourceBootstrapConfiguration # initialize(),在其中获取到 PropertySourceLocator的实现类遍历执行locate()方法。

那么我们可以有一个猜想了,NacosPropertySourceLocator在locate()中,请求配置中心获取到配置,设置到Environment中,Spring Boot 不就可以正常启动了。

NacosPropertySourceLocator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
arduino复制代码public class NacosPropertySourceLocator implements PropertySourceLocator {

 @Override
   public PropertySource<?> locate(Environment env) {
       // ....

       CompositePropertySource composite = new CompositePropertySource(
               NACOS_PROPERTY_SOURCE_NAME);

   // 1.加载共享配置
       loadSharedConfiguration(composite);
   // 2.加载扩展配置
       loadExtConfiguration(composite);
   // 3.加载应用配置
       loadApplicationConfiguration(composite, dataIdPrefix, nacosConfigProperties, env);

       return composite;
  }

 // 1、2、3底层都是通过调用 loadNacosDataIfPresent
   private void loadNacosDataIfPresent(final CompositePropertySource composite,
       final String dataId, final String group, String fileExtension,
       boolean isRefreshable) {
   // ....
   NacosPropertySource propertySource = this.loadNacosPropertySource(dataId, group,
           fileExtension, isRefreshable);
   this.addFirstPropertySource(composite, propertySource, false);
}

 // 继续调用 loadNacosPropertySource()
 private NacosPropertySource loadNacosPropertySource(final String dataId,
           final String group, String fileExtension, boolean isRefreshable) {
   // 不自动刷新,直接从本地缓存拿
       if (NacosContextRefresher.getRefreshCount() != 0) {
           if (!isRefreshable) {
               return NacosPropertySourceRepository.getNacosPropertySource(dataId,
                       group);
          }
      }
   // 这里是发请求的方法
       return nacosPropertySourceBuilder.build(dataId, group, fileExtension,
               isRefreshable);
  }
}

NacosPropertySourceBuilder

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
typescript复制代码public class NacosPropertySourceBuilder {

   NacosPropertySource build(String dataId, String group, String fileExtension,
           boolean isRefreshable) {
       Map<String, Object> p = loadNacosData(dataId, group, fileExtension);
       NacosPropertySource nacosPropertySource = new NacosPropertySource(group, dataId,
               p, new Date(), isRefreshable);
       NacosPropertySourceRepository.collectNacosPropertySource(nacosPropertySource);
       return nacosPropertySource;
  }

   private Map<String, Object> loadNacosData(String dataId, String group,
           String fileExtension) {
       String data = null;
       try {
     // 获取配置。重点。
           data = configService.getConfig(dataId, group, timeout);
           // 格式化配置
           Map<String, Object> dataMap = NacosDataParserHandler.getInstance()
                  .parseNacosData(data, fileExtension);
           return dataMap == null ? EMPTY_MAP : dataMap;
      }
       return EMPTY_MAP;
  }
}

NacosConfigService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
vbnet复制代码public class NacosConfigService implements ConfigService {

   @Override
   public String getConfig(String dataId, String group, long timeoutMs) throws NacosException {
       return getConfigInner(namespace, dataId, group, timeoutMs);
  }

 private String getConfigInner(String tenant, String dataId, String group, long timeoutMs) throws NacosException {
       group = blank2defaultGroup(group);
       ParamUtils.checkKeyParam(dataId, group);
       ConfigResponse cr = new ConfigResponse();
       cr.setDataId(dataId);
       cr.setTenant(tenant);
       cr.setGroup(group);

           // ...

       try {
           // 从远程获取配置信息。content 字符串的内容就是具体的配置内容。
           ConfigResponse response = worker.getServerConfig(dataId, group, tenant, timeoutMs, false);
           cr.setContent(response.getContent());
           cr.setEncryptedDataKey(response.getEncryptedDataKey());
           configFilterChainManager.doFilter(null, cr);
           content = cr.getContent();
           return content;
      } catch (NacosException ioe) {
           // ...
      }
       // ...
  }
}

image-20211108224422948

从远程获取到配置项后,就可以保存到上下文环境中,进行应用程序的启动了。

小结

Nacos 配置中心是如何与 Spring Cloud 结合的?

  1. Spring Boot 有初始化器机制,专门用于初始化属性的。Spring Cloud 实现了一个初始化器,并在其中调用PropertySourceLocator#locate()。
  2. Nacos 有一PropertySourceLocator的实现类,并在locate()方法中进行远程调用,获取具体配置项。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

设计模式-Strategy

发表于 2021-11-09

行为型模式

策略模式(Strategy Pattern)对某种行为提供了一组实现策略,并把这一组策略分别封装在不同的实现类中。在执行行为时,根据上下文场景选择不同的实现类(实现策略),从而使得策略的变化独立于使用它的客户而变化。

组成

策略模式有3个参与者:

  • 某种行为:可以看做是一个接口对外提供的一系列方法;
  • 行为的一组实现策略:接口的一组实现类;
  • 行为的一个持有者:持有接口引用的对象,并且包含上下文信息,能够选择不同的实现类;

三者之间的关系如图1所示:

图1. 策略模式的组成结构
图1中各个各个组成介绍:

  • Strategy:一个接口,定义了对外提供的方法;
  • ConcreteStrategyA、ConcreteStrategyB、ConcreteStrategyC:Strategy接口的具体实现类;
  • Context:持有 Strategy 引用,并根据上下文条件选择不同的实现类来执行方法;

Context 根据客户请求的条件,选择出一个具体的 Strategy 实现类;当具体的 Strategy 被调用时,Context 把参数传入方法中。
对于客户而言,它仅需要与 Context 交互,并不知道具体执行任务的 Strategy 是哪一个,隔离了客户与 Strategy。这样,在后续迭代过程中修改代码时,仅需要修改 Strategy 的某个实现类,对“客户、Context” 都是无影响的。

应用场景

当存在以下情况时,推荐使用 策略模式:

  • 存在许多相关的类,它们对外提供同一个方法,方法的执行逻辑功能相似,区别在于方法中的执行对象不同,那么可以使用一个 Context 从这些类中进行动态选择;
  • 一个类定义了许多行为,这些行为以用多个条件语句的形式出现,那么可以把相关的条件分支移入各自的 Strategy 实现类中,用 Context 的选择代替条件语句的选择;
  • 需要使用一个算法的不同变体,提供对接口/父类方法的多态实现。

下面用一个例子来演示策略模式的常见应用场景:

业务的 Service 层需要根据 Controller 传入的条件,来执行相似的业务逻辑,演示代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public class Service {

public void doMethod(int condition) {
if (conditionA) {
// 逻辑A
} else if (conditionB) {
// 逻辑B
} else if (conditionC) {
// 逻辑C
} else {
// 逻辑D
}
}
}

我们可以在 Service 层中维护一个 StrategyFactory(相当于 Context),StrategyFactory 根据条件选择不同的 Strategy 实现,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
java复制代码public interface Strategy {
void method();
}

public class StrategyA implements Strategy {
@Override
public void method() {
// 逻辑A
}
}

public class StrategyB implements Strategy {
@Override
public void method() {
// 逻辑B
}
}

public class StrategyC implements Strategy {
@Override
public void method() {
// 逻辑C
}
}

public class StrategyD implements Strategy {
@Override
public void method() {
// 逻辑D
}
}

public class StrategyFactory {

private Strategy strategy;

public Strategy getStrategyByCondition(int condition) {
// 根据条件返回不同的 strategy
}
}


public class Service {

StrategyFactory strategyFactory;

public void doMethod(int condition) {
Strategy strategy = strategyFactory.getStrategyByCondition(condition);
strategy.method();
}
}

初始版本的 Service 类使用 if-else 做条件判断,虽然写起来很方便,但是如果条件增多,代码会变得越来越臃肿,而且违反了两个设计原则:

  • 单一职责原则:一个类应该只有一个发生变化的原因,每次增删条件、修改条件的执行逻辑,Service 类都会被修改;
  • 开闭原则:对扩展开放,对修改关闭,如果要增删新的逻辑,每次都会修改 Service 类;

使用策略模式之后,Service 类的代码更加简洁:

  • 逻辑的实现细节被隐藏到具体的 Strategy 实现类中;
  • 只有当 Service 本身的逻辑变化时,才会更改 Service,其他情况下不会修改 Service 类;
  • 如果修改实现策略,可以修改 Strategy 的实现类,不需要修改 Service 类;

优点和缺点

从上面的演示示例可以看出策略模式的优点和缺点。

优点:

1、策略方法的抽象:Strategy 接口对外提供方法的抽象,具体的逻辑由它的实现子类提供;

2、使用组合替代继承:我们可以使用 Context 的多个子类,来实现多个逻辑的实现,但这样会把行为方法硬编码到 Context 中,使得 Context 难以理解、难以维护和难以扩展,使用策略模式把 Strategy 组合到 Context 中,可以使方法的实现独立于 Context ,使得它易于切换、理解、扩展;

3、消除了条件语句:使用具体的实现类封装条件逻辑,使用动态选择来代替 if-else/switch-case 的条件选择。

缺点:

1、客户和开发需要了解所有的策略,清楚它们的不同:Context 根据场景上下文来决定使用哪个 Strategy,场景上下文由客户传入;

2、增加了类的数量:每个实现类都封装为了一个策略类,类文件的个数会随着策略的增加而增加,并且每个 ConcreteStrategy 都要实现 Strategy 的所有方法,有些 ConcreteStrategy 并不会用到 Strategy 的所有方法;

3、Strategy 和 Context 之间的通信开销:Context 不能直接调用 Strategy 的具体实现对象,需要一个 StrategyFactory 类来维护 Strategy;

4、只适合扁平的代码结构:策略模式中各个策略的实现是平等的关系,而不是层级关系。

示例代码

策略模式关键在于两步:

  • 如何把 Strategy 的具体实现“注入”到 Context 中;
  • 如何根据条件选择出具体的 Strategy 实现类;

下面介绍两种条件下的示例。

示例1:非Spring框架

JDK 中 ThreadPoolExecutor 和它的拒绝策略 RejectedExecutionHandler 之间的关系,就类似于策略模式中 Context 与Strategy 之间的关系:ThreadPoolExecutor 是 Context,RejectedExecutionHandler 是 Strategy,两者之间的关系如图2所示:

图2. ThreadPoolExecutor 和 RejectedExecutionHandler
在非Spring框架中,要创建 ThreadPoolExecutor 对象的时候传入具体的实现策略:

1
2
3
4
5
6
7
java复制代码ThreadPoolExecutor pool = new ThreadPoolExecutor(
/*...省略 ...*/
new ThreadPoolExecutor.AbortPolicy());

ThreadPoolExecutor pool = new ThreadPoolExecutor(
/*...省略 ...*/
new ThreadPoolExecutor.DiscardOldestPolicy());

在非Spring框架下,我们需要在创建 Context 时,手动注入具体的策略对象。

示例2:Spring框架

Spring 会管理所有的 Bean,利用这一点,可以把所有 Strategy 的实现都注册到 Spring 容器中,然后 Context 从容器中取出 Bean,实现自动注入。如图3所示:

图3. Spring框架下的策略模式
Context 在注入具体策略时可以从 Spring 容器中取出,Context 和 Strategy 之间实现了一步的解耦。

下面的示例代码来自于参考阅读[2],演示了 StrategyFactory 在 Spring 框架下的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
java复制代码@Component
public class FormSubmitHandlerFactory implements InitializingBean, ApplicationContextAware {

private static final
Map<String, FormSubmitHandler<Serializable>> FORM_SUBMIT_HANDLER_MAP = new HashMap<>(8);

private ApplicationContext appContext;

/**
* 根据提交类型获取对应的处理器
*
* @param submitType 提交类型
* @return 提交类型对应的处理器
*/
public FormSubmitHandler<Serializable> getHandler(String submitType) {
return FORM_SUBMIT_HANDLER_MAP.get(submitType);
}

@Override
public void afterPropertiesSet() {
// 将 Spring 容器中所有的 FormSubmitHandler 注册到 FORM_SUBMIT_HANDLER_MAP
appContext.getBeansOfType(FormSubmitHandler.class)
.values()
.forEach(handler -> FORM_SUBMIT_HANDLER_MAP.put(handler.getSubmitType(), handler));
}

@Override
public void setApplicationContext(@NonNull ApplicationContext applicationContext) {
appContext = applicationContext;
}
}

在该示例代码中,StrategyFactory 利用了 Spring 框架自身的接口(InitializingBean 和 ApplicationContextAware),在 afterPropertiesSet 方法中取出容器中具体的 Strategy Bean 保存进 Map 中;当 Context 调用 Strategy 时,方法 getHandler() 从 Map 中取出 Bean。

这样做,在 Spring 容器启动时就实现了 Strategy Bean 的自动注入和维护,省去了“非Spring框架”下每次创建具体策略的步骤,实现了

项目实践-多策略的自动注入

我们在实际项目开发中,就仿照“参考阅读2”的代码实现策略模式,比如我们要判断一个资源是否存在,资源的种类很多,我们可以创建一个“资源策略接口-Strategy ”,每种资源有对应“资源策略接口”的一个具体实现类,同时创建一个“资源策略工厂-StrategyFactory”,像“参考阅读2”一样,实现 Spring 的 InitializingBean 和 ApplicationContextAware 接口。

但是,随后发现了一个问题:在项目的其他业务场景下使用策略模式时,依然要创建一个“xxx策略工厂-xxxStrategyFactory”,依然要实现 Spring 的个接口,导致了重复代码的存在。

为了减少这种重复代码,我们在项目中,通过使用 ”泛型接口、注解、Spring的配置类“等方法,实现了多策略工厂的自动注入。

配置类1

从 “参考阅读2” 可以看出,StrategyFactory 的主要方法有:

  • setApplicationContext:设置 ApplicationContext 的引用;
  • afterPropertiesSet:把 Strategy Bean 保存进 map 中;
  • getHandler:根据 context 传入的 type 从 map 中取出 Strategy Bean;

其实,StrategyFactory 真正的功能只有两个:

  • 把 Strategy Bean 保存进 map 中;
  • 根据 context 传入的 type 从 map 中取出 Strategy Bean;

至于 ApplicationContext 的引用,可以在一个配置类中设置。

因此,我们创建一个配置类 StrategyConfig,实现 InitializingBean, ApplicationContextAware 接口,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码@Configuration
public class StrategyConfig implements ApplicationContextAware, InitializingBean {

private ApplicationContext applicationContext;

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}

@Override
public void afterPropertiesSet() throws Exception {
// 配置 StrategyFactory 和 Strategy Bean
}
}

在该配置类中,setApplicationContext 方法的作用就是设置 ApplicationContext 的引用;afterPropertiesSet 方法是用来配置 StrategyFactory 和 Strategy Bean,在后面 “配置类2” 部分有介绍。

泛型接口

一个 StrategyFactory 管理一个 Strategy 接口及其实现类,如果我们在多个业务场景下使用策略模式,就需要创建多个 StrategyFactory 类和多个 Strategy 接口。

另一方面,每个 Strategy 实现类在 StrategyFactory 的 map 中都有一个唯一的 key 值,比如 “参考阅读2” 中,每个 Strategy 实现类会返回一个 type 值,这个 type 值可以是字符串、数字等等。

我们把上述两个行为抽象出来,创建泛型 Strategy 接口和泛型 StrategyFactory 接口,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
java复制代码/**
* 泛型 Strategy 接口
* @author shimengjie
* @date 2021/11/5 14:39
**/
public interface Strategy<K> {

/**
* 返回类型
*
* @return K
*/
K getType();
}

/**
* 泛型 StrategyFactory 接口
*
* @author shimengjie
* @date 2021/11/5 14:41
**/
public interface StrategyFactory<K> {

/**
* 添加 Strategy 实例
*
* @param k Strategy 的类型
* @param v Strategy 实例
*/
void addStrategy(K k, Strategy<K> v);

/**
* 根据 key 值取出对应的 Strategy
*
* @param key Strategy 的类型
* @return Strategy 实例
*/
Strategy<K> getByType(K key);
}

项目中具体业务的 Strategy 接口、StrategyFactory 类都分别继承自这两个泛型接口:

  • 泛型 就是每个 Strategy 返回的 type 类型;
  • 因为其他的的 Strategy 接口都继承自 Strategy,所以 StrategyFactory 中addStrategy 方法参数、getByType 方法返回值,都是 Strategy。

项目中具体业务的 Strategy 接口、StrategyFactory 类与这两个泛型接口的关系如图4(a)、图4(b) 所示:

图4(a). Strategy 接口与泛型 Strategy 接口的关系

图4(b). StrategyFactory 类与泛型 StrategyFactory 接口的关系
这样,我们就有了 Strategy 接口和 StrategyFactory 接口的统一抽象,但是,在配置类 StrategyConfig 的 afterPropertiesSet 方法中,我们该怎么知道每个 StrategyFactory 类管理的是哪个 Strategy 接口呢?

我们给 StrategyFactory 类添加注解,来标识它管理的 Strategy 接口。

注解

我们定义注解 RegistryStrategyFactory,它只有一个必填参数 strategy,值是泛型接口 Strategy 的子类,代码如下:

1
2
3
4
5
6
7
8
9
10
java复制代码@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface RegistryStrategyFactory {

/**
* 指定策略实现类
*/
Class<? extends Strategy> strategy();
}

比如,我们创建 WorkStrategyFactory 类来管理 WorkStrategy 接口,只需要在 StrategyFactory 类上添加该注解即可,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
java复制代码public interface WorkStrategy extends Strategy<String> {

/**
* 判断作品是否存在
*
* @param id 作品ID
* @return true/false
*/
boolean isExisted(Long id);
}

@Component
@RegistryStrategyFactory(strategy = WorkStrategy.class)
public class WorkStrategyFactory implements StrategyFactory<String> {

/**
* 保存 WorkStrategy 实例
*/
private Map<String, WorkStrategy> map;

public WorkStrategyFactory() {
this.map = new HashMap<>();
}

@Override
public void addStrategy(String k, Strategy<String> v) {
map.put(k, (WorkStrategy) v);
}

@Override
public WorkStrategy getByType(String key) {
return map.get(key);
}
}

配置类2

有了泛型接口、注解,我们可以完成配置类 StrategyConfig 的 afterPropertiesSet 方法。很显然,该方法主要步骤如下:

  • 找出所有的 StrategyFactory Bean;
  • 从每个 StrategyFactory Bean 的注解上找到它管理的 Strategy 接口;
  • StrategyFactory Bean 把 Strategy Bean 添加到 map 中。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码@Override
public void afterPropertiesSet() throws Exception {
// 便利注册的 StrategyFactory
for (StrategyFactory factory : applicationContext.getBeansOfType(StrategyFactory.class).values()) {
RegistryStrategyFactory annotation = AnnotationUtils.findAnnotation(factory.getClass(), RegistryStrategyFactory.class);
if (annotation != null) {
// 取出注解中指定的 策略 Bean
Class<? extends Strategy> strategyClazz = annotation.strategy();
Map<String, ? extends Strategy> map = applicationContext.getBeansOfType(strategyClazz);
// 添加进 map 中
for (Strategy value : map.values()) {
factory.addStrategy(value.getType(), value);
}
}
}
}

小结

经过上述改造,我们每次创建 StrategyFactory 类时,不再需要实现 InitializingBean, ApplicationContextAware 接口,只需要实现泛型接口 StrategyFactory 并添加注解,就可以实现 StrategyFactory 和 Strategy 的自动注入。

上述代码在项目:github.com/ShiMengjie/… 的 work 模块中有演示。

总结

策略模式利用了多态特性,通过面向接口编程,隔离了 客户、Context 与具体策略实现类,降低了业务代码之间的耦合。

另外,为了避免在多次使用策略模式时,StrategyFactory 重复实现 InitializingBean, ApplicationContextAware 接口,可以使用范型接口、注解与配置 来减少重复代码。

附录. 参考阅读

设计模式最佳套路—— 愉快地使用策略模式

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

你以为委派模式很神秘,其实你每天都在用

发表于 2021-11-09

本文节选自《设计模式就该这样学》

1 使用委派模式模拟任务分配场景

我们用代码来模拟老板给员工分配任务的业务场景。
首先创建IEmployee员工接口。

1
2
3
4
java复制代码
public interface IEmployee {
void doing(String task);
}

创建员工EmployeeA类。

1
2
3
4
5
6
7
java复制代码
public class EmployeeA implements IEmployee {
protected String goodAt = "编程";
public void doing(String task) {
System.out.println("我是员工A,我擅长" + goodAt + ",现在开始做" +task + "工作");
}
}

创建员工EmployeeB类。

1
2
3
4
5
6
7
java复制代码
public class EmployeeB implements IEmployee {
protected String goodAt = "平面设计";
public void doing(String task) {
System.out.println("我是员工B,我擅长" + goodAt + ",现在开始做" +task + "工作");
}
}

创建项目经理Leader类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码
public class Leader implements IEmployee {

private Map<String,IEmployee> employee = new HashMap<String,IEmployee>();

public Leader(){
employee.put("爬虫",new EmployeeA());
employee.put("海报图",new EmployeeB());
}

public void doing(String task) {
if(!employee.containsKey(task)){
System.out.println("这个任务" +task + "超出我的能力范围");
return;
}
employee.get(task).doing(task);
}
}

然后创建Boss类下达命令。

1
2
3
4
5
6
java复制代码
public class Boss {
public void command(String task,Leader leader){
leader.doing(task);
}
}

最后编写客户端测试代码。

1
2
3
4
5
6
7
8
java复制代码
public class Test {
public static void main(String[] args) {
new Boss().command("海报图",new Leader());
new Boss().command("爬虫",new Leader());
new Boss().command("卖手机",new Leader());
}
}

通过上面代码,我们生动地还原了老板分配任务的业务场景,这也是委派模式的生动体现。其类图如下图所示。

file

2 委派模式在JDK源码中的应用

JDK中有一个典型的委派,众所周知,JVM在加载类时用双亲委派模型,这又是什么呢?一个类加载器在加载类时,先把这个请求委派给自己的父类加载器去执行。如果父类加载器还存在父类加载器,则继续向上委派,直到顶层的启动类加载器;如果父类加载器能够完成类加载,则成功返回;如果父类加载器无法完成加载,则子加载器尝试自己去加载。从定义中可以看到,当双亲委派加载的一个类加载器加载类时,首先不是自己加载,而是委派给父类加载器。下面来看loadClass()方法的源码,此方法在ClassLoader中。在这个类里定义了一个双亲,用于下面的类加载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
java复制代码
public abstract class ClassLoader {
...
private final ClassLoader parent;
...
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}

if (c == null) {
long t1 = System.nanoTime();
c = findClass(name);

sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
...
}

同样,在Method类里,常用的代理执行方法invoke()也存在类似机制,代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码
public Object invoke(Object obj, Object... args)
throws IllegalAccessException, IllegalArgumentException,
InvocationTargetException
{
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, obj, modifiers);
}
}
MethodAccessor ma = methodAccessor; // read volatile
if (ma == null) {
ma = acquireMethodAccessor();
}
return ma.invoke(obj, args);
}

看完代码,相信小伙伴们把委派模式和代理模式的区别弄清楚了吧。

3 委派模式在Spring源码中的应用

下面来看委派模式在Spring中的应用,Spring IoC模块中的DefaultBeanDefinitionDocumentReader类,当调用doRegisterBeanDefinitions()方法时,即在BeanDefinition进行注册的过程中,会设置BeanDefinitionParserDelegate类型的Delegate对象传给this.delegate,并将这个对象作为一个参数传入parseBeanDefinitions(root, this.delegate)。主要的解析工作就是由delegate作为主要角色来完成的,代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码
protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
//判断节点是否属于同一命名空间,如果是,则执行后续的解析
if (delegate.isDefaultNamespace(root)) {
NodeList nl = root.getChildNodes();
for (int i = 0; i < nl.getLength(); i++) {
Node node = nl.item(i);
if (node instanceof Element) {
Element ele = (Element) node;
if (delegate.isDefaultNamespace(ele)) {
parseDefaultElement(ele, delegate);
}
else {
//注解定义的Context的nameSpace进入这个分支
delegate.parseCustomElement(ele);
}
}
}
}
else {
delegate.parseCustomElement(root);
}
}

上面代码中的parseDefaultElement(ele, delegate)方法,主要功能是针对不同的节点类型,完成Bean的注册操作,而在这个过程中,delegate会调用element的parseBeanDefinitionElement()方法,从而得到一个BeanDefinitionHolder类型的对象,之后通过这个对象完成注册。
再来还原一下Spring MVC的DispatcherServlet是如何实现委派模式的。创建业务类MemberController。

1
2
3
4
5
6
7
8
9
10
11
java复制代码
/**
* Created by Tom.
*/
public class MemberController {

public void getMemberById(String mid){

}

}

创建OrderController类。

1
2
3
4
5
6
7
8
9
10
11
java复制代码
/**
* Created by Tom.
*/
public class OrderController {

public void getOrderById(String mid){

}

}

创建SystemController类。

1
2
3
4
5
6
7
8
9
10
java复制代码
/**
* Created by Tom.
*/
public class SystemController {

public void logout(){

}
}

创建DispatcherServlet类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
java复制代码
public class DispatcherServlet extends HttpServlet {

private Map<String,Method> handlerMapping = new HashMap<String,Method>();

@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doDispatch(req,resp);
}

private void doDispatch(HttpServletRequest req, HttpServletResponse resp) {
String url = req.getRequestURI();
Method method = handlerMapping.get(url);
//此处省略反射调用方法的代码
...

}

@Override
public void init() throws ServletException {
try {
handlerMapping.put("/web/getMemeberById.json", MemberController.class.getMethod("getMemberById", new Class[]{String.class}));
}catch (Exception e){
e.printStackTrace();
}
}
}

配置web.xml文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:javaee="http://java.sun.com/xml/ns/javaee"
xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
version="2.4">
<display-name>Web Application</display-name>


<servlet>
<servlet-name>delegateServlet</servlet-name>
<servlet-class>com.tom.pattern.delegate.mvc.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
<servlet-name>delegateServlet</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>

</web-app>

这样,一个完整的委派模式就实现了。当然,在Spring中运用委派模式的情况还有很多,大家通过命名就可以识别。在Spring源码中,只要以Delegate结尾的都实现了委派模式。例如,BeanDefinitionParserDelegate根据不同的类型委派不同的逻辑来解析BeanDefinition。

关注『 Tom弹架构 』回复“设计模式”可获取完整源码。

【推荐】Tom弹架构:30个设计模式真实案例(附源码),挑战年薪60W不是梦

本文为“Tom弹架构”原创,转载请注明出处。技术在于分享,我分享我快乐!如果本文对您有帮助,欢迎关注和点赞;如果您有任何建议也可留言评论或私信,您的支持是我坚持创作的动力。关注『 Tom弹架构 』可获取更多技术干货!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

lettuce连接超时问题

发表于 2021-11-09

项目使用springboot搭建,涉及到redis的使用,开始经常出现『Caused by: io.lettuce.core.RedisCommandTimeoutException: Command timed out after 10 second(s)』报错(查询、插入都会报错),查询资料知道原因是springboot2.0以上版本redis客户端默认使用lettuce,修改为使用jedis,但是性能达不到要求,又改为使用lettuce,在配置时开启了心跳。

截屏2021-11-09 上午11.54.16.png

但是发现还是会出现同样的报错,只不过查询、删除都没事,只有偶尔插入会报错。。。看到有开启每次连接前校验的设置,但是担心影响性能。

1
2
3
4
5
scss复制代码stringRedisTemplate.watch(key);
stringRedisTemplate.multi();

stringRedisTemplate.opsForValue().set
stringRedisTemplate.exec();

插入的时候开启了事务,并使用了watch乐观锁,和这个有关系吗?

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

488 祖玛游戏 「搜索 + 剪枝」&「AStar 算法

发表于 2021-11-09

「这是我参与11月更文挑战的第 9 天,活动详情查看:2021最后一次更文挑战」。

题目描述

这是 LeetCode 上的 488. 祖玛游戏 ,难度为 困难。

Tag : 「DFS」、「搜索」、「启发式搜索」

你正在参与祖玛游戏的一个变种。

在这个祖玛游戏变体中,桌面上有 一排 彩球,每个球的颜色可能是:红色 'R'、黄色 'Y'、蓝色 'B'、绿色 'G' 或白色 'W' 。你的手中也有一些彩球。

你的目标是 清空 桌面上所有的球。每一回合:

  • 从你手上的彩球中选出 任意一颗 ,然后将其插入桌面上那一排球中:两球之间或这一排球的任一端。
  • 接着,如果有出现 三个或者三个以上 且 颜色相同 的球相连的话,就把它们移除掉。
    • 如果这种移除操作同样导致出现三个或者三个以上且颜色相同的球相连,则可以继续移除这些球,直到不再满足移除条件。
  • 如果桌面上所有球都被移除,则认为你赢得本场游戏。
  • 重复这个过程,直到你赢了游戏或者手中没有更多的球。

给你一个字符串 board ,表示桌面上最开始的那排球。另给你一个字符串 hand ,表示手里的彩球。请你按上述操作步骤移除掉桌上所有球,计算并返回所需的 最少 球数。如果不能移除桌上所有的球,返回 -1 。

示例 1:

1
2
3
4
5
6
7
8
rust复制代码输入:board = "WRRBBW", hand = "RB"

输出:-1

解释:无法移除桌面上的所有球。可以得到的最好局面是:
- 插入一个 'R' ,使桌面变为 WRRRBBW 。WRRRBBW -> WBBW
- 插入一个 'B' ,使桌面变为 WBBBW 。WBBBW -> WW
桌面上还剩着球,没有其他球可以插入。

示例 2:

1
2
3
4
5
6
7
8
rust复制代码输入:board = "WWRRBBWW", hand = "WRBRW"

输出:2

解释:要想清空桌面上的球,可以按下述步骤:
- 插入一个 'R' ,使桌面变为 WWRRRBBWW 。WWRRRBBWW -> WWBBWW
- 插入一个 'B' ,使桌面变为 WWBBBWW 。WWBBBWW -> WWWW -> empty
只需从手中出 2 个球就可以清空桌面。

示例 3:

1
2
3
4
5
6
7
8
php复制代码输入:board = "G", hand = "GGGGG"

输出:2

解释:要想清空桌面上的球,可以按下述步骤:
- 插入一个 'G' ,使桌面变为 GG 。
- 插入一个 'G' ,使桌面变为 GGG 。GGG -> empty
只需从手中出 2 个球就可以清空桌面。

示例 4:

1
2
3
4
5
6
7
8
9
rust复制代码输入:board = "RBYYBBRRB", hand = "YRBGB"

输出:3

解释:要想清空桌面上的球,可以按下述步骤:
- 插入一个 'Y' ,使桌面变为 RBYYYBBRRB 。RBYYYBBRRB -> RBBBRRB -> RRRB -> B
- 插入一个 'B' ,使桌面变为 BB 。
- 插入一个 'B' ,使桌面变为 BBB 。BBB -> empty
只需从手中出 3 个球就可以清空桌面。

提示:

  • 1 <= board.length <= 16
  • 1 <= hand.length <= 5
  • board 和 hand 由字符 'R'、'Y'、'B'、'G' 和 'W' 组成
  • 桌面上一开始的球中,不会有三个及三个以上颜色相同且连着的球

搜索 + 剪枝

数据范围 1<=board.length<=161 <= board.length <= 161<=board.length<=16 和 1<=hand.length<=51 <= hand.length <= 51<=hand.length<=5。

为了方便,我们使用 aaa 和 bbb 来代指 boardboardboard 和 handhandhand。

但在爆搜过程中同时维持两个字符串构造会超时,考虑使用一个 int 来记录 handhandhand 的使用情况。

image.png

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
Java复制代码class Solution {
int INF = 0x3f3f3f3f;
String b;
int m;
Map<String, Integer> map = new HashMap<>();
public int findMinStep(String a, String _b) {
b = _b;
m = b.length();
int ans = dfs(a, 1 << m);
return ans == INF ? -1 : ans;
}
int dfs(String a, int cur) {
if (a.length() == 0) return 0;
if (map.containsKey(a)) return map.get(a);
int ans = INF;
int n = a.length();
for (int i = 0; i < m; i++) {
if (((cur >> i) & 1) == 1) continue;
int next = (1 << i) | cur;
for (int j = 0; j <= n; j++) {
boolean ok = false;
if (j > 0 && j < n && a.charAt(j) == a.charAt(j - 1) && a.charAt(j - 1) != b.charAt(i)) ok = true;
if (j < n && a.charAt(j) == b.charAt(i)) ok = true;
if (!ok) continue;
StringBuilder sb = new StringBuilder();
sb.append(a.substring(0, j)).append(b.substring(i, i + 1));
if (j != n) sb.append(a.substring(j));
int k = j;
while (0 <= k && k < sb.length()) {
char c = sb.charAt(k);
int l = k, r = k;
while (l >= 0 && sb.charAt(l) == c) l--;
while (r < sb.length() && sb.charAt(r) == c) r++;
if (r - l - 1 >= 3) {
sb.delete(l + 1, r);
k = l >= 0 ? l : r;
} else {
break;
}
}
ans = Math.min(ans, dfs(sb.toString(), next) + 1);
}
}
map.put(a, ans);
return ans;
}
}
  • 时间复杂度:略。「爆搜」同时还得考虑「剪枝」的复杂度分析意义不大。
  • 空间复杂度:略

AStar 算法

我们建立一个类 Node 来代指当前搜索局面。

1
2
3
4
5
6
7
8
9
10
11
12
Java复制代码class Node {
// 当前的棋盘状况
String a;
// cur 代表当前 hand 的使用情况(若 cur 二进制表示中的第 k 位为 1,代表 hand 的第 k 个彩球已被使用)
// val 代表「当前棋盘为 a」和「hand 使用情况为 cur」的情况下,至少还需要多少步才能将 a 全部消掉(启发式估算值)
// step 代表当前局面是经过多少步而来
int cur, val, step;
Node (String _a, int _c, int _v, int _s) {
a = _a;
cur = _c; val = _v; step = _s;
}
}

显然,直接对此进行 BFS,会 TLE。

我们考虑将优化 BFS 中使用到的队列改为优先队列:更接近答案的局面先出队进行局面延展。

然后我们考虑如何设计 AStar 的启发式函数。

首先,一个合格的 AStar 启发式函数应当能够确保「估值不会小于理论最小距离」。同时由于启发式的估值函数是针对于最终状态进行估算,因此只确保最终状态的第一次出队时为最短路,其余中间状态的首次出队不一定是最短路,为此我们需要使用哈希表来记录中间状态的距离变化,如果某个局面的最短距离被更新,我们应当将其再次入队。

基于此,我们设计如下的 AStar 的启发式函数:使用哈希表来统计「当前的棋盘 aaa 的彩球数量」&「当前手上拥有的彩球数量」,对「无解情况」和「理论最小次数」进行分析:

  • 对于某个彩球 ccc 而言,如果当前棋盘的数量 + 手上的数量 都不足 333 个,那么该局面往下搜索也必然无解,该局面无须入队;
  • 对于某个彩球 ccc 而言,如果当前棋盘数量少于 333 个,那么至少需要补充至 333 个才能被消除,而缺少的个数则是「从手上彩球放入棋盘内」的次数,即对于彩球 ccc,我们理论上至少需要消耗 3−cnt3 - cnt3−cnt 次(cntcntcnt 为当前棋盘拥有的彩球 ccc 的数量)。

需要注意的是:对于某个局面 nodenodenode 而言,最终的距离是由「已确定距离」+「估值距离」两部分组成,我们应当根据这两部分之和进行出队,才能确保算法的正确性。

image.png

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
Java复制代码class Solution {
class Node {
String a;
int cur, val, step;
Node (String _a, int _c, int _v, int _s) {
a = _a;
cur = _c; val = _v; step = _s;
}
}
int f(String a, int k) {
Map<Character, Integer> m1 = new HashMap<>(), m2 = new HashMap<>();
for (int i = 0; i < a.length(); i++) {
m1.put(a.charAt(i), m1.getOrDefault(a.charAt(i), 0) + 1);
}
for (int i = 0; i < m; i++) {
if (((k >> i) & 1) == 0) m2.put(b.charAt(i), m2.getOrDefault(b.charAt(i), 0) + 1);
}
int ans = 0;
for (char c : m1.keySet()) {
int c1 = m1.get(c), c2 = m2.getOrDefault(c, 0);
if (c1 + c2 < 3) return INF;
if (c1 < 3) ans += (3 - c1);
}
return ans;
}

int INF = 0x3f3f3f3f;
String b;
int m;
Map<String, Integer> map = new HashMap<>();
public int findMinStep(String _a, String _b) {
b = _b;
m = b.length();
PriorityQueue<Node> q = new PriorityQueue<>((o1,o2)->(o1.val+o1.step)-(o2.val+o2.step));
q.add(new Node(_a, 1 << m, f(_a, 1 << m), 0));
map.put(_a, 0);
while (!q.isEmpty()) {
Node poll = q.poll();
String a = poll.a;
int cur = poll.cur;
int step = poll.step;
int n = a.length();
for (int i = 0; i < m; i++) {
if (((cur >> i) & 1) == 1) continue;
int next = (1 << i) | cur;
for (int j = 0; j <= n; j++) {
boolean ok = false;
if (j > 0 && j < n && a.charAt(j) == a.charAt(j - 1) && a.charAt(j - 1) != b.charAt(i)) ok = true;
if (j < n && a.charAt(j) == b.charAt(i)) ok = true;
if (!ok) continue;
StringBuilder sb = new StringBuilder();
sb.append(a.substring(0, j)).append(b.substring(i, i + 1));
if (j != n) sb.append(a.substring(j));
int k = j;
while (0 <= k && k < sb.length()) {
char c = sb.charAt(k);
int l = k, r = k;
while (l >= 0 && sb.charAt(l) == c) l--;
while (r < sb.length() && sb.charAt(r) == c) r++;
if (r - l - 1 >= 3) {
sb.delete(l + 1, r);
k = l >= 0 ? l : r;
} else {
break;
}
}
String nextStr = sb.toString();
if (nextStr.length() == 0) return step + 1;
if (f(nextStr, next) == INF) continue;
if (!map.containsKey(nextStr) || map.get(nextStr) > step + 1) {
map.put(nextStr, step + 1);
q.add(new Node(nextStr, next, f(nextStr, next), step + 1));
}
}
}
}
return -1;
}
}
  • 时间复杂度:略。「爆搜」同时还得考虑「启发式加速」的复杂度分析意义不大。
  • 空间复杂度:略

最后

这是我们「刷穿 LeetCode」系列文章的第 No.488 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:github.com/SharingSour… 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

设计模式 -- 装饰器模式

发表于 2021-11-09

「这是我参与11月更文挑战的第4天,活动详情查看:2021最后一次更文挑战」

含义: 在不改变原有对象的基础上添加新的功能,与继承相比能够更加灵活地扩展对象的功能。

下面是具体的实现方式,拿 coffee 和 tea 中添加 milk 举例,分别被装饰成 latte 和 milky tea

创建一个接口

1
2
3
csharp复制代码public interface Drink{
   void getName();
}

创建实现接口的实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
typescript复制代码public class Coffee implements Drink{
   @Override
   public void getName(){
       System.out.println("Drink: Coffee");
  }
}
​
public class Tea implements Drink{
   @Override
   public void getName(){
       System.out.println("Drink: Tea");
  }
}

创建装饰 Drink 的接口

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public interface DrinkDecorator extends Drink{
   
   private Drink drink;
   
   public DrinkDecorator(Drink  drink){
       this.drink = drink;
  }
   
   @Override
   public void getName(){
       drink.getName();
  }
}

创建具体的装饰

1
2
3
4
5
6
7
8
9
10
11
12
13
scala复制代码public class MilkDecorator extends DrinkDecorator{
   
   public MilkDecorator(Drink  drink) {
       super(drink);
  }
   public void getName() {
       super.getName();
       setMilk();
  }
   public void setMilk() {
       System.out.println("setMilk");
  }
}

使用 MilkDecorator 来装饰 Drink 对象

1
2
3
4
5
6
7
8
9
10
11
12
csharp复制代码public class DecoratorDemo {
  public static void main(String[] args) {
     
      System.out.println("drink latte");
      Drink latte = new MilkDecorator(new Coffee());
      latte.getName();
     
      System.out.println("drink milky tea");
      Drink milkyTea = new MilkDecorator(new Tea());
      milkyTea.getName();
  }
}

上面代码可以看出装饰器模式包含四个角色,抽象接口,具体事物,抽象装饰,具体装饰;但是并不是所有的装饰器模式都要有这四个角色。当只有一个具体事物时,可以让抽象装饰直接继承具体事务;当只有一个具体装饰时,可以让抽象装饰和具体装饰进行合并。

什么时候使用这一模式呢?

  • 采用继承的方式会产生大量的子类
  • 类不可以被继承
  • 动态的添加和删除对象的功能

装饰器模式的主要优点

  • 在不改变原有对象的情况下,可以动态的给一个对象添加功能,能够做到即插即用
  • 装饰器模式遵守设计模式原则的开闭原则

但是装饰器模式同样会增加许多的子类,过度的依赖装饰器模式也会增加程序的复杂性。

与代理模式的区别

装饰器模式的关注点在于给对象动态添加⽅法,⽽动态代理更注重对象的访问控制。动态代理通常会在代理类中创建被代理对象的实例,⽽装饰器模式会将装饰者作为构造⽅法的参数。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

5个节约生命的python小技巧

发表于 2021-11-09

介绍常用的5个python小技巧,人生苦短,我用Python。

Python是一种强大且易上手的语言,语法简洁优雅,不像Java那么繁琐废话,并且有一些特殊的函数或语法可以让代码变得更加简短精悍。

根据笔者经验,下面介绍常用的5个python小技巧:

  1. 字符串操作
  2. 列表推导
  3. lambda 及 map() 函数
  4. if、elif和else单行表达式
  5. zip()函数
  1. 字符串操作

Python善于用数学运算符(如+和*)对字符串进行操作:

  • + 拼接字符串
  • * 重复字符串
1
2
3
4
5
python复制代码my_string = "Hi Python..!"
print(my_string * 2)
#Hi Python..!Hi Python..!
print(my_string + " I love Python" * 2)
#Hi Python..! I love Python I love Python

也可以用切片操作[::-1]轻松反转一个字符串,并且不限于字符串(如列表翻转)!

1
2
3
4
5
6
python复制代码my_string = "Hi Python..!"
print(my_string[::-1])
# !..nohtyP iH
my_list = [1,2,3,4,5]
print(my_list[::-1])
# [5, 4, 3, 2, 1]

下面是对一个单词列表进行了反转拼接成字符串:

1
2
3
python复制代码word_list = ["awesome", "is", "this"]
print(' '.join(word_list[::-1]) + '!')
#this is awesome!

用.join()方法,''(空格)连接反转列表中的所有单词,并加上一个惊叹号!。

  1. 列表推导

5905342520623104

列表推导,一个可以改变你世界观的技巧!这是一个非常强大、直观和可读的方法,可以对列表进行快速操作。

假设,有一个随机的函数,返回一个数字的平方并加上5:

1
2
python复制代码def stupid_func(x):
return x**2 + 5

现在,想把函数stupid_func()应用于列表中的所有奇数,如果不用列表推导,笨办法如下:

1
2
3
4
5
6
7
8
9
10
python复制代码def stupid_func(x):
return x**2 + 5

my_list = [1, 2, 3, 4, 5]
new_list = []
for x in my_list:
if x % 2 != 0:
new_list.append(stupid_func(x))
print(new_list)
#[6, 14, 30]

如果用列表推导,代码瞬间变的优雅:

1
2
3
4
5
6
python复制代码def stupid_func(x):
return x**2 + 5

my_list = [1, 2, 3, 4, 5]
print([stupid_func(x) for x in my_list if x % 2 != 0])
#[6, 14, 30]

列表推导的语法:[ expression for item in list ],如果觉得不够花哨,还可以加上一个判断条件,比如上面的”奇数”条件: [expression for item in list if conditional]。本质上如下代码的功能:

1
2
3
python复制代码for item in list:
if conditional:
expression

Very Cool!。不过还可以更进一步,直接省去stupid_func()函数:

1
2
3
4
python复制代码my_list = [1, 2, 3, 4, 5]

print([x ** 2 + 5 for x in my_list if x % 2 != 0])
#[6, 14, 30]
  1. Lambda & Map函数

Lambda

Lambda看上去有点点奇怪,但奇怪的东西一般功能都很强大,一旦你掌握就很直观,省去大量废话代码。

基本上,Lambda函数是一个小型的匿名函数。为什么是匿名的?

因为Lambda最常被用来执行的简单操作,但不需要像def my_function()那样正儿八经,所以Lambda又名吊儿郎当函数(瞎编的,忽略忽略)。

改进上面的例子:def stupid_func(x)可以用一行Lambda函数来代替:

1
2
3
python复制代码stupid_func = (lambda x : x ** 2 + 5)
print([stupid_func(1), stupid_func(3), stupid_func(5)])
#[6, 14, 30]

那么为什么要使用这种奇怪的语法呢?当想进行一些简单操作而不需要定义实际函数时,这就变得很有用。

以一个数字列表为例。假设对列表进行排序?一种方法是使用 sorted() 方法:

1
2
3
python复制代码my_list = [2, 1, 0, -1, -2]
print(sorted(my_list))
#[-2, -1, 0, 1, 2]

sorted()函数可以完成排序,但假设想按每个数的平方进行排序呢?此时可用lambda函数来定义排序键key,这也是sorted()方法用来决定如何排序的:

1
2
3
python复制代码my_list = [2, 1, 0, -1, -2]
print(sorted(my_list, key = lambda x : x ** 2))
#[0, -1, 1, -2, 2]

Map函数

map是python内置函数,会根据提供的函数对指定的序列做映射。假设有一个列表,想把列表中的每个元素与另一个列表中的相应元素相乘,如何做到这一点?使用lambda函数和map!

1
2
python复制代码print(list(map(lambda x, y : x * y, [1, 2, 3], [4, 5, 6])))
#[4, 10, 18]

与下面这种常规废话代码,简单而优雅:

1
2
3
4
5
6
python复制代码x, y = [1, 2, 3], [4, 5, 6]
z = []
for i in range(len(x)):
z.append(x[i] * y[i])
print(z)
#[4, 10, 18]
  1. if-else 单行表达

在你的代码的某个地方,可能会有这样废话的条件语句:

1
2
3
4
5
6
7
python复制代码x = int(input())
if x >= 10:
print("Horse")
elif 1 < x < 10:
print("Duck")
else:
print("Baguette")

当运行程序时,提示从input()函数中输入一个信息,比如输入5,得到Duck。但其实也可以一行代码完成整个事情:

1
bash复制代码print("Horse" if x >= 10 else "Duck" if 1 < x < 10 else "Baguette")

一行代码简单直接! 翻阅你的旧代码,会发现很多判断都可以规约为一个if-else单行表达式。

  1. zip()函数

还记得map()函数部分两个列表元素按位相乘吗?

zip()使之更加简单。假设有两个列表,一个包含名,一个包含姓,如何有序地合并它们呢?使用zip()!

1
2
3
4
python复制代码first_names = ["Peter", "Christian", "Klaus"]
last_names = ["Jensen", "Smith", "Nistrup"]
print([' '.join(x) for x in zip(first_names, last_names)])
#['Peter Jensen', 'Christian Smith', 'Klaus Nistrup']

小节

上面列出的5个快速小技巧,希望对你有用。

如果觉得还可以,点赞收藏,好人一生平安👏。

pythontip 官方出品,Happy Coding!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Linux平台下的MongoDB基础 MongoDB简介 L

发表于 2021-11-09

MongoDB简介

MongoDB 是由C++语言编写的,是一个基于分布式文件存储的开源数据库系统。

在高负载的情况下,添加更多的节点,可以保证服务器性能。

MongoDB 旨在为WEB应用提供可扩展的高性能数据存储解决方案。

MongoDB 将数据存储为一个文档,数据结构由键值(key=>value)对组成。MongoDB 文档类似于 JSON 对象。字段值可以包含其他文档,数组及文档数组。

img

主要特点

  • MongoDB 是一个面向文档存储的数据库,操作起来比较简单和容易。
  • 你可以在MongoDB记录中设置任何属性的索引 (如:FirstName=”Sameer”,Address=”8 Gandhi Road”)来实现更快的排序。
  • 你可以通过本地或者网络创建数据镜像,这使得MongoDB有更强的扩展性。
  • 如果负载的增加(需要更多的存储空间和更强的处理能力) ,它可以分布在计算机网络中的其他节点上这就是所谓的分片。
  • Mongo支持丰富的查询表达式。查询指令使用JSON形式的标记,可轻易查询文档中内嵌的对象及数组。
  • MongoDb 使用update()命令可以实现替换完成的文档(数据)或者一些指定的数据字段 。
  • Mongodb中的Map/reduce主要是用来对数据进行批量处理和聚合操作。
  • Map和Reduce。Map函数调用emit(key,value)遍历集合中所有的记录,将key与value传给Reduce函数进行处理。
  • Map函数和Reduce函数是使用Javascript编写的,并可以通过db.runCommand或mapreduce命令来执行MapReduce操作。
  • GridFS是MongoDB中的一个内置功能,可以用于存放大量小文件。
  • MongoDB允许在服务端执行脚本,可以用Javascript编写某个函数,直接在服务端执行,也可以把函数的定义存储在服务端,下次直接调用即可。
  • MongoDB支持各种编程语言:RUBY,PYTHON,JAVA,C++,PHP,C#等多种语言。
  • MongoDB安装简单

Linux平台安装MongoDB

MongoDB 提供了 linux 各个发行版本 64 位的安装包,你可以在官网下载安装包。

MongoDB 源码下载地址:www.mongodb.com/download-ce…

安装前我们需要安装各个 Linux 平台依赖包。

Red Hat/CentOS:

1
复制代码sudo yum install libcurl openssl

Ubuntu 18.04 LTS (“Bionic”)/Debian 10 “Buster”:

1
arduino复制代码sudo apt-get install libcurl4 openssl

Ubuntu 16.04 LTS (“Xenial”)/Debian 9 “Stretch”:

1
arduino复制代码sudo apt-get install libcurl3 openssl

查看ubuntu的版本

1
css复制代码lsb_release -a

image-20211026193919108

image-20211026201305053

image-20211026201645786

这里我们选择 tgz 下载,下载完安装包,并解压 tgz(以下演示的是 64 位 Linux上的安装) 。

1
2
bash复制代码wget https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu1604-4.4.10.tgz #下载
wget https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu1604-4.4.10.tgz #解压

MongoDB 的可执行文件位于 bin 目录下,所以可以将其添加到 PATH 路径中

1
bash复制代码export PATH=<mongodb-install-directory>/bin:$PATH

****为你 MongoDB 的安装路径。

创建数据库目录

默认情况下 MongoDB 启动后会初始化以下两个目录:

  • 数据存储目录:/var/lib/mongodb
  • 日志文件目录:/var/log/mongodb

我们在启动前可以先创建这两个目录:

1
2
bash复制代码sudo mkdir -p /var/lib/mongo
sudo mkdir -p /var/log/mongodb

接下来启动 Mongodb 服务:

1
css复制代码mongod --dbpath /var/lib/mongo --logpath /var/log/mongodb/mongod.log --fork

MongoDB 后台管理 Shell

如果你需要进入 mongodb 后台管理,由于已经将MongoDB可执行文件添加到PATH路径,所以可以直接执行 mongo 命令文件。

MongoDB Shell 是 MongoDB 自带的交互式 Javascript shell,用来对 MongoDB 进行操作和管理的交互式环境。

当你进入 mongoDB 后台后,它默认会链接到 test 文档(数据库):

image-20211027223343278

MongoDB 概念解析

在mongodb中基本的概念是文档、集合、数据库。下表将帮助您更容易理解Mongo中的一些概念:

SQL术语/概念 MongoDB术语/概念 解释/说明
database database 数据库
table collection 数据库表/集合
row document 数据记录行/文档
column field 数据字段/域
index index 索引
table joins 表连接,MongoDB不支持
primary key primary key 主键,MongoDB自动将_id字段设置为主键

MongoDB 创建数据库

数据库

一个mongodb中可以建立多个数据库。

MongoDB的默认数据库为”db”,该数据库存储在data目录中。

MongoDB的单个实例可以容纳多个独立的数据库,每一个都有自己的集合和权限,不同的数据库也放置在不同的文件中。

“show dbs” 命令可以显示所有数据的列表。

1
2
3
4
5
6
sql复制代码toby@recsys:~$ mongo
MongoDB shell version: 2.6.10
connecting to: test
> show dbs
admin (empty)
local 0.078GB

执行 “db” 命令可以显示当前数据库对象或集合。

1
2
3
4
5
css复制代码toby@recsys:~$ mongo
MongoDB shell version: 2.6.10
connecting to: test
> db
test

运行”use”命令,可以连接到一个指定的数据库。

1
2
3
4
5
6
7
8
css复制代码toby@recsys:~$ mongo
MongoDB shell version: 2.6.10
connecting to: test
> use admin
switched to db admin
> db
admin
>

语法

MongoDB 创建数据库的语法格式如下:

1
perl复制代码use DATABASE_NAME

如果数据库不存在,则创建数据库,否则切换到指定数据库。

实例

以下实例我们创建了数据库 tobytest:

1
2
3
4
5
6
7
8
css复制代码toby@recsys:~$ mongo
MongoDB shell version: 2.6.10
connecting to: test
> use tobytest
switched to db tobytest
> db
tobytest
>

如果你想查看所有数据库,可以使用 show dbs 命令:

1
2
3
4
sql复制代码> show dbs
admin (empty)
local 0.078GB
>

可以看到,我们刚创建的数据库 tobytest并不在数据库的列表中, 要显示它,我们需要向 tobytest数据库插入一些数据。

1
2
3
4
5
6
7
sql复制代码> db.tobytest.insert({"name":"Toby"})
WriteResult({ "nInserted" : 1 })
> show dbs
admin (empty)
local 0.078GB
tobytest 0.078GB
>

MongoDB 中默认的数据库为 test,如果你没有创建新的数据库,集合将存放在 test 数据库中。

注意: 在 MongoDB 中,集合只有在内容插入后才会创建! 就是说,创建集合(数据表)后要再插入一个文档(记录),集合才会真正创建。

MongoDB 创建集合

MongoDB 中使用 createCollection() 方法来创建集合。

语法格式:

1
scss复制代码db.createCollection(name, options)

参数说明:

  • name: 要创建的集合名称
  • options: 可选参数, 指定有关内存大小及索引的选项

options 可以是如下参数:

字段 类型 描述
capped 布尔 (可选)如果为 true,则创建固定集合。固定集合是指有着固定大小的集合,当达到最大值时,它会自动覆盖最早的文档。 当该值为 true 时,必须指定 size 参数。
autoIndexId 布尔 3.2 之后不再支持该参数。(可选)如为 true,自动在 _id 字段创建索引。默认为 false。
size 数值 (可选)为固定集合指定一个最大值,即字节数。 如果 capped 为 true,也需要指定该字段。
max 数值 (可选)指定固定集合中包含文档的最大数量。

在插入文档时,MongoDB 首先检查固定集合的 size 字段,然后检查 max 字段。

实例

在 tobytest 数据库中创建 tobycollection 集合:

1
2
3
4
5
javascript复制代码> use tobytest
switched to db tobytest
> db.createCollection("tobycollection")
{ "ok" : 1 }
>

如果要查看已有集合,可以使用 show collections 或 show tables 命令:

1
2
3
4
5
markdown复制代码> show tables
system.indexes
tobycollection
tobytest
>

MongoDB 删除集合

MongoDB 中使用 drop() 方法来删除集合。

语法格式:

1
scss复制代码db.collection.drop()

参数说明:

  • 无

返回值

如果成功删除选定集合,则 drop() 方法返回 true,否则返回 false。

实例

在数据库 tobytest中,我们可以先通过 show collections 命令查看已存在的集合:

1
2
3
4
5
6
7
markdown复制代码> use tobytest
switched to db tobytest
> show collections
system.indexes
tobycollection
tobytest
>

接着删除集合 tobycollection:

1
2
3
markdown复制代码> db.tobycollection.drop()
true
>

通过 show collections 再次查看数据库 tobytest中的集合:

1
2
3
4
markdown复制代码> show collections
system.indexes
tobytest
>

从结果中可以看出 tobycollection集合已被删除。

MongoDB 插入文档

文档的数据结构和 JSON 基本一样。

所有存储在集合中的数据都是 BSON 格式。

BSON 是一种类似 JSON 的二进制形式的存储格式,是 Binary JSON 的简称。

插入文档

MongoDB 使用 insert() 或 save() 方法向集合中插入文档,语法如下:

1
2
3
javascript复制代码db.COLLECTION_NAME.insert(document)
或
db.COLLECTION_NAME.save(document)
  • save():如果 _id 主键存在则更新数据,如果不存在就插入数据。该方法新版本中已废弃,可以使用 db.collection.insertOne() 或 db.collection.replaceOne() 来代替。
  • insert(): 若插入的数据主键已经存在,则会抛 org.springframework.dao.DuplicateKeyException 异常,提示主键重复,不保存当前数据。

实例

以下文档可以存储在 MongoDB 的 tobytest 数据库 的 col 集合中:

1
2
3
4
5
6
7
php复制代码> db.col.insert({title:'Toby MongoDB',
... description:'this is MongoDB',
... tags:['mongodb','database','NoSQL'],
... likes:1
... })
WriteResult({ "nInserted" : 1 })
>

以上实例中 col 是我们的集合名,如果该集合不在该数据库中, MongoDB 会自动创建该集合并插入文档。

查看已插入文档:

1
2
3
javascript复制代码> db.col.find()
{ "_id" : ObjectId("617970fc286e9ff2b1250d70"), "title" : "Toby MongoDB", "description" : "this is MongoDB", "tags" : [ "mongodb", "database", "NoSQL" ], "likes" : 1 }
>

我们也可以将数据定义为一个变量,如下所示:

1
2
3
4
5
python复制代码> document=({title:'Toby another MongoDB',
... description:'this is another MongoDB',
... tags:['mongodb','database','NoSQL'],
... likes:2
... })

执行后显示结果如下:

1
2
3
4
5
6
7
8
9
10
json复制代码{
"title" : "Toby another MongoDB",
"description" : "this is another MongoDB",
"tags" : [
"mongodb",
"database",
"NoSQL"
],
"likes" : 2
}

执行插入操作:

1
2
3
4
5
6
javascript复制代码> db.col.insert(document)
WriteResult({ "nInserted" : 1 })
> db.col.find()
{ "_id" : ObjectId("617970fc286e9ff2b1250d70"), "title" : "Toby MongoDB", "description" : "this is MongoDB", "tags" : [ "mongodb", "database", "NoSQL" ], "likes" : 1 }
{ "_id" : ObjectId("61797229286e9ff2b1250d71"), "title" : "Toby another MongoDB", "description" : "this is another MongoDB", "tags" : [ "mongodb", "database", "NoSQL" ], "likes" : 2 }
>

MongoDB 更新文档

MongoDB 使用 update() 和 save() 方法来更新集合中的文档。接下来让我们详细来看下两个函数的应用及其区别。


update() 方法

update() 方法用于更新已存在的文档。语法格式如下:

1
2
3
4
5
6
7
8
9
php复制代码db.collection.update(
<query>,
<update>,
{
upsert: <boolean>,
multi: <boolean>,
writeConcern: <document>
}
)

参数说明:

  • query : update的查询条件,类似sql update查询内where后面的。
  • update : update的对象和一些更新的操作符(如,,,inc…)等,也可以理解为sql update查询内set后面的
  • upsert : 可选,这个参数的意思是,如果不存在update的记录,是否插入objNew,true为插入,默认是false,不插入。
  • multi : 可选,mongodb 默认是false,只更新找到的第一条记录,如果这个参数为true,就把按条件查出来多条记录全部更新。
  • writeConcern :可选,抛出异常的级别。

实例

我们在集合 col 中插入如下数据:

1
2
3
4
5
6
7
php复制代码> db.col.insert({title:'Toby MongoDB',
... description:'this is MongoDB',
... tags:['mongodb','database','NoSQL'],
... likes:1
... })
WriteResult({ "nInserted" : 1 })
>

接着我们通过 update() 方法来更新标题(title):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
css复制代码> db.col.update({'title':'Toby MongoDB'},{$set:{'title':'MongoDB'}})
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
> db.col.find().pretty()
{
"_id" : ObjectId("617970fc286e9ff2b1250d70"),
"title" : "MongoDB",
"description" : "this is MongoDB",
"tags" : [
"mongodb",
"database",
"NoSQL"
],
"likes" : 1
}
{
"_id" : ObjectId("61797229286e9ff2b1250d71"),
"title" : "Toby another MongoDB",
"description" : "this is another MongoDB",
"tags" : [
"mongodb",
"database",
"NoSQL"
],
"likes" : 2
}
>

可以看到标题(title)由原来的 “Toby MongoDB” 更新为了 “MongoDB”。

MongoDB 删除文档

MongoDB remove() 函数是用来移除集合中的数据。

MongoDB 数据更新可以使用 update() 函数。在执行 remove() 函数前先执行 find() 命令来判断执行的条件是否正确,这是一个比较好的习惯。

语法

remove() 方法的基本语法格式如下所示:

1
2
3
4
xml复制代码db.collection.remove(
<query>,
<justOne>
)

如果你的 MongoDB 是 2.6 版本以后的,语法格式如下:

1
2
3
4
5
6
7
php复制代码db.collection.remove(
<query>,
{
justOne: <boolean>,
writeConcern: <document>
}
)

参数说明:

  • query :(可选)删除的文档的条件。
  • justOne : (可选)如果设为 true 或 1,则只删除一个文档,如果不设置该参数,或使用默认值 false,则删除所有匹配条件的文档。
  • writeConcern :(可选)抛出异常的级别。

实例

以下文档我们执行两次插入操作:

1
2
3
4
5
php复制代码> db.col.insert({title:'Toby MongoDB', description:'this is MongoDB', tags:['mongodb','database','NoSQL'], likes:1 })
WriteResult({ "nInserted" : 1 })
> db.col.insert({title:'Toby MongoDB', description:'this is MongoDB', tags:['mongodb','database','NoSQL'], likes:1 })
WriteResult({ "nInserted" : 1 })
>

使用 find() 函数查询数据:

1
2
3
4
5
6
css复制代码> db.col.find()
{ "_id" : ObjectId("617970fc286e9ff2b1250d70"), "title" : "MongoDB", "description" : "this is MongoDB", "tags" : [ "mongodb", "database", "NoSQL" ], "likes" : 1 }
{ "_id" : ObjectId("61797229286e9ff2b1250d71"), "title" : "Toby another MongoDB", "description" : "this is another MongoDB", "tags" : [ "mongodb", "database", "NoSQL" ], "likes" : 2 }
{ "_id" : ObjectId("6179747d286e9ff2b1250d72"), "title" : "Toby MongoDB", "description" : "this is MongoDB", "tags" : [ "mongodb", "database", "NoSQL" ], "likes" : 1 }
{ "_id" : ObjectId("61797481286e9ff2b1250d73"), "title" : "Toby MongoDB", "description" : "this is MongoDB", "tags" : [ "mongodb", "database", "NoSQL" ], "likes" : 1 }
>

接下来我们移除 title 为 ‘Toby MongoDB’ 的文档:

1
2
3
4
5
6
csharp复制代码> db.col.remove({'title':'Toby MongoDB'})
WriteResult({ "nRemoved" : 2 }) # 删除了两个
> db.col.find()
{ "_id" : ObjectId("617970fc286e9ff2b1250d70"), "title" : "MongoDB", "description" : "this is MongoDB", "tags" : [ "mongodb", "database", "NoSQL" ], "likes" : 1 }
{ "_id" : ObjectId("61797229286e9ff2b1250d71"), "title" : "Toby another MongoDB", "description" : "this is another MongoDB", "tags" : [ "mongodb", "database", "NoSQL" ], "likes" : 2 }
>

如果你只想删除第一条找到的记录可以设置 justOne 为 1,如下所示:

1
csharp复制代码>db.COLLECTION_NAME.remove(DELETION_CRITERIA,1)

如果你想删除所有数据,可以使用以下方式(类似常规 SQL 的 truncate 命令):

1
2
3
4
scss复制代码> db.col.remove({})
WriteResult({ "nRemoved" : 2 })
> db.col.find()
>

MongoDB 查询文档

MongoDB 查询文档使用 find() 方法。

find() 方法以非结构化的方式来显示所有文档。

语法

MongoDB 查询数据的语法格式如下:

1
lua复制代码db.collection.find(query, projection)
  • query :可选,使用查询操作符指定查询条件
  • projection :可选,使用投影操作符指定返回的键。查询时返回文档中所有键值, 只需省略该参数即可(默认省略)。

如果你需要以易读的方式来读取数据,可以使用 pretty() 方法,语法格式如下:

1
scss复制代码>db.col.find().pretty()

pretty() 方法以格式化的方式来显示所有文档。

实例

以下实例我们查询了集合 col 中的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
php复制代码> db.col.insert({title:'Toby MongoDB', description:'this is MongoDB',by:'Toby', tags:['mongodb','database','NoSQL'], likes:100 })
WriteResult({ "nInserted" : 1 })
> db.col.find().pretty()
{
"_id" : ObjectId("6179772f286e9ff2b1250d75"),
"title" : "Toby MongoDB",
"description" : "this is MongoDB",
"by" : "Toby",
"tags" : [
"mongodb",
"database",
"NoSQL"
],
"likes" : 100
}
>

除了 find() 方法之外,还有一个 findOne() 方法,它只返回一个文档。

MongoDB AND 条件

MongoDB 的 find() 方法可以传入多个键(key),每个键(key)以逗号隔开,即常规 SQL 的 AND 条件。

语法格式如下:

1
php复制代码>db.col.find({key1:value1, key2:value2}).pretty()

实例

以下实例通过 by 和 title 键来查询 Toby 中 Toby MongoDB 的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
javascript复制代码> db.col.find({'by':'Toby','title':'Toby MongoDB'}).prettydb.col.find({'by':'Toby','title':'Toby MongoDB'}).pretty()
{
"_id" : ObjectId("6179772f286e9ff2b1250d75"),
"title" : "Toby MongoDB",
"description" : "this is MongoDB",
"by" : "Toby",
"tags" : [
"mongodb",
"database",
"NoSQL"
],
"likes" : 100
}
>

以上实例中类似于 WHERE 语句:WHERE by=’Toby’ AND title=’Toby MongoDB’


MongoDB OR 条件

MongoDB OR 条件语句使用了关键字 $or,语法格式如下:

1
2
3
4
5
6
7
css复制代码>db.col.find(
{
$or: [
{key1: value1}, {key2:value2}
]
}
).pretty()

实例

以下实例中,我们演示了查询键 by 值为 Toby或键 title 值为 Toby MongoDB 的文档。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
css复制代码> db.col.find({$or:[{"by":"Toby"},{"title":"Toby MongoDB"}]}).pretty()
{
"_id" : ObjectId("6179772f286e9ff2b1250d75"),
"title" : "Toby MongoDB",
"description" : "this is MongoDB",
"by" : "Toby",
"tags" : [
"mongodb",
"database",
"NoSQL"
],
"likes" : 100
}
>

AND 和 OR 联合使用

以下实例演示了 AND 和 OR 联合使用,类似常规 SQL 语句为: ‘where likes>50 AND (by = ‘Toby’ OR title = ‘Toby MongoDB’)’

1
2
3
4
5
6
7
8
9
10
11
12
13
14
css复制代码> db.col.find({"likes":{$gt:50},$or:[{"by":"Toby"},{"title":"Toby MongoDB"}]}).pretty()
{
"_id" : ObjectId("6179772f286e9ff2b1250d75"),
"title" : "Toby MongoDB",
"description" : "this is MongoDB",
"by" : "Toby",
"tags" : [
"mongodb",
"database",
"NoSQL"
],
"likes" : 100
}
>

MongoDB 排序


MongoDB sort() 方法

在 MongoDB 中使用 sort() 方法对数据进行排序,sort() 方法可以通过参数指定排序的字段,并使用 1 和 -1 来指定排序的方式,其中 1 为升序排列,而 -1 是用于降序排列。

语法

sort()方法基本语法如下所示:

1
lua复制代码>db.COLLECTION_NAME.find().sort({KEY:1})

实例

col 集合中的数据如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
css复制代码> db.col.find().pretty()
{
"_id" : ObjectId("61797a56286e9ff2b1250d78"),
"title" : "Toby PHP",
"description" : "this is PHP",
"by" : "Toby",
"tags" : [
"PHP",
"Language"
],
"likes" : 100
}
{
"_id" : ObjectId("61797a62286e9ff2b1250d79"),
"title" : "Toby JAVA",
"description" : "this is JAVA",
"by" : "Toby",
"tags" : [
"JAVA",
"Language"
],
"likes" : 50
}
{
"_id" : ObjectId("61797a83286e9ff2b1250d7a"),
"title" : "Toby Python",
"description" : "this is Python",
"by" : "Toby",
"tags" : [
"Python",
"Language"
],
"likes" : 20
}
>

以下实例演示了 col 集合中的数据按字段 likes 的降序排列:

1
2
3
4
5
javascript复制代码> db.col.find({},{'title':1,_id:0}).sort({"likes":-1})
{ "title" : "Toby PHP" }
{ "title" : "Toby JAVA" }
{ "title" : "Toby Python" }
>

Python MongoDB


PyMongo

Python 要连接 MongoDB 需要 MongoDB 驱动,这里我们使用 PyMongo 驱动来连接。

pip 安装

pip 是一个通用的 Python 包管理工具,提供了对 Python 包的查找、下载、安装、卸载的功能。

安装 pymongo:

1
ruby复制代码$ python3 -m pip install pymongo

测试 PyMongo

接下来我们可以创建一个测试文件 demo_test_mongodb.py,代码如下:

1
arduino复制代码import pymongo

执行以上代码文件,如果没有出现错误,表示安装成功。

创建数据库

创建一个数据库

创建数据库需要使用 MongoClient 对象,并且指定连接的 URL 地址和要创建的数据库名。

如下实例中,我们创建的数据库 pydb:

实例

1
2
3
python复制代码import pymongo
myclient=pymongo.MongoClient("mongodb://localhost:27017/")
mydb=myclient["pydb"]

注意: 在 MongoDB 中,数据库只有在内容插入后才会创建! 就是说,数据库创建后要创建集合(数据表)并插入一个文档(记录),数据库才会真正创建。

判断数据库是否已存在

我们可以读取 MongoDB 中的所有数据库,并判断指定的数据库是否存在:

实例

1
2
3
4
5
6
7
8
9
10
python复制代码import pymongo
myclient=pymongo.MongoClient("mongodb://localhost:27017/")
mydb=myclient["pydb"]

dblist = myclient.list_database_names()
# dblist = myclient.database_names()
if "pydb" in dblist:
print("数据库已存在!")
else:
print('数据库不存在')

**注意:**database_names 在最新版本的 Python 中已废弃,Python3.7+ 之后的版本改为了 list_database_names()。

image-20211030141217841

创建集合

MongoDB 中的集合类似 SQL 的表。

创建一个集合

MongoDB 使用数据库对象来创建集合,实例如下:

实例

1
2
3
4
5
python复制代码import pymongo
myclient=pymongo.MongoClient("mongodb://localhost:27017/")
mydb=myclient["pydb"]

mycol=myclient["col_set"]

注意: 在 MongoDB 中,集合只有在内容插入后才会创建! 就是说,创建集合(数据表)后要再插入一个文档(记录),集合才会真正创建。

判断集合是否已存在

我们可以读取 MongoDB 数据库中的所有集合,并判断指定的集合是否存在:

实例

1
2
3
4
5
6
7
8
9
10
11
python复制代码import pymongo
myclient=pymongo.MongoClient("mongodb://localhost:27017/")
mydb=myclient["pydb"]

mycol=myclient["col_set"]

collist = mydb. list_collection_names()
if "col_set" in collist: # 判断 sites 集合是否存在
print("集合已存在!")
else:
print('集合不存在')

image-20211030141526295

Python Mongodb 插入文档

MongoDB 中的一个文档类似 SQL 表中的一条记录。

插入集合

集合中插入文档使用 insert_one() 方法,该方法的第一参数是字典 name => value 对。

以下实例向 col_set 集合中插入文档:

实例

1
2
3
4
5
6
7
8
9
10
python复制代码import pymongo

myclient = pymongo.MongoClient("mongodb://localhost:27017/")
mydb = myclient["pydb"]
mycol = mydb["col_set"]

mydict = { "name": "Toby", "age": "23", "url": "https://juejin.cn/user/3403743731649863" }

x = mycol.insert_one(mydict)
print(x)

image-20211030142137931

在命令行看一下是否插入成功

1
2
3
4
5
css复制代码> use pydb
switched to db pydb
> db.col_set.find()
{ "_id" : ObjectId("617ce42cbc6011eaf1529012"), "name" : "Toby", "url" : "https://juejin.cn/user/3403743731649863", "age" : "23" }
>

插入多个文档

集合中插入多个文档使用 insert_many() 方法,该方法的第一参数是字典列表。

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
python复制代码import pymongo

myclient = pymongo.MongoClient("mongodb://localhost:27017/")
mydb = myclient["pydb"]
mycol = mydb["col_set"]

mylist = [
{ "name": "Tom", "age": "100", "url": "https://juejin.cn/user/3403743731649863" },
{ "name": "Mary", "age": "101", "url": "https://juejin.cn/user/3403743731649863" },
{ "name": "Timi", "age": "10", "url": "https://juejin.cn/user/3403743731649863" },
]

x = mycol.insert_many(mylist)

# 输出插入的所有文档对应的 _id 值
print(x.inserted_ids)

image-20211030142656115

在命令行看一下是否插入成功

1
2
3
4
5
6
7
8
css复制代码> use pydb
switched to db pydb
> db.col_set.find()
{ "_id" : ObjectId("617ce42cbc6011eaf1529012"), "name" : "Toby", "url" : "https://juejin.cn/user/3403743731649863", "age" : "23" }
{ "_id" : ObjectId("617ce591826d13d898f97890"), "name" : "Tom", "url" : "https://juejin.cn/user/3403743731649863", "age" : "100" }
{ "_id" : ObjectId("617ce591826d13d898f97891"), "name" : "Mary", "url" : "https://juejin.cn/user/3403743731649863", "age" : "101" }
{ "_id" : ObjectId("617ce591826d13d898f97892"), "name" : "Timi", "url" : "https://juejin.cn/user/3403743731649863", "age" : "10" }
>

Python Mongodb 查询文档

MongoDB 中使用了 find 和 find_one 方法来查询集合中的数据,它类似于 SQL 中的 SELECT 语句。

查询一条数据

我们可以使用 find_one() 方法来查询集合中的一条数据。

查询 col_set 文档中的第一条数据:

实例

1
2
3
4
5
6
7
8
9
python复制代码import pymongo

myclient = pymongo.MongoClient("mongodb://localhost:27017/")
mydb = myclient["pydb"]
mycol = mydb["col_set"]

x = mycol.find_one()

print(x)

image-20211030142943707

查询集合中所有数据

find() 方法可以查询集合中的所有数据,类似 SQL 中的 SELECT * 操作。

以下实例查找 col_set 集合中的所有数据:

实例

1
2
3
4
5
6
7
8
python复制代码import pymongo

myclient = pymongo.MongoClient("mongodb://localhost:27017/")
mydb = myclient["pydb"]
mycol = mydb["col_set"]

for x in mycol.find():
print(x)

image-20211030143207556

查询指定字段的数据

我们可以使用 find() 方法来查询指定字段的数据,将要返回的字段对应值设置为 1。

实例

1
2
3
4
5
6
7
8
python复制代码import pymongo

myclient = pymongo.MongoClient("mongodb://localhost:27017/")
mydb = myclient["pydb"]
mycol = mydb["col_set"]

for x in mycol.find({},{ "_id": 0, "name": 1, "age": 1 }):
print(x)

image-20211030144042132

根据指定条件查询

我们可以在 find() 中设置参数来过滤数据。

以下实例查找 name 字段为 “Toby” 的数据:

实例

1
2
3
4
5
6
7
8
9
10
11
12
python复制代码import pymongo

myclient = pymongo.MongoClient("mongodb://localhost:27017/")
mydb = myclient["pydb"]
mycol = mydb["col_set"]

myquery = { "name": "Toby" }

mydoc = mycol.find(myquery)

for x in mydoc:
print(x)

image-20211030144414902

返回指定条数记录

如果我们要对查询结果设置指定条数的记录可以使用 limit() 方法,该方法只接受一个数字参数。

以下实例返回 3 条文档记录:

实例

1
2
3
4
5
6
7
8
9
10
11
python复制代码import pymongo

myclient = pymongo.MongoClient("mongodb://localhost:27017/")
mydb = myclient["pydb"]
mycol = mydb["col_set"]

myresult = mycol.find().limit(3)

# 输出结果
for x in myresult:
print(x)

image-20211030144609160

Python Mongodb 修改文档

我们可以在 MongoDB 中使用 update_one() 方法修改文档中的记录。该方法第一个参数为查询的条件,第二个参数为要修改的字段。

如果查找到的匹配数据多于一条,则只会修改第一条。

以下实例将 age字段的值 23改为 12345:

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
python复制代码import pymongo

myclient = pymongo.MongoClient("mongodb://localhost:27017/")
mydb = myclient["pydb"]
mycol = mydb["col_set"]

myquery = { "age": "23" }
newvalues = { "$set": { "age": "12345" } }

mycol.update_one(myquery, newvalues)

# 输出修改后的 "sites" 集合
for x in mycol.find():
print(x)

image-20211030144819907

排序

sort() 方法可以指定升序或降序排序。

sort() 方法第一个参数为要排序的字段,第二个字段指定排序规则,1 为升序,-1 为降序,默认为升序。

对字段 age 按升序排序:

实例

1
2
3
4
5
6
7
8
9
python复制代码import pymongo

myclient = pymongo.MongoClient("mongodb://localhost:27017/")
mydb = myclient["pydb"]
mycol = mydb["col_set"]

mydoc = mycol.find().sort("age")
for x in mydoc:
print(x)

image-20211030145059219

对字段 age按降序排序:

1
2
3
4
5
6
7
8
9
10
python复制代码import pymongo

myclient = pymongo.MongoClient("mongodb://localhost:27017/")
mydb = myclient["pydb"]
mycol = mydb["col_set"]

mydoc = mycol.find().sort("alexa", -1)

for x in mydoc:
print(x)

image-20211030145239034

Python Mongodb 删除数据

我们可以使用 delete_one() 方法来删除一个文档,该方法第一个参数为查询对象,指定要删除哪些数据。

以下实例删除 name 字段值为 “Timi” 的文档:

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
python复制代码import pymongo

myclient = pymongo.MongoClient("mongodb://localhost:27017/")
mydb = myclient["pydb"]
mycol = mydb["col_set"]

myquery = { "name": "Timi" }

mycol.delete_one(myquery)

# 删除后输出
for x in mycol.find():
print(x)

image-20211030145408484

删除集合中的所有文档

delete_many() 方法如果传入的是一个空的查询对象,则会删除集合中的所有文档:

实例

1
2
3
4
5
6
7
8
9
python复制代码import pymongo

myclient = pymongo.MongoClient("mongodb://localhost:27017/")
mydb = myclient["pydb"]
mycol = mydb["col_set"]

x = mycol.delete_many({})

print(x.deleted_count, "个文档已删除")

image-20211030145528857

删除集合

我们可以使用 drop() 方法来删除一个集合。

以下实例删除了 col_set集合:

实例

1
2
3
4
5
6
7
python复制代码import pymongo

myclient = pymongo.MongoClient("mongodb://localhost:27017/")
mydb = myclient["pydb"]
mycol = mydb["col_set"]

mycol.drop()

我们在终端查看一下

1
2
3
4
5
markdown复制代码> use pydb
switched to db pydb
> show tables
system.indexes
>

参考链接:

  • www.runoob.com/python3/pyt…
  • www.runoob.com/mongodb/mon…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

小册上新 Go 语言原理与实践

发表于 2021-11-09

掘金联合 4 位字节跳动讲师,推出《Go 语言原理与实践》。课程中各讲师真人出镜,有原理剖析,又有案例讲解,手把手带你攻克 Go 语言层面的难题。

自 2010 年 5 月起,谷歌开始将 Go 语言投入到后端基础设施的实际开发中,作为其母公司,内部有很多我们耳熟能详的应用都有使用 Go 语言,比如谷歌地图、Google Cloud、youtube.com、dl.google.com 等。

随着时间的验证,Go 逐渐深得开发者们的信赖和喜爱,从 C++ 和 Java 中杀出一条血路。据 HackerEarth 调研报告显示,最近几年 Go 一直荣登“程序员最想学习的语言”的榜首。这都得益于其“大道至简”的设计,学习和上手开发成本极低,很容易就可以写出能够运行的程序。

据统计,Go 在全球范围内的使用量都在增长,尤其是在大势的云计算领域,比如,用 Go 编写的云基础设施项目—— Docker 和 Kubernetes。至于使用国家,那更是遍地开花。

国内使用 Go 的公司非常多,字节、阿里、百度、腾讯、小米等互联网大厂赫然在列。而各个大厂中 Go 开发工程师的薪资待遇也很可观,很有市场竞争力,让你一不小心就跨进高薪阶层。

随着 Go 逐渐成为各个互联网大厂的新宠,市面上对 Go 语言的学习需求也越来越多。那么,Go 语言要怎么学呢?跟着谁来学呢?

据字节技术大数据显示,Go 在字节跳动常用开发语言中占据比例最高,并且大部分产品后端都是使用的 Go,甚至还研发出了自己的组件,不难看出,字节跳动的 Go 实力在行业内的水平绝对是拔尖的,如果能跟着它学,肯定没有错。当然,如果能在学习过程中 get 到一些小 tips,一下迈进大厂,岂不更是美哉。

学得早不如赶得巧,这里我给你推荐一门新鲜出炉的好课:《Go 语言原理与实践》视频课程。

这门课程是由掘金与 4 位字节跳动研发工程师精心设计的,课程中各讲师真人出镜,有原理剖析,又有案例讲解,手把手带你攻克 Go 语言层面的难题。

这个原价 69 元的课程,上新期间只要 34.5 元,就可永久解锁。还等什么,赶紧扫描👇🏻👇🏻👇🏻海报二维码,加入学习吧~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…388389390…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%