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

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


  • 首页

  • 归档

  • 搜索

Mysql 范围查询导致索引失效

发表于 2021-10-30

前言

此文章包含Mysql的Where条件查询执行过程、范围查询使联合索引停止匹配、回表操作分析、常见索引失效场景、Extra分析等知识。

背景

6千万数据量的数据表出现了一个慢查询,复现sql语句发现查询并没有走索引而是走全表查询,找出索引失效原因。

1
2
sql复制代码# sql语句
EXPLAIN SELECT count(*) FROM order_recipient_extend_tab WHERE start_date>'1628442000' and start_date<'1631120399' and station_id='1809' and status='2';

image-20211027103123015.png
order_recipient_extend_tab 表有6千万数据,慢查询的查询字段包括 start_date、station_id、status,按照索引设计初衷会走但实际上失效的索引是:

联合索引 字段1 字段2 字段3
idx_date_station_driver start_date station_id driver_id

Where条件查询执行过程

了解Mysql怎么执行where条件查询,能更快速清晰地洞见索引失效的原因。此次慢查询中匹配度高的索引是idx_date_station_driver,分析此次慢查询中where条件查询的执行过程。

Mysql对where条件提取规则主要可以归纳为三大类:Index Key (First Key & Last Key),Index Filter,Table Filter。

Index Key

Index Key用于确定此次sql查询在索引树上的范围。一个范围包括起始和终止,Index First Key用于定位索引查询的起始范围,Index Last Key用于定位索引查询的终止范围。

  • Index First Key

提取规则:从索引的第一个字段开始,检查该字段在where条件中是否存在,若存在且条件是=、>=,则将对应的条件加入Index First Key之中,继续读取索引的下一个字段;若存在且条件是>,则将对应的条件加入Index First Key中,然后终止Index First Key的提取;若不存在,也终止Index First Key的提取。

  • Index Last Key

与Index First Key正好相反,提取规则:从索引的第一个字段开始,检查其在where条件中是否存在,若存在并且条件是=、<=,则将对应条件加入到Index Last Key中,继续提取索引的下一个字段;若存在并且条件是 < ,则将条件加入到Index Last Key中,然后终止提取;若不存在,也终止Index Last Key的提取。

按照Index Key的提取规则,在此次慢查询中提取出来的Index Last Key为:start_date>’1628442000’,Index Last Key为: start_date<’1631120399’。

Index First Key只是用来定位索引的起始范围,使用Index First Key条件,从索引B+树的根节点开始,使用二分搜索方法快速索引到正确的叶节点位置。Where查询过程中Index First Key只做了一次判断。

Index Last Key,用来定位索引的终止范围,因此对于起始范围之后读到的每一条索引记录,均需要判断是否已经超过了Index Last Key的范围,若超过,则当前查询结束。

Index Filter

在Index Key确定的索引范围中,并不是所有的索引记录都满足查询条件。比如Index Last Key和Index Last Key范围中,不是所有索引记录都满足 station_id = ‘1809’。这个时候就需要用到Index Filter了。

Index Filter,又名索引下推,用于过滤索引查询范围中不满足查询条件的记录。对于索引范围中的每一条记录,均需要与Index Filter进行对比,若不满足Index Filter则直接丢弃,继续读取索引下一条记录。

Index Filter的提取规则:从索引的第一个字段开始,检查其在where条件中是否存在,若存在且条件仅为 =,则跳过第一字段继续检查索引下一字段,下一索引列采取相同的提取规则(解释:条件为=的字段已经在Index Key中过滤掉了);若存在且条件为 >=、>、<、<= 其中的几种,则跳过当前索引字段,将其余where条件中索引相关字段全部加入到Index Filter之中。

按照Index Filter的提取规则,在此次慢查询中提取出来的Index Filter为:station_id=’1809’。在Index Key确定的索引查询范围中,遍历索引记录时都需要比较 station_id=’1809’,不满足该条件则直接丢失,继续读取索引下一条记录。

Table Filter

Table Filter用于过滤掉索引无法过滤的数据。在二级索引中通过主键回表查询到整行记录后,判断该记录是否符合Table Filter条件,不符合则丢失,继续判断下一条记录。

提取规则很简单:所有不属于索引字段的查询条件,均归为Table Filter之中。按照Table Filter的提取规则,在此次查询中Table Filter为:status=‘2’。

总结和补充

Index Key用于确定索引扫描的范围;Index Filter用于在索引中进行过滤;Table Filter需要回表后在Mysql服务器进行过滤。

Index Key和Index Filter发生在InnoDB存储层,Table Filter发生在Mysql Server层。

在 MySQL5.6 之前,并不区分Index Filter与Table Filter,统统将Index First Key与Index Last Key范围内的索引记录,回表读取完整记录,然后返回给MySQL Server层进行过滤。

在MySQL 5.6及之后,Index Filter与Table Filter分离,Index Filter下降到InnoDB的存储引擎层进行过滤,减少了回表与返回MySQL Server层的记录交互开销,提高了SQL的执行效率。

分析索引失效原因

首先是count(🌟),此时通配符 * 经优化并不会拓展所有列,实际上会忽略所有的列直接统计行数。所以只想收集行数最好使用count(🌟)。

接下来分析where语句。假设此慢查询会使用了二级索引idx_date_station_driver,按照上面where条件查询的执行过程,该慢查询的Index First Key为start_date>’1628442000’,Index Last Key为: start_date<’1631120399’,Index Filter为:station_id=’1809’,Table Filter为:status=‘2’。

提取Index First Key后在索引B+树上定位索引起始范围就是索引匹配的过程,在索引B+树上使用二分搜索方法快速定位符合查询条件的起始叶子节点。通过上文Where条件查询执行过程,我们知道该慢查询的where条件(start_date>'1628442000' and start_date<'1631120399' and status='2' and station_id='1809'),只匹配了索引idx_date_station_driver(start_date, station_id, driver_id)的第一个字段,即只匹配了idx_date_station_driver(start_date),station_id=’1809‘精确查询并没有作用到匹配索引上,而是在Index Filter即索引下推过程中发挥了作用。实际上这里是因为范围查询使联合索引停止匹配。

范围查询导致联合索引停止匹配

为什么范围查询会使联合索引停止匹配?这里涉及到最左前缀匹配原理。假设建立一个联合索引 index(a, b),会先对a进行排序,在a相等的情况下对b进行排序,如下图所示。在该索引树上,a是全局有序的,而b则处于全局无序、局部有序状态。从全局来看,b的值为1、2、1、4、1、2,只有 b=2 查询条件无法直接使用该索引;从局部来看,当a的值确定时,b则是有序状态,a=2 && b=4可以使用该索引。所以范围查询使联合索引停止匹配的根本原因是,索引树上非首字段的有序状态依赖前一个字段相等情况,而范围查询破坏了下一个索引字段局部有序状态,导致索引停止匹配。

image-20211018023009060.png

范围查询使联合索引停止匹配,并不能在索引匹配的时候就过滤掉 station_id不等于’1809’ 的数据,导致Mysql在索引上的扫描范围Index First Key和Index Last Key完全由start_timestamp_of_date时间决定。start_timestamp_of_date范围查询可以过滤73%数据量,而station_id=’1809’精确查询能过滤掉99%的数据量。

查询条件 数据量 占比
所有数据 6367万 100%
start_timestamp_of_date>’1628442000’ and start_timestamp_of_date<’1631120399’ 1742万 27.35%
station_id=’1809’ 8万 0.16%

回表操作的开销

由于status字段不在索引idx_date_station_driver字段上,所以需要回表查询索引过滤的数据,在Mysql服务层判数据是否符合查询条件。

Mysql的优化器在执行sql语句时会先估算走匹配度高的索引的开销,如果走索引的开销比查全表还大,那么Mysql会选择全表扫描。这个结论可能反常识,在我们印象中索引就是用来提高查询效率的。这里主要涉及两个因素:

  1. 当查询条件或查找的字段不在二级索引的字段上时,会执行回表操作,会走:二级索引+主键索引。
  2. 磁盘随机I/O的性能低于顺序I/O。回表查询在主键索引上是随机I/O,全表扫描在主键索引上是顺序I/O。

做实验分析回表操作的开销是否是索引失效的直接原因?

去除status=’0’查询条件,explain查看该查询是否使用到了索引idx_date_station_driver。结果如下图所示,少了回表操作的开销,索引并未失效。

image-20211027011549434.png

总结

结合以上分析总结索引失效原因是:范围查询使联合索引停止匹配,索引匹配过滤的数据不够多,导致Mysql优化器估算出Table Filter的回表操作开销大于全表查询,所以选择了全表查询。范围查询使联合索引停止匹配是索引失效的罪魁祸首,而回表操作的开销是索引失效的直接原因。

优化索引

该慢查询索引失效的罪魁祸首是范围查询使联合索引停止匹配,只需要把范围查询的字段调整到精确查询的字段后面,即将

联合索引 idx_date_station_driver(start_date, station_id, driver_id) 修改为 idx_station_date_driver(station_id, start_date, driver_id) 。优化后的结果如下图所示。

image-20211007003413075.png

拓展

索引失效常见场景

  1. 违反最左前缀匹配原则。例如有索引index(a,b),但查询条件只有b字段。
  2. 在索引列上做任何操作,包括计算、函数、类型转换等。
  3. 范围查询使联合索引停止匹配。
  4. 减少select*的使用。避免不必要的回表操作开销,尽量使用覆盖索引。
  5. 使用不等于(!=、<>),使用or操作。
  6. 字符串不加单引号索引失效。
  7. like以通配符开头’%abc’。注意like ‘abc%’ 是可以走索引的。
  8. order by 违反最左匹配原则,含非索引字段排序,会产生文件排序。
  9. group by 违反最左匹配原则,含非索引字段分组,会导致产生临时表。

Explain分析

慢查询的分析离不开mysql的explain语句,explain主要关注两个字段Type和Extra。

Type表示访问数据的方式,Extra表示过滤和整理数据的方式。这里列举出来方便查找。

Type Extra
ALL 全表扫描 Using index 使用覆盖索引,不需要回表,不需要Mysql服务层过滤
index 索引树全扫描 Using where 从存储引擎层获取数据,在Mysql服务层用where查询条件过滤数据。
range 索引树范围扫描 Using where; Using index 索引范围扫描。索引扫描和全表扫描类似,只是发生的层面不一样。
ref 非唯一性索引扫描,比如非唯一索引和唯一索引的非唯一前缀 Using index condition 使用索引下推,在存储引擎层充分利用查询索引字段过滤数据
eq_ref 唯一性索引扫描,比如唯一索引、主键索引 Using temporary 临时表存储结果,用于排序和分组查询
const 将查询转化成常量 Using filesort 文件排序,用于排序
NULL 不用访问表或索引 NULL 回表

参考资料

  1. SQL中的where条件,在数据库中提取与应用浅析
  2. 一张图搞懂MySQL的索引失效

本文转载自: 掘金

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

【评论抽奖】还在用redis来实现分布式锁吗?这些坑不得不防

发表于 2021-10-30

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

前言

在工作中,我们或多或少都用到过锁,比如lock、synchronized等。今天我们要讨论的是,在分布式场景下,可以通过哪种方式来解决并发场景下的线程安全问题,其实在面试过程中,也经常会遇到这类问题:分布式场景中,如何保证线程安全。

为什么需要锁

首先我们要搞懂,为什么需要锁?这是因为在同一时刻可能会有两个或两个以上的线程执行同一段代码,最经典的场景就是秒杀系统扣减库存,如果不对其加以限制,必定会出现超卖问题。

所以就算对并发编程没有系统学习过小伙伴,也会直接掏出万能方法-synchronized来,但是这种方式(JVM锁)只能解决单台服务器下的线程安全,如果是分布式场景下,这种方式肯定是无法满足的,这时候就需要用到分布式锁。

如何实现分布式锁

实现分布式锁的方式有很多,我们常见的有数据库、redis、zookeeper,但无论哪种方式,其核心思想是共同的,就是同一时刻只能有一个线程能够获取到锁。

比如现在有一个【下单系统】,分别在三台服务器上都部署一个实例,在同一时刻,每台服务器都想操作同一个订单的状态,但是这个时候只能有一台服务器能够操作成功,这时候就需要分布式锁的帮助了。

Redis分布式锁

redis用来做分布式锁是最常见的一种方式,之所以redis能够实现分布式锁,首先是因为它是单线程的,使用一个线程来处理所有的网络请求,因此也就不需要担心并发安全问题。

image.png

如上图,系统A在三台服务器分别部署一台实例,如果他们在同一时候都想修改某一个订单信息,那redis是通过哪种方式来实现分布式锁呢。

熟悉redis的小伙伴都知道,redis有一个命令【SET key 随机值 NX PX 1000】:

  • NX:当key不存在的时,会设置成功。
  • PX 1000:过期时间1000毫秒,当超过该时间,会自动释放。

通过这个命令,当第一个线程设置key时,redis服务返回OK,表示获取锁成功,在超时时间内,如果有其他服务器线程通过该命令,也来尝试获取锁时,redis服务会直接返回nil,表示当前锁被其他线程占用,获取失败。

执行结果7.gif

面临的问题及解决

上述方式虽然可以满足分布式锁的需求,但是有几点问题需要我们注意:

第一点就是在设置value值时,必须使用随机值。

这是因为线程一拿到锁,在处理完自身业务后,会将该锁进行释放(主动删除redis中的key),但是有可能该线程阻塞了很长时间才处理完成,此时redis锁已经超时自动释放,并且被其他线程获取到,此时线程一直接删除key,必然会导致问题出现。

因此建议设置value为随机值,这样在删除key时通过lua脚本实现,删除前判断要删除的key的value值是否与当前线程的value值相同,只有在相同情况下才进行删除操作。

第二点就是redis单点故障。

因为如果是普通的redis单实例,那就是单点故障。或者是redis普通主从,那redis主从异步复制,如果主节点挂了,key还没同步到从节点,此时从节点切换为主节点,别人就会拿到锁。

RedLock原理介绍

该方案是redis官方支持的分布式锁算法,也是对上述方式的优化,这里花哥简单介绍一下原理。

image.png

如上图,假设现在有5个redis节点,节点之间相互独立,彼此之间不进行同步,某个线程如果想要成功获取到锁,需要完成以下几个步骤:

  1. 获取当前时间戳,单位是毫秒;
  2. 使用相同的key和随机value,轮流尝试在每个master节点上创建锁;
  3. 当且仅当从大多数(N/2+1,这里是3个节点)的Redis节点都取到锁,并且获取锁使用的时间小于锁失效时间时,锁才算获取成功。;
  4. 客户端计算建立好锁的时间,如果建立锁的时间小于超时时间,就算建立成功了;
  5. 如果由于某些原因未能获得锁,比如无法在至少N/2+1个Redis实例获取锁或获取锁的时间超过了有效时间,客户端应该在所有的Redis实例上进行解锁。

RedLock存在的问题

  1. 如果线程1从3个实例获取到了锁,但是这3个实例中的某个实例的系统时间走的稍微快一点,则它持有的锁会提前过期被释放,当他释放后,此时又有3个实例是空闲的,则线程2也可以获取到锁,则可能出现两个线程同时持有锁了。
  2. 如果线程1从3个实例获取到了锁,但是万一其中有1台重启了,则此时又有3个实例是空闲的,则线程2也可以获取到锁,此时又出现两个线程同时持有锁了。

总结

今天和大家分享了分布式锁中redis的实现方式,介绍了它的实现原理和需要注意点,不过说实话,redis用来做分布式锁个人认为并不是很完美,一般我也不这么用。至于为什么,除了redis自身的不足外,还要和其他实现方式进行对比取舍,明天花哥继续带大家认识如何使用zookeeper完成分布式锁的实现。

那么,大家平时都是怎么实现的呢,不妨在评论中分享,共同学习更完善的分布式锁的实现吧。

欢迎在评论区讨论,掘金官方将在掘力星计划活动结束后,在评论区抽送100份掘金周边。

本文转载自: 掘金

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

浅谈设计模式 - 建造者模式(十七) 前言 定义 优缺点 应

发表于 2021-10-30

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动

前言

​ 这个设计模式在lombok其实已经被封装为一个@Builder的注解,所以这个轮子基本不需要自己的造,直接拿来用即可,但是我们还是需要了解这个模式底层是如何实现的,建造者设计模式在个人看来更多是编写出更加“优雅”的代码,特别是参数很多的时候使用建造者模式的链式调用会让代码干净很多。

这里推荐idea的插件GenrateAllGetSet,一件生成一个对象的所有set方法也比较好用,特别是不想编写套版化的建造者对象的时候。

定义

​ 建造者模式将复杂的构建过程和对象的具体展示进行切分,客户端只需要了解建造者所需的参数,不需要了解建造的细节,并且根据构建的操作可以自定义不同的对象。这个模式主要解决的问题是构建过程复杂的问题,并且重点关注对象的构建过程的“配置化”。

优缺点

优点:

  1. 构建的产品必须有共同点,不能对于完全不同的产品使用同一个建造器。
  2. 如内部变化复杂,会有很多的建造类,并且很容易出现嵌套。

缺点:

  1. 如果产品的内部变化复杂,可能会导致需要定义很多具体建造者类来实现这种变化,导致系统变得很庞大。
  2. 建造者模式所创建的产品一般具有较多的共同点,其组成部分相似;如果产品之间的差异性很大,则不适合使用建造者模式,因此其使用范围受到一定的限制。

应用场景

  1. 针对对象的大量set方法,有可能会误传参数的,可以使用建造者的 链式调用构建参数构建对象。
  2. 根据不同的组合参数实现不同的效果套件。
  3. **使用工厂模式+建造者模式不仅可以构建出不同的产品,并且可以定制产品的构建细节。**但是注意不要和工厂混淆了,工厂负责的是解耦生产和使用的过程,而建造者更加关心构建的细节。

结构图:

​ 这个模式比较特殊,个人对于模板代码印象比较深,但是对于这个结构图个人认为没有必要了解,所以这里直接用网络上的截图进行简单介绍:

Effective java应用第二条

​ 《effective java》这本书中第二条提到了使用构建器来弥补多个构造器参数的缺点,案例如下,通常我们构造器如果参数过多的情况下,如果编写构建器一般会有如下的方法:

1
2
3
4
5
6
7
8
9
java复制代码 public User(String userId, String orgId, String orgName, String merchNo, String userName, String userRealName, String userPwd) {
this.userId = userId;
this.orgId = orgId;
this.orgName = orgName;
this.merchNo = merchNo;
this.userName = userName;
this.userRealName = userRealName;
this.userPwd = userPwd;
}

​ 这样的写法既然容易传错参数,并且如果加入boolean的参数判断更是地狱,比如我们构建对象的时候会是这种情况,下面的构造器构建对象的参数顺序基本没有人记得住,基本都是肉眼一一核对才不容易出错:

1
java复制代码User user = new User("xx","xx","xx","xx","xx","xx","xx");

​ 正常情况下我们会选择使用set方法替代,比如像下面这样,但是这样的处理方式会直接导致对象的不可变特性被破坏,这里顺带介绍一下前文提到的生成set方法的使用,举例来说,在mac的电脑上,把光标放到对象上面然后按下option+Enter即可(前提保证需要的参数都有set方法,因为插件底层基于对象的方法的反射生成)。

​ 生成完成之后,就可以发现如下的效果,但是很明显这种方式破坏了对象的封装性:

​ 为了解决上面的问题,我们使用建造者模式的来对于这样的代码进行重构,这也是建造者模式的一个模板代码,这里通过私有构造器,使用建造者对于具体的实例进行一层”保护“,这样直接对于对象的生成过程封闭并且保证对象的线程安全:

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
java复制代码public class UserBuilder {

/**
* 用户id
*/
private final String userId;

/**
* 机构id
*/
private final String orgId;

/**
* 机构名称
*/
private final String orgName;

public static class Builder {
/**
* 用户id
*/
private String userId;

/**
* 机构id
*/
private String orgId;

/**
* 机构名称
*/
private String orgName;

public Builder(String userId, String orgId, String orgName) {
this.userId = userId;
this.orgId = orgId;
this.orgName = orgName;
}

public Builder userId(String userId) {
this.userId = userId;
return this;
}

public Builder orgId(String orgId) {
this.orgId = orgId;
return this;
}

public Builder orgName(String orgName) {
this.orgName = orgName;
return this;
}

public UserBuilder build(){
return new UserBuilder(this);
}
}

private UserBuilder(Builder builder) {
this.userId = builder.userId;
this.orgId = builder.orgId;
this.orgName = builder.orgName;
}
}

实际案例

​ 这个案例也是对于《effective java》这本书关于构建器的一次简化,这个案例的表现了对于构造器的 进阶用法,对于构造器也可以进行动态扩展,这里直接给出代码了,在书中的案例会更为复杂一点,这里进行了简化。

​ 这个构建器的大致目的是实现动态的性格构建,在顶层的抽象对象和建造者中定义了抽象的方法供子类实现,子类可以实现具体的产品实现的同时可以实现对于建造者的建造器的细节处理,这里同时使用了范型。

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
java复制代码public abstract class AbstractUser {

private final NATURE nature;

protected enum NATURE {LIVELY, MELANCHOLY, LONELY, NORMAL}

abstract static class Builder<T extends Builder<T>> {

private NATURE nature = LIVELY;

/**
* 构建方法
*
* @return
*/
protected abstract T build();

/**
* 需要由子类实现
*
* @return
*/
protected abstract AbstractUser process();


}

public AbstractUser(Builder builder) {
this.nature = builder.nature;

}
}

​ 具体的实现产品以及子类化的构建器,可以看到通过这种方式,就可以完全发挥建造者模式的作用了,不仅保证了动态扩展,同时可以保证细节处理和公用的处理进行解耦,但是使用这种设计模式需要一定的编程经验才写得出,同时要对与设计模式 熟练掌握才建议用下面的写法,否则更加建议使用别的设计模式改写,因为这种写法明显对于阅读性有一定的影响。

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
java复制代码public class ConcreteUser extends AbstractUser{

private final NATURE nature;

private ConcreteUser(ConcreteBuilder concreteBuilder) {
super(concreteBuilder);
this.nature = concreteBuilder.nature;
}

public static class ConcreteBuilder extends AbstractUser.Builder{

private final NATURE nature;

public ConcreteBuilder(NATURE na) {
this.nature = na;
}

@Override
protected ConcreteUser process() {
System.err.println("处理逻辑一");
return new ConcreteUser(this);
}

@Override
public Builder build() {
return this;
}
}
}

总结

​ 总之,建造者模式还是老老实实用注解比较合适,多数情况用注解的方式也能搞定,对于建造者的深层扩展需要深厚的编程经验和技巧支持,这是我们需要进步和学习的点,但是写出这样的代码无疑需要大量的代码编写练习。

写在最后

​ 建造者模式还是十分好理解的一个设计模式,知道模板代码之后就可以快速回忆的一个设计模式。

本文转载自: 掘金

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

Quartz自定义配置详解(一)——实现自定义配置

发表于 2021-10-30

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动

学了那么久的quartz框架,都是在基于quartz默认配置的基础上操作的,这次就来学习以下如何自定义配置quartz的相关属性。

  1. 默认配置

首先看一下Quartz框架在不进行任何配置,即使用quartz默认配置项时,执行项目。

1.1 控制台输出

image-20211030224242103

1.2 内容含义

1
2
3
4
5
6
7
8
9
java复制代码//启动
Scheduler class: 'org.quartz.core.QuartzScheduler' - running locally.
 NOT STARTED. //还没有开始
 Currently in standby mode. //待机状态
 Number of jobs executed: 0 //执行任务数
 //默认使用SimpleThreadPool线程池 ,线程数量是10
 Using thread pool 'org.quartz.simpl.SimpleThreadPool' - with 10 threads.
 //默认使用RAMJobStore类型,在内存中,不进行持久化,没有开启集群
 Using job-store 'org.quartz.simpl.RAMJobStore' - which does not support persistence. and is not clustered.

2.自定义配置

通过默认配置下的quartz信息输出可以看到,quartz默认不支持数据的持久化,且无集群支持;但是在实际项目使用中,为了保证相关数据的准确和定时任务执行稳定,通常会将定时任务的信息持久化到数据库中。

2.1 初始化数据表

在quartz官网下载quartz ,然后解压文件,找到其中的sql文件。quartz2.4.0版本的sql路径为:\quartz-2.4.0-SNAPSHOT\src\org\quartz\impl\jdbcjobstore

找到如下mysql的语句:

image-20211030224635071

在对应的数据库中执行sql语句,生成数据表结构如下:

image-20211030224708837

对应的表含义为:

  • qrtz_blob_triggers:以blob格式存放自定义trigger信息
  • qrtz_calendars:记录quartz任务中的日历信息
  • qrtz_cron_triggers:记录cronTrigger,即cron表达式相关触发器的信息
  • qrtz_fired_triggers:存储正在触发的定时器的信息,执行完后数据清空
  • qrtz_job_details:记录每个定时任务详细信息的表
  • qrtz_locks:分布式处理时多个节点定时任务的锁信息
  • qrtz_paused_triggers_grps:存储暂停的任务触发器信息
  • qrtz_scheduler_state:记录调度器状态的表
  • qrtz_simple_triggers:记录SimpleTrigger,即普通的触发器信息
  • qrtz_simprop_triggers:存储CalendarIntervalTrigger和DailyTimeIntervalTrigger触发器信息
  • qrtz_triggers:记录每个触发器详细信息的表

2.2 自定义配置内容

对于quartz框架的配置信息,可以将内容配置在quartz.properties文件中,并通过Properties读取配置内容。

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
properties复制代码# 调度器实例名称(不配置则使用默认配置:quartzScheduler)
org.quartz.scheduler.instanceName = Scheduler
# 调度器实例编号自动生成
org.quartz.scheduler.instanceId = AUTO

#持久化方式配置   =org.quartz.simpl.RAMJobStore 即存储在内存中
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
#持久化方式配置数据驱动,MySQL数据库
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate
#开启分布式部署
org.quartz.jobStore.isClustered = true
#分布式节点有效性检查时间间隔,单位:毫秒
org.quartz.jobStore.clusterCheckinInterval = 10000
# quartz相关数据表前缀名(默认QRTZ_)
org.quartz.jobStore.tablePrefix = quartz_
# JobDataMaps内容是否以key-value形式存储,默认true
org.quartz.jobStore.useProperties = false

#线程池实现类(不配置则使用默认配置)
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
#执行最大并发线程数量
org.quartz.threadPool.threadCount = 20
#线程优先级
org.quartz.threadPool.threadPriority = 5
#配置是否启动自动加载数据库内的定时任务,默认true
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread = true

2.3 初始化配置并注入

使用quartz.properties文件配置后,还需要将配置的信息应用到定时任务调度器对象中,这就需要在Spring配置类中对quartz的属性进行初始化,并通过容器管理注入到调度器中。

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复制代码@Configuration
public class QuartzConfig {
   
  ...
       
   @Bean
   public Properties quartzProperties() throws IOException {
       PropertiesFactoryBean factoryBean = new PropertiesFactoryBean();
       factoryBean.setLocation(new ClassPathResource("/quartz.properties"));
       // 在quartz.properties中的属性被读取并注入后再初始化对象
       factoryBean.afterPropertiesSet();
       return factoryBean.getObject();
  }
   
   //创建调度器工厂对象
   @Bean
   public SchedulerFactoryBean schedulerFactoryBean() throws IOException {
       SchedulerFactoryBean factoryBean = new SchedulerFactoryBean();
       factoryBean.setSchedulerName("Scheduler");
       factoryBean.setDataSource(dataSource);
       //根据配置文件生成内容来配置调度器
       factoryBean.setQuartzProperties(quartzProperties());
       // 设置触发器
       factoryBean.setTriggers(simpleTriggerFactoryBean().getObject());
       return factoryBean;
  }
}

2.3 自定义配置启动

数据源开启持久、集群配置后,项目运行输出内容:

image-20211030224744598

如果在springboot的application.yml/properties 文件中配置quartz属性,那么quartz.properties内容不生效。

  1. 总结

根据官方提供的sql语句创建定时任务表,并配置quartz的持久化方式,最后在项目执行时就会使用配置的方式来持存储定时任务数据。

数据支持持久化后,数据表中会记录需要执行的任务,这样就算系统出现了故障宕机,在恢复之后耽搁的定时任务也会恢复继续执行。

如果使用了集群配置,多个定时任务服务操作同一个数据库,quartz同样提供了锁等一系列方式来保证分布式数据的准确性。

本文转载自: 掘金

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

Rust 知识点梳理 - 变量,所有权和引用

发表于 2021-10-30

个人学习和使用 Rust 语言已经有一段时间了,在此期间深感 Rust 语言的入门和深入掌握之不易。因此希望在这里将自己的一些个人经验以尽可能简洁清晰的方式呈现出来,作为大家在学习与回顾时的参考。

本文将会是一个系列的一部分,随缘更新。如果喜欢,欢迎点赞和留下宝贵意见,这将对我起到很大的激励作用。

变量与所有权

  • 与其它语言有所不同,在 Rust 中,「变量」指的是一种 绑定关系(binding):比如 let x = 42; 代表将「42」这个对象 绑定 给变量 x。此时 x 并不是真的占有 42 这个对象的存储空间,而只是指向这个对象的一个符号。
  • 由于变量只是一种「绑定」,而不是真的占有了对象的存储空间,因此这种「绑定关系」是可以 随意更换 的。比如 let x = 42; let x = "Hello"; 这两个语句代表着:首先将 x 绑定到 42,然后立刻解绑,改成绑定 "Hello" 这个字符串。从「绑定」的角度来理解的话,我们并不是将变量 x 重新定义了一遍,而仅仅是将它的绑定关系更换了。这与其它语言中的「定义变量」是不同的概念。
  • 在同一时间,一个对象只能绑定到一个变量上。此时,我们称该变量是该对象的「所有者」(owner)。在任意时刻,一个对象的所有者都是唯一的。
  • 对象的所有权是可以发生 转移 的,这种情况只发生在不支持 Copy 的类型上。比如 let a = String::from("Hello"); let b = a; 会把字符串 "Hello" 的所有权从 a 转移到 b。当所有权被转移之后,原变量会自动失效,无法再被使用,除非重新赋值(绑定)。
  • 对于支持 Copy 的类型,在发生传值操作时,原对象会被 复制给新变量,而不是发生所有权转移。比如在 let a = 42; let b = a; 之后,a 和 b 会指向两个(拥有相同值的)不同的对象,并且 b 指向的对象是由 a 指向的对象复制而来的。关于 Copy 的详细信息将会在之后的章节介绍。

引用和借用

  • 所有权系统已经能够保证内存安全,但为了更高效地编程,只有所有权的概念还是不够的。比如,在给函数传递参数时,有些函数可能并不需要参数对象的所有权,如果我们把所有权传递给这些函数就太麻烦了。这时,我们可以只把对象的「使用权」「租借」(lend)给这些函数,这就是 引用(references)和 借用(borrowing)的概念。
  • Rust 中的引用分为 共享引用(shared references)和 可变引用(mutable references)两种。(也有其它命名方式,但以上两个是最官方的命名。)其中,共享引用的特点是:只读,允许有多个租借者,并且所有其它租借者也只能持有共享引用;而可变引用的特点是:可读可写,只允许有一个租借者(即 独占)。根据以上的定义,共享引用和可变引用是 不能同时存在的,在同一时间,只能有其中一种存在。
  • 另外,Rust 通过 生命周期约束 来保证 所有的引用一定是有效的(即,永远不会指向无效的位置)。这就彻底避免了 C 语言的「悬吊指针」问题。(生命周期问题在之后的章节会解释。)
  • 高阶知识点:一个引用 &'a T 实际上有 两个类型参数,一个是被引用的类型 T,一个是该引用的生命周期 'a。'a 决定了该引用的 有效范围。超出这个有效范围之后,该引用就会被回收,无法再被使用。(有关生命周期为什么是类型参数的问题,可以参考之后章节的 协变与逆变 的内容,这属于 unsafe Rust 的范畴。)
  • 为了保证内存安全,在有对象引用存活时,被引用的对象本身将被 冻结(freeze),其使用会受到限制。具体来说,当共享引用存在时,原对象将变成 只读的;而 当可变引用存在时,原对象将被 彻底屏蔽,无法使用,只能通过可变引用来对该对象进行操作。冻结操作是暂时的,当所有的引用都被释放之后,原对象会被自动解冻。(注:「冻结」不是 Rust 官方的概念,是他人为了方便理解而创造的。)
  • 如果想要以可视化的方式理解借用和生命周期,可以参考这篇文章:Graphical depiction of ownership and borrowing in Rust - Rufflewind’s Scratchpad。另外,RustViz 这个程序能够可视化任何一个能够编译的 Rust 程序。
  • 一些个人观点:初学者通常会很希望绕过 Rust 的所有权、借用和生命周期机制,大量使用 unsafe 进行编程,以获得像 C++ 一样不受限制的编程体验。这其实是不正确的,因为这就相当于放弃了 Rust 最重要的一个特性(没有之一):内存安全。如果实在认为借用机制难以对付,可以给自己出一些简单的编程题目(如:从零开始编写一个 TCP 服务器),然后多在 Google,GitHub 等网站上寻找与自己的需求类似的程序,观察他人是如何实现类似的需求的。在入门时,向他人学习也不失为一种良好的成长方式。

移动,复制和克隆

  • 在 Rust 中,在不同变量之间传递值的方式共有 3 种,分别是 移动(move),复制(copy)和克隆(clone)。其中,移动和复制是在发生传值操作时,由编译器自动触发的;克隆是需要通过 clone 方法手动触发的。
  • 移动和复制发生在需要传值的情景中。比如 let b = a; 表示将变量 a 的值传递给变量 b;调用一个函数 fn foo(s: String) 时,相当于将一个 String 值传递给该函数的参数。
  • 当 Rust 编译器发现一个操作需要传值时,只要被传递的类型允许复制(即实现了 Copy trait),编译器就会优先进行复制;否则,编译器会进行移动。(简记:能 Copy 则 Copy,不能 Copy 则 Move。)
  • 移动操作指的是把对应的值从旧变量「抢走」。移动操作发生后,对象的所有权被转移给新变量,旧变量无法再被使用,否则会发生编译错误。(移动操作的好处在于尽可能避免发生数据复制操作,比如在需要传递 String 时,可以避免复制字符串的内容。)
  • 只有实现了 Copy trait 的类型才支持复制。Copy trait 的含义是:对应的类型能够通过 直接复制对象底层数据 的方式进行复制。这保证了 Copy 是一个低成本的操作。(因此诸如 String 和 Rc 等类型都不是 Copy,因为 String 类型作为容器,其内部数据是无法直接复制的;而 Rc 在复制时需要将引用计数加 1,这些都属于「附加操作」,是不被 Copy 所允许的。)
  • 与 Copy 不同,Clone 除了复制对象数据之外,还能够执行一些额外的步骤,比如 复制容器内部的数据,或者进行一些必要的修改,以保证程序正确性等。(比如 String 的 clone 方法会将内部的字符串数据也复制一份;而 Rc 的 clone 方法会修改引用计数。)因此,Clone 操作通常不是零成本的,并且在部分类型上是一个比较耗时的操作。为了实现 Rust 的「零成本抽象」承诺,Clone 操作只能通过 clone 方法触发。在程序中,除了确实必要的情况之外,应该尽量少使用 clone。

本文转载自: 掘金

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

Spring Boot、Spring MVC 和 Sprin

发表于 2021-10-30

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

按照一贯的作风,我们首先还是先来介绍一下 Spring,Spring Boot,Spring MVC。了解一下他们都是做什么的,解决了什么问题,我们才能清楚的理解他们之间的区别和联系。

先上结论!!!

解释过程在下面。

结论

Spring 是一个框架,Spring MVC 是 Spring 的一个模块,一个 web 框架,Spring Boot则是一个 Spring 的快速开发整合包。

概念

  1. 应用程序:是能完成我们所需要的功能的成品,比如购物网站、OA系统。
  2. 框架:是能完成一定功能的半成品。比如我们可以使用框架进行购物网站开发,框架做一部分功能,我们做一部分功能,这样应用程序就创建出来了。而且框架规定了你在开发应用程序时的整体结构,提供了一些基础功能,还规定了类和对象的如何创建、如何协作等。从而简化我们开发,让我们专注于业务逻辑开发。
  3. 非侵入式设计:从框架角度可以这样理解,无需继承框架提供的类,这种设计就可以看作是非侵入式设计,如果继承了这些框架类,就是侵入设计,如果以后想更换框架,之前写过的代码几乎无法重用,如果是非侵入式设计,则之前写过的代码仍然可以继续使用。
  4. 轻量级和重量级:轻量级是相对于重量级而言的,轻量级一般就是非侵入性、所依赖的东西非常少、资源占用非常少、部署简单等等,其实就是比较容易使用,而重量级正好相反。
  5. POJO:POJO(Plain Old Java Objects)简单的 Java 对象。POJO的内在含义是指那些没有从任何类继承、也没有实现任何接口,更没有被其他框架侵入的 Java 对象。POJO 有无参构造函数,每个字段都有 getter 和 setter 的 Java 类。
  6. 容器:在日常生活中容器就是一种盛放东西的器具,从程序设计角度看就是装对象的对象,因为存在放入、拿出等操作,所以容器还要管理对象的生命周期。
  7. 控制反转:IoC(Inversion of Control),控制反转还有另外一个名字叫做依赖注入(Dependency Injection)。就是以前程序控制对象,现在由框架控制。
  8. 脚手架:建筑上的脚手架就是先搭个架子,然后工人们慢慢往里面砌砖头,直到建筑成型。编程中的脚手架可以理解为有个空架子把环境配置、各种依赖都搭建好,要创建新项目的时候拿来直接往里面添加新项目需要的东西。

Spring、Spring MVC 和 Spring Boot

Spring

什么是 Spring

一句话,Spring 是一个开发应用框架,什么样的框架呢,有这么几个标签:轻量级、非侵入式、一站式、模块化,其目的是用于简化企业级应用程序开发。

目的

  • 为了简化企业级应用程序开发

做了什么

Spring 除了不能帮我们写业务逻辑,其余的几乎都能帮助我们简化开发。

管理对象,以及对象之间的依赖关系

传统程序开发,创建对象及组装对象间依赖关系由我们在程序内部进行控制,这样会加大各个对象间的耦合,如果我们要修改对象间的依赖关系就必须修改源代码,重新编译、部署。而如果我们采用 Spring,则由 Spring 根据配置文件来进行创建及组装对象间依赖关系,只需要修改配置文件即可,无需重新编译,所以,Spring 能帮我们根据配置文件创建及组装对象之间的依赖关系。

面向切面编程

当我们要进行一些日志记录、权限控制、性能统计等时,在传统应用程序当中我们可能在需要的对象或方法中进行,比如权限控制、性能统计,大部分代码都是重复的,这样代码中就存在大量重复代码,即时有人说我把通用部分提取出来,那必然存在调用,还是存在重复复,像性能统计我们可能只是在必要时才进行,在诊断完毕后要删除这些代码。等等。

如果采用 Spring,这些日志记录、权限控制、性能统计能从业务逻辑中分离出来,通过 Spring 支持的面向切面编程,在需要这些功能的地方动态添加这些功能,无需渗透到各个需要的方法或对象中。

即使可以通过“代理设计模式”或“包装器设计模式”,但还是需要通过编程方式来创建代理对象,还是要耦合这些代理对象,而采用 Spring 面向切面编程能提供一种更好的方式来完成上述功能,一般通过配置方式,而且不需要在现有代码中添加任何额外代码,现有代码专注于业务逻辑。所以,Spring 面向切面编程能帮助我们无耦合的实现日志记录、性能统计、安全控制。

数据库事务管理

在传统应用程序当中,我们如何来完成数据库事务管理呢?我们需要一系列“获取连接、执行SQL、提交或者回滚事务、关闭连接”操作,而且还要保证在最后一定要关闭连接,多么可怕的事情,而且也很无聊。如果采用 Spring,我们只需要获取连接,执行 SQL,其他的都交给 Spring 来管理就可以了。

第三方数据访问框架集成

Spring 提供了与第三方数据访问框架(如 Hibernate 、JPA)无缝集成,而且自己也提供了一套 JDBC 访问模板,来方便数据库访问。

第三方 Web 框架集成

Spring 提供了与第三方 Web(如Struts、JSF)框架无缝集成,而且自己也提供了一套 Spring MVC 框架,来方便 web 层搭建

优点

  • 方便解耦,简化开发。使用 Spring 的 IoC 容器,将对象之间的依赖关系交给 Spring,让我们更专注于应用逻辑。
  • 对主流的光甲提供了很好的集成支持,如 Hibernate、Struts2、JPA 等。
  • Spring 提供面向切面编程,可以方便的实现对程序进行权限拦截、运行监控等功能。
  • Spring 的高度可开放性,并不强制依赖于 Spring,开发者可以自由选择 Spring 部分或全部

Spring MVC

什么是 MVC

MVC 是一种架构模式,MVC 是三个字母的首字母缩写,它们是 Model(模式)、View(视图)和 Controller(控制)。该模式可以把不论简单或复杂的程序,都从结构上划分为三层。

  • 最上面的一层,是直接面向最终用户的“视图层”(View)。它是提供给用户的操作界面,是程序的外壳。
  • 最底下的一层,是核心的”数据层“(MOdel),也就是程序需要操作的数据或者信息。
  • 中间的一层,就是”控制层“(Controller),它负责根据用户从”视图层“输入的指令,选取”数据层“中的数据,然后对其进行相应的操作,产生最终结果。

每一部分都相对独立,职责单一,在实现的过程中可以专注于自身的核心逻辑。MVC 是对系统复杂性的一种合理的梳理与划分,它的思想实质就是”关注点分离“。

什么是 Spring MVC

首先是一个 MVC 框架。

其次是一个 Spring 子框架。

Spring 的 WEB MVC 框架是围绕 DispatchServlet 设计的,它把请求分配给处理程序,同时带有可配置的 处理程序映射、视图解析、本地语言、主题解析 以及 上载文件支持。应用控制器其实拆为 处理器映射器(Handler Mapping) 进行处理器处理和 视图解析器(View Resolver) 进行视图管理,页面控制器 是非常简单的 Controller 接口,只有一个方法 ModelAndView handleRequest(request, response)。Spring 提供了一个 控制器层次结构,可以派生子类。如果应用程序需要处理用户输入表单,那么可以继承 AbstractFormController,如果需要把多页输入处理到一个表单,那么可以集成 AbstractWizardFormController。

基本流程

当 web 程序启动的时候, ContextLoaderServlet 会把对应的配置文件信息读取出来,通过注入去初始化控制器 DispatchServlet。

当接收到一个 HTTP 请求的时候,ContextLoaderServlet 会让 HandlerMapping 去处理这个请求,HandlerMapping 根据请求 URL(不一定非要是 URL,完全可以自定义,非常灵活)来选择一个 Controller,然后 DispatchServlet 会在调用选定的 Controller 的 HandlerRequest 方法,并且在这个方法前后调用这个 Controller 的 1interceptor(假如有配置的话),然后返回一个视图和模型的集合 ModelAndView。框架通过 ViewResolver 来解析视图并且返回一个 View 对象,最后调用 View 的 render 方法返回到客户端

特性(优势)

  1. 让我们能非常简单的设计出干净的 Web 层和薄薄的 Web 层。
  2. 进行更简洁的 Web 层的开发。
  3. 天生与 Spring 框架集成(如 IoC 容器、AOP 等)。
  4. 提供了强大的约定大于配置的契约式编程支持。
  5. 能简单的进行 Web 层的单元测试。
  6. 支持灵活的 URL 到页面控制器的映射。
  7. 非常容易与其他视图技术集成,如 Velocity、FreeMarker等等,因为模型数据不放在特定的 API 里,而是放在一个 Model 里(Map 数据结构实现,因此很容易被其他框架使用)。
  8. 非常灵活的数据验证,格式化和数据绑定机制,能使用任何对象进行数据绑定,不必实现特定框架的 API。
  9. 提供一套强大的 JSP 标签库,简化 JSP 开发。
  10. 更加简单的异常处理。
  11. 对静态资源的支持。
  12. 支持 Restful 风格。

Spring Boot

什么是 Spring Boot

Boot 是启动的意思。

Spring Boot 并不是一个全新的框架,它不是 Spring 解决方案的一个替代品,而是 Spring 的一个封装。

什么是 starter

Starter 可以总结为一种对依赖的合成。

在没有 starter之前,加入我想要在 Spring 中使用 jpa,那我可能需要做以下操作:

  1. 在 Maven 中引入使用的数据库的依赖(即 JDBC 的 jar)
  2. 引入 jpa 的依赖
  3. 在 xxx.xml 中配置一些属性信息
  4. 反复的调试直到可以正常运行

需要注意的是,上述操作在我们每次新建一个需要用到 jpa 的项目的时候都需要重复的做一次。

但是这样操作就会带来很多问题:

  1. 如果过程比较繁琐,这样一步步操作会增加出错的可能性
  2. 不停地 copy&paste,不符合 Don't repert yourself 精神
  3. 在第一次配置的时候(尤其如果开发者比较小白),需要花费掉大量的时间

starter 主要目的就是为了解决上述问题。

starter 的理念是 starter 会把所有用到的依赖都给包含进来,避免了开发者自己去引入依赖所带来的麻烦。

为什么用 boot?

免费开源。中小型企业,没有成本研究自己的框架,boot 功能全也能快速搭建项目,降低开发成本。

其他语言的简易化,给 java 造成的冲击,再不改善,就会被抛弃了,催生了这款脚手架的诞生。

优点

  1. 使业务开发变得简单。Spring Boot 采用了 java config 的方式,对 Spring 进行了配置,并且提供了大量的注解,极大的提供了工作的效率。
  2. 使配置变得简单。Spring Boot 提供许多默认配置,当然也提供自定义配置。但是所有的 Spring Boot 的项目都只有一个配置文件:application.properties/application.yml。用了 Spring Boot,再也不用担心配置出错找不到问题所在了。
  3. 使部署变得简单。Spring Boot 内置了三种 servlet 容器:tomcat、jetty、undertow。所以你只需要一个 java 的运行环境就可以跑 Spring Boot 的项目了。Spring Boot 的项目可以打成一个 jar 包,然后通过运行 java -jar xxx.jar 来运行。(Spring boot 项目的入口是一个 main 方法,运行该方法即可。)
  4. 使监控变得简单。Spring Boot 提供了 actuator 包,可以使用它来对你的应用进行监控。
  5. 插拔式搭建项目。boot 是个脚手架,如果需要 web 功能, maven 引入对应的 jar 包;需要集成 mybatis ,引入包,简单几句注解则组装完成。

参考文档

  • spring boot与spring mvc的区别是什么?
  • Spring,Spring MVC及Spring Boot区别
  • Spring是什么
  • Spring框架基础知识
  • 「Java学习」Spring框架简介
  • SpringMVC是什么?
  • MVC架构模式
  • Spring MVC是什么?它的优势有那些?
  • Spring Boot Starters
  • 【springboot 入门篇】第0篇 spring-boot是什么

本文转载自: 掘金

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

Mac M1快速配置开发环境 开头 安装软件部分 最后 20

发表于 2021-10-30

开头

最近刚到手一台macbook pro,起初的想法并没有打算用来作开发机器,不过偶尔还是想写点代码玩,于是花了两天算是填了各种坑,终于把平时常用的环境都配置好了,下面就来分开说说各个软件的安装方法。

首先我的电脑配置是16G+2T
在这里插入图片描述
刚拿到手就有一次系统更新,更新之后再开始安装。

安装软件部分

1.QQ/微信/百度云等

这类软件能在app store找到就直接安装,找不到就去官网安装,大不了就是转译使用,问题不是特别大。

2.JetBrains系列

在这里插入图片描述
目前安装了这几款,并且已经都成功license(违规原因,所以不能写方法)
在这里插入图片描述

3.nvm/node

因为最近在学习vue相关的知识,那前端的一些软件也是必不可少。安装node,我推荐使用brew安装nvm,然后nvm安装node。nvm管理node会比较方便,不过目前只有node v15是适配M1的,所以这个管理问题显得没那么必要。下面来看步骤

1.安装homebrew

查看这篇博客,按照上面的步骤来操作。
一定要配置path,不然会找不到brew

2.用brew安装nvm,node

参考博客
只需要安装nvm,然后用nvm安装node
nvm install v15
在这里插入图片描述
如果终端找不到nvm,那就按上图中的brew info nvm,按照提示将path配置好就行了。

展示结果:
在这里插入图片描述
然后cnpm、vue、vue-cli……等就和以前步骤一样安装就好

4.conda+tf+pytorch+opencv

现在anaconda还没有M1适配,只能用miniforge

miniforge
在这里插入图片描述
选择Miniforge3-MacOSX-arm64,其中arm64版本的miniforge的基础环境是python3.9。为了后续安装tensorflow等支持,所以创建python3.8的虚拟环境,具体操作可以看下图
在这里插入图片描述
第一句改为conda create -n python38 python=3.8
因为我是这样命名的,你们也一样的话后续的命令方便参考,就不用更改太多地方。

然后以下安装操作都在python38环境下进行
conda activate python38
which pip
要确保使用的是python3.8

1.tensorflow

目前推出适配M1的tensorlfow是2.4版本的,首先得去下载alpha3版本的tensorflow
具体操作步骤查看这篇博客
记得下载的是
在这里插入图片描述
所以对于上面博客中相对应tensorflow的文件请注意文件名的问题,不然会出错。

这样操作结束安装好之后,会看到successful。但是运行python导入tensorflow可能会出现killed python

解决办法:
一开始我以为是版本问题,三个版本全部试了一遍还是没用,搜索发现了mac的SIP问题。关闭SIP的方法,关机然后长按开机键(也就是指纹识别的那个键),直到有选项出现。点击继续打开终端,输入csrutil disable,然后y,确认。等待一会命令行结束重启就好。再次运行虚拟环境尝试tensorflow就成功了。

2.pytorch

下载whl
https://ossci-macos-build.s3.amazonaws.com/torch-1.8.0a0-cp38-cp38-macosx_11_0_arm64.whl
安装之前用conda安装numpy等
或者
conda install pytorch torchvision -c pytorch '-c=conda-forge'

3.opencv

opencv安装
按照上面的步骤一步一步来,只要把cmake改成自己对应路径就行。

对于python其他的库可以直接conda安装,换源等操作和之前一样。

展示结果
在这里插入图片描述
在这里插入图片描述

5.Go

go官方已经发布1.16 beta版,现在可以直接从官网下载安装包即可
在这里插入图片描述
下载之后解压,直接安装就行。

6.vscode

去官网,下载对应版本。解压之后应该是一个app,直接用就好。
在这里插入图片描述

7.其他

还有一些mysql等其他的工具没什么特别的地方就没注意,以后遇到了再来更新。

最后

经过这几天的使用还有源码编译,感觉M1用起来还是比较舒服的。不过现在还是有些软件没有适配M1,比如微信……另外视频剪辑,渲染导出我还没有测试,所以这里不评价。至于续航这一块确实很厉害,开着pycharm、idea以及一堆软件的情况下续航依然可观。

我的个人建议就是如果很想体验M1,那可以买一个air体验。如果用来开发,可能会遇到一些奇奇怪怪的问题,而且不一定很好解决得自己摸索。要是现在手上有充足资金,但是不急于换电脑的话,可以稍微等等,等到下半年M1x(M2)发布,貌似还有16寸版本,而且那个时候软件适配更普及。



2021/4/2更新

selenium+浏览器driver的配置

selenium还是pip安装就好
配置文章
查看浏览器版本,下载对应版本的驱动(文章中的链接点击去下载就好),然后放在项目中(直接打包给别人就不用额外配置),代码里注明路径。但是Mac中要更改selenium中的源码,将源码中的.exe去掉。
在这里插入图片描述
pycharm打开源码方式
==command+点击==

本文转载自: 掘金

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

【Go】 worker pool(goroutine池)和

发表于 2021-10-30

Worker pool(goroutine池)

在工作中我们通常会使用可以指定启动的goroutine数量–worker pool模式,控制goroutine的数量,防止goroutine泄漏和暴涨。

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
go复制代码func main() {
jobs := make(chan int, 100)
results := make(chan int, 100)
// 开启3个goroutine
for id:=0; id<3; id++ {
go worker(id, jobs, results)
}

// 生成5个job
for num:=0; num<5; num++ {
jobs <- num
}
close(jobs)

// 输出结果
for a := 0; a < 5; a++ {
<-results
}


// 死锁,只有 close(results) 可用
//for {
// x,ok := <- results
// if !ok {
// break
// }
// fmt.Println(x)
//}
// 死锁,只有 close(results) 可用
//for x := range results {
// fmt.Println(x)
//}

}

func worker(id int, jobs <-chan int, results chan<- int) {
for j := range jobs {
time.Sleep(time.Second)
fmt.Printf("JobID:{%d} Job is:%d \n", id, j)
results <- j * 2
}
}

输出结果如下:

1
2
3
4
5
text复制代码JobID:{0} Job is:0
JobID:{2} Job is:2
JobID:{1} Job is:1
JobID:{2} Job is:4
JobID:{0} Job is:3

Select多路复用

在某些场景下我们需要同时从多个通道接收数据。通道在接收数据时,如果没有数据可以接收将会发生阻塞。你也许会写出如下代码使用遍历的方式来实现:

1
2
3
4
5
6
7
go复制代码for{
   // 尝试从ch1接收值
   data, ok := <-ch1
   // 尝试从ch2接收值
   data, ok := <-ch2
   …
}

这种方式虽然可以实现从多个通道接收值的需求,但是运行性能会差很多。为了应对这种场景,Go内置了select关键字,可以同时响应多个通道的操作。

select的使用类似于switch语句,它有一系列case分支和一个默认的分支。每个case会对应一个通道的通信(接收或发送)过程。select会一直等待,直到某个case的通信操作完成时,就会执行case分支对应的语句。具体格式如下:

1
2
3
4
5
6
7
8
9
10
go复制代码select{
   case <-ch1:
       ...
   case data := <-ch2:
       ...
   case ch3<-data:
       ...
   default:
       默认操作
}

举个小例子来演示下select的使用:

1
2
3
4
5
6
7
8
9
10
11
go复制代码func main() {
ch := make(chan int, 1)
for i := 0; i < 10; i++ {
select {
case x := <-ch:
fmt.Println(x)
case ch <- i:
fmt.Printf("%d放进去了\n",i)
}
}
}

使用select语句能提高代码的可读性。

  • 可处理一个或多个channel的发送/接收操作。
  • 如果多个case同时满足,select会随机选择一个。
  • 对于没有case的select{}会一直等待,可用于阻塞main函数。

本文转载自: 掘金

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

数据库和缓存的一致性问题,看这一篇就够了

发表于 2021-10-30

写在前面

在我们后端平时开发中,经常会讨论这样的问题:该如何保证缓存和数据库一致性呢。

相信有一大部分人,对这个问题是一知半解的,或者是有挺多疑惑:

  • 更新数据时,是要先更新数据库,再删缓存,还是先删缓存,然后再更新数据库呢?
  • 是否要考虑引入消息队列来保证数据的一致性呢?
  • 延迟双删是否可以用,用了又会有啥问题呢?
  • ……

接下来这篇文章会把以上问题讲清楚,先来看下大纲。

读写数据库1.png

为什么要引入缓存?

对于小公司,或者说对于每天请求量没多少的业务来说,引入缓存只会让系统更加复杂,简单来说就是没必要引入缓存,除非你就是想为了用缓存而引入缓存。简单的架构模型如下:

读写数据库.png

但是随着公司业务的增长,项目的请求量也随着上来了,此时如果还是用上面的简单架构支撑的话,那就会有性能问题了。

这个时候就可以引入缓存了,引入缓存可以提高性能,此时升级一版的架构如下:

读写数据库2.png

可以看到缓存用的中间件是 Redis,它不仅性能高,而且有着丰富而又简单的数据结构,没有用到特殊的数据类型都是可以满足滴。

缓存方案以及该怎么使用缓存

那么我们先来看一个相对简单而且比较直接的缓存方案:

  • 将数据库的数据全量刷到缓存,而且是不设置失效时间;
  • 然后写操作时,只更新数据库,缓存不更新;
  • 另外需要有一个定时任务,定时的把数据库的增量数据更新到缓存中。

读写数据库3.png

该方案的优点:

  • 所有请求都打在了缓存,不用查数据库;
  • 直接访问 Redis 性能非常高。

但是缺点也是很明显的:

  • 缓存的利用率比较低,不常用的数据一直保留在缓存中,占用内存;
  • 缓存和数据库数据可能会不一致,这个取决于定时任务刷新缓存的时间频率。

同样,上面这种方案比较适合业务量小,而且对数据一致性要求不是很高的业务场景。

那么如果是针对业务量大,对数据一致性有要求的场景呢?

什么情况下会出现数据一致性问题?

在讨论数据一致性的问题之前,不妨先来看下如何提高缓存利用率的问题。

前面也说到,把不常用的数据放到缓存里,会占用内存,降低缓存利用率,所以想要提高缓存的利用率,比较容易想到的方案就是:Redis 缓存中只保留最近访问比较多的数据,我们将这些数据称为“热数据”。具体实现如下:

  • 写数据依然是写到数据库;
  • 读请求先从缓存里读,如果读取的数据不在缓存,则从数据库读取,并将读取的数据刷到缓存;
  • 另外,刷到缓存中的数据,都需要设置失效时间。

读写数据库4.png

将缓存的数据设置了失效时间,这样缓存中如果不经常访问的数据,就会随着时间被过期淘汰掉,缓存剩下的都是经常被访问到的“热数据”,从而提高了缓存的利用率,其实就是 LRU 淘汰算法,可以看下我之前写过的一篇文章:LRU缓存淘汰算法你了解多少?。

接下来再看数据一致性问题。

如果想要保证缓存和数据库的一致性,那前面说的定时任务刷新缓存的方法就不能用了。

也就是说,更新数据时,不仅仅要操作数据库,同时也要操作缓存。即修改一条数据,不仅要更新数据库,还要一起更新缓存。

有人可能会注意到,同样是更新数据,我是要先更新缓存,再更新数据库,还是先更新数据库,再更新缓存呢?先后顺序无非就两个:

  1. 先更新数据库,后更新缓存;
  2. 先更新缓存,再更新数据库;

那么选哪个方案优先呢?下面就分情况讨论。

我们先抛开并发带来的问题,而且在正常情况下,上面两种方案均可选,不管先更新数据还是先更新缓存,都是可以让两者的数据保持一致的,我们需要关心的就是异常情况。

先更新数据库,再更新缓存

先更新数据库,再更新缓存的情况下:如果数据库更新成功了,但是缓存更新失败,则数据库的数据是新的值,缓存中的数据还是旧数据。

接下来,有读请求进来,读缓存读到的是旧数据(缓存失效前),只直当缓存失效后,才会从数据库中读到最新值,然后重建缓存,此时缓存的数据才是最新的。

如果缓存失效前用户来读的数据,会发现前面修改的数据还没生效,一段时间后,数据才更新过来,这样会对业务有影响。

先更新缓存,在更新数据库

先更新缓存,在更新数据库的情况下:如果缓存更新成功了,但是数据库更新失败,则缓存的数据是新的值,数据库中的数据还是旧数据。

接下来,有读请求进来,虽然可以命中缓存,读到的是新的值,即正确的值,但是缓存一旦失效,就会从数据库中读取旧数据,然后重建缓存,这样缓存的数据也是旧的了。

此时用户又过来读数据,会发现之前修改的数据又变回旧的数据了,同样会对业务有影响。

综上两种方案所述:

无论先更新谁,但凡后者更新发生了异常,都会对业务造成一定的影响。那么该怎么解决这种问题呢?后面会继续分析,并给出相应的解决方案。

并发场景下引发的数据一致性问题

先来看下前提条件:使用“先更新数据库,再更新缓存”的方案,而且这两步更新操作都是成功的。

在以上的前提下,如果存在并发的情况,又会是怎么样的呢?

先来看下面一个场景:

有两个线程,分别为 A 和 B,都需要更新同一条数据(假设更新数据 X),执行顺序如下:

  1. A 更新数据库,X = 1;
  2. B 更新数据库,X = 2;
  3. B 更新缓存,X = 2;
  4. A 更新缓存,X = 1;

根据执行的顺序,最后 X 在缓存中的值为 1,在数据库中的值为 2,可见,缓存和数据库中 X 的值是不一致的,是不符合预期的。

同样,使用“先更新缓存,再更新数据库”的方案,也会有类似的问题,就不再赘述了。

另外,如果每次修改数据,都要更新缓存的话,但是缓存中的数据又不一定会被马上来读取,还是上面提到的,会导致缓存中可能存放了很多不经常访问到的数据,占用了内存,缓存利用率也不高。

而且在一些情况下,写到缓存中的数据并不是从数据库中直接刷过来的,即不是一一对应的,有可能是查了数据库的数据,然后经过一些计算得出来的值,再更新到缓存中的。

由此可见,“更新数据库 + 更新缓存”的方案,不仅缓存利用率不高,还会降低性能,得不偿失。

所以,我们需要考虑使用另外一种方案:删除缓存。

删除缓存能否做到一致性?

同样,删除缓存也会有对应以下的两种方案:

  1. 先删除缓存,再更新数据库;
  2. 先更新数据库,再删除缓存;

由上面的分析可以知道,如果第二步操作失败,都会导致数据不一致的情况。

这里不再赘述。

我们还是重点关注并发带来的问题,以及该如何处理。

先删除缓存,再更新数据库

同样以上面并发举例的场景(稍作修改):

假设 X 原值为 1,有线程 A 和 B。

  1. A 要更新数据(X = 2),先删除缓存;
  2. B 读缓存,发现不存在,从数据库中读取数据(X = 1);
  3. A 将新值(X = 2)写入数据库;
  4. B 将旧数据(X = 1)写入缓存;

根据以上执行顺序可知,最后缓存中 X 的值是 1(旧数据),在数据库中的值是 2(新值),数据不一致。

可见,“先删除缓存,后更新数据库”的方案,当发生「读+写」并发时,还是会存在数据不一致的情况。

先更新数据库,再删除缓存

同样有线程 A 和 B 并发操作:

  1. 缓存中不存在 X 数据,数据库中存在 X = 1;
  2. A 读取数据库,得到 X = 1;
  3. B 更新数据库 X = 2;
  4. B 删除缓存;
  5. A 将 X = 1(旧值) 写入缓存;

最后缓存中 X 的值是 1(旧值),数据库中的值是 2 (新值),数据不一致。

这种情况理论上来说是有可能发生的,但实际上其实发生的概率很低,因为这种情况是需要满足下面三个条件才会发生的:

  1. 首先缓存是已失效,即缓存不存在该条数据;
  2. 读和写请求该条数据一起并发过来;
  3. 更新数据库和删除缓存的时间(上面步骤 3 和 4),要比读数据库和写缓存的时间短(上面步骤 2 和 5);

根据多年的开发经验,条件 3 发生的概率其实是比较低的。

因为写数据库操作一般会先加锁,所以写数据库操作通常是要比读数据库操作的时间更长些。

综上所述,“先更新数据库,再删除缓存”的方案,在一定程度上是可以保证数据一致性的。

所以,在平时开发中,我们应该采用此种方案,来操作数据库和缓存。

到这里,并发带来的问题也就解决了,接下来我们继续看前面提到的问题:
第二步执行「失败」或异常,导致数据不一致的问题。

如何确保更新数据库和删除缓存这两步操作都执行成功?

前面已经分析到:无论是更新缓存还是删除缓存,但凡第二步发生失败,就会导致数据库和缓存不一致的问题。

那么该如何解决此问题呢?

方案一:重试

首先想到的一个方案是:执行失败后,重试。

重试的方案这里就不再赘述了。

方案二:异步重试

异步重试其实就是:把重试请求扔到「消息队列」中,然后由专门的消费者来重试,直到成功。

到这里,有些人可能又会注意到,写消息队列也有可能会失败的吧?而且,另外引入消息队列,这不仅增加系统的复杂性,而且增加了更多的维护成本,划得来吗?

这个问题问的好,只有不断的思考,提出问题,解决问题,才会有进步。

在回答上面那个问题前,我们先来思考这样的一个问题:如果不把重试的操作仍到消息队列,在执行失败的线程中一直重试,还没等重试执行成功,此时如果该进程「重启」了,那这次重试请求也就「丢失」了,那这条数据就确确实实的不一致了(再也没机会了)。

所以,这里我们把重试或第二步操作放到另一个服务中,这个服务用消息队列来进行重试操作。

再来复习下消息队列的特性:

  • 保证可靠性:写到队列中的消息,成功消费之前不会丢失,重启服务也没事;
  • 保证消息成功投递:下游从队列拉取消息消费,成功消费后才会删除消息,否则还会继续投递消息给消费者(符合重试的场景);

至于写队列失败和消息队列的维护成本问题:

  • 写队列失败:操作缓存和写消息队列,同时失败的概率比较小;
  • 维护成本:消息队列组件比较成熟了,公司项目中一般也都会用到,谈不上维护成本。

如果确实不想在应用中去写消息队列,同时又可以保证一致性的方案还是有的:订阅数据库变更日志,再操作缓存。

换句话来说,就是在服务中想要修改数据时,只需要修改数据库即可,无需再操作缓存。

操作缓存的操作就交给订阅数据库变更日志的中间件:Canal。

Canal 是阿里开源比较成熟的中间件,详细的我这里就不介绍了,有兴趣的可以自行谷歌。

架构模型如下:

读写数据库5.png

优点:

  • 不用考虑写消息队列失败情况:只要写 MySQL 数据库成功,Binlog 就会有;
  • 自动投递到下游队列:canal 会自动把数据库变更日志投递给下游的消息队列,只需配置好即可,可参考我之前写过的一篇文章:Canal 中间件同步 MySQL 数据到 ElasticSearch。

当然 Canal 的高可用和稳定性还是需要维护的。

到这里,我们可以得出以下结论:

如果要保证数据库和缓存的一致性,推荐采用「先更新数据库,再删除缓存」方案,然后配合「消息队列」或「订阅变更日志」的方式来做。

总结

根据上面讲的内容,可以总结如下几点:

  1. 在业务体量大的场景下,引入缓存可以提高性能;
  2. 加缓存后,要考虑缓存和数据库一致性的问题,参考方案:“更新数据库,再删除缓存”;
  3. 在“先更新数据库,再删除缓存”的方案下,为了保证两步都成功执行,可以配合「消息队列」或「订阅变更日志」的方案来做,本质是通过「重试」的方式来保证数据一致性;

另外,分享一些心得:

  1. 很多时候性能和一致性不能同时满足,为了性能考虑,通常会采用「最终一致性」的方案;
  2. 缓存和数据库一致性问题重点关注:缓存利用率、并发、缓存 + 数据库一起成功问题;
  3. 在一些失败场景下如果要保证一致性,常见方法就是「重试」,同步重试会影响吞吐量,所以通常会采用异步重试的方案;

如果你还想看更多优质的技术文章,欢迎关注我的公众号「Go键盘侠」。

本文转载自: 掘金

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

Go channel,面试官会这样问

发表于 2021-10-30

Go 在并发方面的表现很好,这也是 Go 的招牌。Go 在进行并发编程时,下面两个工具都会用到:

  • goroutine:让多个任务并行,每个任务之间不相互影响
  • channel:负责 goroutine 之间的通信

这篇文章会深入研究 channel 的工作机制,这里假设你已经了解 channel 的基本概念。对于 channel,我们需要知道它有如下的属性:

  • 多个 goroutine 同时访问 channel 是安全的
  • channel 中的任务是先进先出的(FIFO)
  • channel 可以在 goroutine 之间传递值
  • channel 会影响 goroutine 调度

下面来探究一下这些特性是怎么实现的。

  1. 创建 channel

可以创建的 channel 有两种:

  • buffered channel,在创建时候需要指定缓冲区的大小
  • unbuffered channel,也被称之为同步 channel,这个可以看成是 buffered channel 的特殊情况,缓冲区的大小设置为 1
1
2
go复制代码ch := make(chan int, 3) // 缓冲区大小为 3 的 buffered channel
ch := make(chan int) // unbuffered channel

chan 的底层数据结构叫 hchan,具体结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
go复制代码type hchan struct {
buf unsafe.Pointer
lock mutex
sendx uint
recvx uint
sendq waitq
recvq waitq
qcount uint
dataqsiz uint
elemsize uint16
closed uint32
elemtype *_type
}

其中 buf 是一个循环队列,用来存储 channel 接收的数据,lock 用来保护数据安全,goroutine 来访问 channel 的 buf 之前,需要先获取锁。

sendx 表示当前数据发送的的位置,recvx 表示当前数据接收的位置。sendq 和 recvq 是两个队列,这两个结构很重要,我们下面会讲到。

qcount 表示当前 buf 中存储的数据个数,dataqsiz 表示 buf 可以接受的最大数据数量。elemtype 就表示数据的类型。

channel 在使用 make 创建的时候,实际上会在堆上分配一块空间,初始化 hchan 结构,然后返回 hchan 的指针。这就是为什么在使用 channel 的时候,直接传递就可以,而不用获取 channel 的指针,这是因为 channel 本身就是指针。

  1. 发送和接收

下面来详细看一下 channel 发送和接收数据的过程,现在假设有下面两个 goroutine: G1 和 G2,一个发送数据,一个接收数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
go复制代码// G1
func main() {
for _, task := range tasks {
taskCh <- task
}
}

// G2
func worker() {
for {
task := <- taskCh
process(task)
}
}

假设现在 G1 先执行,将数据发送到 channel中,会经过下面的三步:

Untitled.png
在将数据放进 buf 之前,需要先获取锁,然后将数据拷贝一份放进去(注意这里是原数据的拷贝),然后再释放锁。

G2 运行后,发现 buf 中有数据,就可以处理,也需要经过三步:

Untitled 1.png

首先也需要获取锁,然后把数据出队列,最后释放锁。

但是很显然,大多数情况下,数据的接受和发送不会这么顺利,假设现在 G2 对一个数据的处理花了很长的时间,而 G1 还在不断的发送数据:

Untitled 2.png

这样 buf 很快就会被数据填满,那么这么时候 G1 就会被阻塞,直接 buf 中的数据被消耗,才会继续发送数据。

这里 goroutine 的阻塞和唤醒就是通过 Go 运行时的调度器来完成。这里有一个经常会被说到的概念:运行时。在这里可以简单的把运行时理解为 Go 代码运行的环境,运行时负责内存分配、垃圾回收等等。

goroutine 表示操作系统用户态的线程,相比于操作系统的线程,更轻量级,goroutine 之间的切换代价更小,但 goroutine 实际上还是在 操作系统线程上运行的。

这个运行时的调度器被称为 GMP 模型,其中 G 就表示 goroutine,M 表示操作系统的线程,P 表示调度器的上下文,维护了一个可执行 goroutine 的列表,负责 goroutine 的调度。

Untitled 3.png

G1 发送任务被阻塞之后,P 就会将 G1 设置为等待状态,解除 G1 与 M 之间的关系,然后从队列中取出另外一个可运行的 goroutine 来运行。这里虽然 G1 被阻塞了,但是运行 G1 的线程并没有被阻塞,依然可以继续运行,对性能的影响会相对小。

Untitled 4.png

G1 在被阻塞的时候会做一些事情,还记得上面 hchan 中 sendq 么,在这个时候就要起作用了。G1 会在被阻塞前创建一个 sudog 的数据,并把这个放进 sendq 中:

Untitled 5.png

当 G2 消费一个数据之后,就会激活 sendq,将 sudog 中的 task4 直接入队到 buf 中,然后再将 G1 设置为可运行状态。

这里需要注意,是 G2 直接让 task4 进入到 buf 中,而不是先唤醒 G1,这样做是为了优化性能,如果先唤醒 G1,那么 G1 还需要先获取锁,然后才能将 task4 放入到 buf 中。

上面说的情况是 G1 做为数据发送方被阻塞,如果是数据接收方 G2 被阻塞会怎样?

G2 同样也会创建一个 sudog,但不是放进 sendq 中,而是放进 recvq 中:

Untitled 6.png

这里 sudog 指向的是一个内存地址,用来接收 G1 发送进来的数据。在这里,最神奇的事情发生了,我们都知道 goroutine 是在栈上分配的,所以这里的 t 是在 G2 的栈上。

G1 发送数据的时候,会把数据直接写入到 t 所指向的内存地址,而不是放到 buf 中。也就是说 G1 直接向 G2 的栈上写入了数据。之所以这样做,也是从性能的角度考虑,这样 G2 就不用再去获取锁了。

这种一个栈向另一个栈写数据的情况只有这一种,在其他的地方不会出现。

  1. 小结

通过上面我们知道,channel 通过 mutex(锁)来保证多个 goroutine 来访问 channel 的时候是安全的,同时 channel 维护了一个循环队列来保证数据的先进先出,同时也完成了在多个 goroutine 之间的数据传递,如果 goruotine 出现阻塞,则由调度器进行调度。

但是 channel 的实现也做出了一些非常规的操作,跨栈写数据,这样会给垃圾回收带来很大的麻烦。但如果不这么做,性能方面就会牺牲很多,所以这是一个性能和简单的一个妥协。

文 / Rayjun

[1] www.youtube.com/watch?v=KBZ…

本文转载自: 掘金

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

1…450451452…956

开发者博客

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