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

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


  • 首页

  • 归档

  • 搜索

Java实现Redis的Stream数据结构消息队列Demo

发表于 2021-11-11

想做一个对消息队列实现场景、但是网上的基本例子都是监听消息、然后在消费、但是他们只是一个Listener监听器监听这所有组、所有消费者的一种情况、他们的具体是实现场景、我个人不是很明白、
以下是我个人想到场景、如有遗漏还请指教!!!

设想1

有个场景我订阅一个人的动态、等他发布动态、可以以很多种方式来通知我、他发布了动态叫我去看、通知方式(邮件通知、短信通知)、发布动态初始流程是他发布动态 》邮件通知 》短信通知 》返回发布动态成功、这种情况很不正常、我发布个动态我要走这么多通知流程、万一某个流程卡住了、我要等几分钟才返回动态发布成功

image.png

搞一个消息队列来优化上面案例

image.png

上图优化了、他发布动态无需等待所有通知完在返回显示动态发布成功、交给一个第三者来处理消息通知、并通知到每一个需要通知的人。

设想2

发布/订阅的场景、有订阅就有通知、通知什么呢?通知我干嘛干嘛了让你来看、就好比张三和李四订阅了老王下楼修水龙头、这时张三和李四去问老王、都有什么通知方式(属于张三和李四的个人持有特权)、这时通知都有什么方式通知到(张三和李四)?、(小秘密!!!、张三是老王好哥们儿)、老王说QQ上通知你们、嘿嘿嘿!!!

后续

由以上场景得知 张三是老王好哥们儿、而李四特权就少点

  • 张三 QQ通知、短信通知、电话通知。
  • 李四 QQ通知。

image.png

从图中来看、老王写的小脚本就是消费者、由他来操作老王的消息通知张三和李四、但是张三和李四的特权又不一样、我们只好让小脚本来终止没有对应特权的消息、如老王给队列中发布了一条消息并携带(已知张三和李四对应拥有的特权)、内容是下午去李阿姨家修水龙头...这条内容给三个组拿到了(老王提前预定的服务)、接下来就看小脚本(消费者)、接下来就是发消息通知他们俩、老王要行动了!!!

  • QQ通知(两个小脚本):脚本1一下没注意、消息被脚本2拿到消息中的张三和李四和对应拥有的特权并通知有特权的人、发送张三和李四都有特权、发送QQ消息给他们俩、后面给小脚本给服务发送的一条消息说这个消息已经发送(标记已经发送、手动ack)。
  • 短信通知(两个小脚本):脚本2一下没注意、消息被脚本1拿到消息中的张三和李四和对应拥有的特权并通知有特权的人、张三通知完、发现李四没有特权、就不会发短信给李四!!!、后面给小脚本给服务发送的一条消息说这个消息已经发送(标记已经发送、手动ack)。
  • 电话通知(两个小脚本):脚本2一下没注意、消息被脚本1拿到消息中的张三和李四和对应拥有的特权并通知有特权的人、张三通知完、发现李四没有特权、就不会打电话给李四!!!、后面给小脚本给服务发送的一条消息说这个消息已经发送(标记已经发送、手动ack)。

小结

以上内容、是由类似发布/订阅、我发布一条内容、订阅我的人得到对应通知、都有什么通知、取决于订阅的人个人特权、就好比如:张三在后台开启了邮件通知、但是没有开启短信通知、在处理消息的时候会根据订阅人的个人特权来对应通知该消息是否要进行通知!!!这是只会有邮件通知到张三!!!

总结

以上需求案例、是在有多种类型的通知、通知顺序不是有序通知、有可能通知2先通知到用户、在通知1在通知到用户、实现场景还是要根据你们的业务来实现、以上案例可以当做一个思路!!!

码云地址:Java实现Redis的Stream数据结构消息队列Demo

image.png

每日一汤

请不要转达对别人的不好的话、你可能不在意、但是别人已经记住了。 请不要转达对别人的不好的话、你可能不在意、但是别人已经记住了。请不要转达对别人的不好的话、你可能不在意、但是别人已经记住了。

本文转载自: 掘金

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

【双十一】我教女票做秒杀

发表于 2021-11-11

大家好,我是牛牛。

双十一又要到了,牛牛有点慌,以前一个人的时候,一分钱都不花,现在有了女票,不仅得剁手,还得帮忙抢各种秒杀商品。

今年,牛牛真的不想再去抢秒杀了,为什么呢?

太难了,成千上万的人就盯着秒杀放出来的那点商品。牛牛凭着单身十几年的手速也抢不过啊。

牛牛苦思妙想,终于想出一条完(zuo)美(si)妙计:给女朋友讲讲程序员是如何做一个秒杀系统的。

对头,就是要用知识的海洋淹没她。如果她不愿意听,或者听不懂,那么今年就不参加双十一了。

至于拒绝理由嘛。。。那就是【你都不认真听我说话,你一定是不爱我了】;如果不幸她听懂了,也不碍事,至少让她知道了我们程序员兄弟多么牛(jian)逼(xin)。

于是,牛牛找到了女朋友阿酱🐰。

🐮:呐,你知道我工作上也经常做秒杀系统吗?今天我就给你讲讲秒杀是怎么做的,如果你听懂了,今年我就帮你抢秒杀!

🐰:可是要是我听不懂怎么办啊?

🐮:我的宝贝怎么可能听不懂,要是听不懂一定是我讲得不够好!

🐰:那。。。我试试吧

问题抛出

首先,秒杀有哪些要考虑的地方呢?

第一点,海量请求,服务要能扛住。

秒杀活动一开始,瞬间会有海量流量涌入,热门的商品甚至会有几百万人来抢。这个规模的流量砸下来,服务可能就挂了,活动也就GG了,收获的只有骂声。

怎么让服务能打能抗,是需要考虑的问题。

第二点,不能超卖。

因为秒杀有时候就是赔本赚吆喝,价格可能比成本价还低。而这时候要是比原计划的数量卖多了,那到底发不发货呢?

发货会超预算亏损,要是超卖数量过多,说不定厂子都要倒闭了;不发货会被投诉,影响商家声誉。

不管怎样,都是硬伤,只能找程序员赔钱了。

第三点,尽量避免少卖。

少卖会比超卖好一些,商家不存在经济上的损失。但要是被眼尖的消费者发现的话,也是免不了一场麻烦的。所以我们还是要尽可能避免这种情况。

第四点,保证触达到用户而不是黄牛。

黄牛可能是开脚本,一次发很多请求过来,抢到之后再转卖。但我们做活动,希望的就是回馈客户,进而吸引用户,而不是去让黄牛赚外快。因此,我们要尽量挡住黄牛的魔爪。

🐰:不听了,不听了,脑壳痛。

🐮:那今年不用剁手啦~

🐰:???你继续,我能行!

🐮:问题我说完了,下面才是重点,来说说解决方案。

🐰:我好像已经开始听不懂了。。。

对症下药

硬抗高并发

在高并发的情况下,MySQL就显得有些力不从心了。

一方面是MySQL本身要支持事务的ACID,单机性能不高。

另一方面,MySQL是个单机数据库,本身是不能水平扩展的,如果要搞分库分表,费时费力。

这时候就可以借助MySQL的好伙伴Redis的能力。

Redis小哥可是单机支撑每秒几万的写入,并且可以做成集群,提高扩展能力的。

我们可以先将库存名额预加载到Redis,然后在Redis中进行扣减,扣减成功的再通过消息队列,传递到MySQL做真正的订单生成。

为什么要通过消息队列呢?

主要有两点好处,一个是这种投递的方式,可以让抢和购解耦。另一个是可以很方便地限频,不至于让MySQL过度承压。

我们说回Redis,如果请求量超过6W每秒,就要考虑使用多个Redis来分流。预计有100W请求量,我们就可以临时调度20个Redis实例来支持,一个5W/s,留点Buffer。

这种模式倒是不需要使用Redis Cluster那种一致性Hash的做法,直接前面接个Nginx,做负载均衡就可以了。

拒绝超卖

解决了高并发的问题,我们再来看看怎么防止超卖。

既然我们将库存名额加载到了Redis,那就需要精确计数。

我们抢购场景最核心的,有两个步骤:

第一步,判断库存名额是否充足;

第二步,减少库存名额,扣减成功就是抢到。

这里有一个问题要考虑,如果第一步判断的时候还有库存,但是由于是并发操作,实际调用的时候,可能已经没有库存了,这样就会造成超卖。

所以第一步和第二步都是需要原子操作的。

但是Redis没有直接提供这种场景原子化的操作。

遇事不要慌,仔细想一想,Redis是不是还有个特性,专门整合原子操作,对,就是它——Lua。

Redis➕Lua,可以说是专门为解决原子问题而生,在Lua脚本中调用Redis的多个命令,这些命令整体上会作为原子操作来进行。

尽量避免少卖

少卖什么情况会出现呢?

库存减少了,但用户订单没生成。

什么情况会这样呢?

在Redis操作成功,但是向Kafka发送消息失败,这种情况就会白白消耗Redis中的库存。

作为一个专业的程序员,只要知道问题是什么、怎么发生的,问题就解决了一半。说白了,我们只需要保证Redis库存+Kafka消耗的最终一致性。

但是一致性问题,一直是分布式场景的恶龙,要对付并不容易。

第一种,也最简单的方式,在投递Kafka失败的情况下,增加渐进式重试;

第二种,更安全一点,就是在第一种的基础上,将这条消息记录在磁盘上,慢慢重试;

第三种,写磁盘之前就可能失败,可以考虑走WAL路线,但是这样做下去说不定就做成MySQL的undo log,redo log这种WAL技术了,会相当复杂,没有必要。

针对少卖这种极端场景可接受的问题,一般选择第二种方式即可,毕竟是异常情况的小概率事件,真出问题了大不了人工介入。

打击黄牛

黄牛的恶劣影响,很多时候是被低估了。

不仅仅是侵害了正常用户的权益,同时由于黄牛善于使用脚本,很容易造成大量的恶意请求,让本就不富裕的服务器资源,雪上加霜。

通常来说,为了打击黄牛,最常见的方式是限购,一个用户最多只能抢到N份,这样可以大大保障正常用户的权益。

具体怎么做呢,为了性能,我们还是将限制逻辑加入到Redis中,所以我们的Lua脚本中,第一步查询库存,第二步扣减库存,需要优化为第一步查询库存,第二步查询用户已购买个数,第三步扣减库存,第四步记录用户购买数。

这里需要注意的是,如果使用Redis集群,那么Redis的一致性Hash Key,需要根据用户来分Key,不然用户数据会查询不到。

有了限购,我们可以保证货品不会被黄牛占据太多,那么还剩一个问题,黄牛大多是通过代码来抢购,点击速度比人点击快得多,这样就导致了竞争不公平。

作为追求极致的coder,我们希望还能更进一步,做到竞争公平。

怎么解决呢?某个用户请求接口次数过于频繁,一般说明是用脚本在跑,可以只针对该用户做限制。

针对IP做限制也是常见做的做法,但这样容易误杀,主要考虑到使用同一个网络的用户,可能都是一个出口IP。限制IP,会导致正常用户也受到影响。

更好用的方案是加上一个验证码验证。验证码符合91原则,90%的时间,都用在验证码输入上,所以使用脚本点击的影响会降到很低。

当然,我们要明白没有银弹,这种方式缺点在于降低了用户的体验感。
故事尾声
🐮:这样一来,我们的秒杀场景就基本OK啦!

🐰:😣😣😣

🐮:怎么样,听懂了吗?

🐰:嗯嗯!(心虚的用力点头)我们能去秒杀了吗?

🐮:那我来检验一下?

🐰:我难道不是你的宝贝了吗?😰😰😰

🐮无奈地叹气:说吧,你这次又想抢什么东西?

回到现实

理想很美好,然而现实是。。。

牛牛心里苦,牛牛不敢说😭😭😭

本文转载自: 掘金

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

浅拷贝与深拷贝 浅拷贝 深拷贝

发表于 2021-11-11

浅拷贝

浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象共享同一块内存

代码展示

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
java复制代码import java.util.ArrayList;

public class ShallowCopyAndDeepCopyTest {

public static class User {

private String name;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}

public static void main(String[] args) {

User user = new User();

user.setName("123");

ArrayList<User> users1 = new ArrayList<>(),
users2 = new ArrayList<>();

users1.add(user);

users2.add(user);

System.out.println("user1的0索引位置的对象中的name为: " + users1.get(0).getName());
System.out.println("user2的0索引位置的对象中的name为: " + users2.get(0).getName());

users1.get(0).setName("456");

System.out.println("user1的0索引位置的对象中的name为: " + users1.get(0).getName());
System.out.println("user2的0索引位置的对象中的name为: " + users2.get(0).getName());

}
}

打印展示

浅拷贝

深拷贝

深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象

代码展示

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
java复制代码import java.util.ArrayList;

public class ShallowCopyAndDeepCopyTest {

/**
* Cloneable: 实现了该接口后,可以重写Object类中的clone方法,而不抛出CloneNotSupportedException异常
*/
public static class User implements Cloneable {

private String name;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

/**
* 克隆对象
*/
public Object clone() throws CloneNotSupportedException {
return super.clone();
}

@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
'}';
}
}

public static void main(String[] args) throws CloneNotSupportedException {

User user1 = new User();

user1.setName("123");

/* 拷贝对象 */
User user2 = (User)user1.clone();

/* 创建新对象 */
User user3 = new User();

user3.setName("123");

ArrayList<User> users1 = new ArrayList<>(),
users2 = new ArrayList<>(),
users3 = new ArrayList<>();

users1.add(user1);
users2.add(user2);
users3.add(user3);

users1.get(0).setName("456");

System.out.println("user1的0索引位置的对象中的name为: " + users1.get(0).getName());
System.out.println("user2的0索引位置的对象中的name为: " + users2.get(0).getName());
System.out.println("user3的0索引位置的对象中的name为: " + users3.get(0).getName());

}
}

打印展示

深拷贝

本文转载自: 掘金

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

订单系统的表设计探讨

发表于 2021-11-11

一,订单系统的最小模型

1
2
diff复制代码|===订单信息============|      |=======商品信息====================|======支付信息========|
|订单编号|订单状态|下单时间|用户ID|商家ID|商品ID|款式ID|价格|购买数量|运费|支付金额|支付状态|支付时间|

以上包含了一张订单的最基本的信息,我们看到基本信息包含了几大要素和扩展思路

1,用户信息

用户ID用来关联统计用户的购买信息或者限购

核销订单:记录用户的手机号,可建order_contact表,记录用户下单时填充的联系方式

物流订单:记录用户下单时选择的地址信息联系方式,可建order_address表,用来记录用户下单的地址信息,这些都是以下单时刻的信息为准,不能关联ID

2,商品款式信息

物流商品绝大多数都是支持跨商家的多商品的下单的,因此需要在订单表中拆出

1
2
复制代码订单表
|订单编号|订单状态|下单时间|用户ID|商家ID|商品总金额|运费总金额|支付金额|支付状态|支付时间|
1
2
复制代码订单商品表
|订单ID|商家ID|商品ID|款式ID|价格|购买数量|支付金额|运费金额|

3,快照问题

订单快照一般是对应订单中的某个商品的快照记录,是商品纬度的信息,也就是说,如果一个订单下单购买了同一个商品的不同款式,只需要记录一条商品快照即可,一般用mongodb来存储信息,但不同的数据库,也造成了查询表的麻烦,大量的实践经验表明,商品信息,价格信息是查询最常用的几个字段,我们冗余到订单商品表中,改造后为这样:

1
2
3
diff复制代码订单商品表
|=====商品信息=========|====款式信息==========|
|订单ID|商家ID|商品ID|商品名称|商品头图|款式ID|款式名称|款式头图|原价|售卖价|下单价格|购买数量|支付金额|运费金额|
  • 另一种思路:
    商品编辑的过程引入版本的概念,每次的编辑,都更新一个版本号字段,存储快照的时候,检测这个版本的商品,是否已经有快照存在了,如果没有就插入,有就直接保存一个快照ID即可

4,支付信息

绝大多数订单系统里只有一种支付模式,微信支付,支付宝支付等等,我们需要记录回调的信息

1
复制代码|订单ID|支付类型|下单第三方商户号|下单第三方交易流水号|支付金额|

商户号记录可能会用到的商户统计问题,比如多个商户号的支付模式,可以记录资金的流向

5,价格信息

商品可能存在的价格:进货价,市场价,销售价,实际支付时的单价,结算价

订单需要保存的:商品总支付金额(不包含运费,所有商品销售价*购买数量),商品总运费,商品总调整金额(即各种插件优惠劵等优惠金额),商品总的需要结算的金额

6,结算信息

在SAAS平台中,通常会需要对售出的商品进行结算,我们需要记录下单时的结算价

1
2
复制代码订单商品表
|订单ID|商家ID|商品ID|商品名称|商品头图|款式ID|款式名称|款式头图|原价|售卖价|下单价格|购买数量|支付金额|运费金额|结算价|

7,售后信息

售后的配置一般都保存在商品的快照里,取实时的话,就是拿商品的售后配置信息

8,优惠信息

参加活动是商品非常常见的需求,各种组合方式,也带来了各种复杂的逻辑设计,但万变不离其宗,都是对于价格的变动,我们定义为计价系统,后续进行叙述。

1
2
复制代码订单价格调整表
|订单ID|调整类型|调整金额|关联活动ID|关联活动信息|是否参与计价|

对于活动和插件的延伸:我们在普通交易的一个订单中,我们往往需要在下单时,就计算出这个订单使用了多少优惠,以及这个订单下单某个商品款式使用了多少优惠,此时我们需要把订单调整金额,也冗余到订单表和订单商品表中,所以最终的表设计为:

1
2
复制代码订单表
|订单编号|订单状态|下单时间|用户ID|商家ID|商品总金额|运费总金额|优惠总金额|支付金额|支付状态|支付时间|
1
2
复制代码订单商品表
|订单ID|商家ID|商品ID|商品名称|商品头图|款式ID|款式名称|款式头图|原价|售卖价|下单价格|购买数量|支付金额|运费金额|结算价|优惠金额|下单单价|

总结

这里我们讨论哪些需要存储,哪些只要关联主键,取决于产品需求本身是要求以下单时的信息为准,还是以实时的配置信息为准。我们在此可以看到,其实订单表可以认为是商家—纬度的信息,订单商品表是商品款式—纬度的信息,所以当具体到商家的一些关于下单的配置时,可以冗余到订单表,比如商家的配置限购优惠之类的,而关于商品的信息,比如分销配置,核销配置,结算配置,售后配置等等跟商品关联的信息,可以在订单商品表考虑进行冗余

本文转载自: 掘金

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

Python matplotlib底层原理浅析 复习回顾 1

发表于 2021-11-11

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

复习回顾

前期,我们已经学习了matplotlib模块相关的基础知识,对 matplotlib 模块折线图、饼图、柱状图进行操作。

我们都知道matplotlib 是偏向底层用于可视化数据处理的库,我们在绘制图表的时候主要步骤主要有四大步骤

  • 导入 matplotlib.pplot库
  • 使用pandas/numpy模块对数据进行整分析理
  • 调用pyplot中绘制方法绘制折线图、饼图等
  • 调用pyplot.show展示出来

在matplotlib官网上,可以看到丰富多样的图表教程

image.png

以上是我们上一期学习的内容,对于matplotlib模块来说它的底层是怎么工作的?

俗话说,学习要做到知其然,也要知其所以然,这样才能更好使用matplotlib模块相关方法。

哪我们接下来开始对matplotlib底层进行学习,Let’s go~

  1. matplotlib 框架组成

matplotlib 模块在众多数据可视化库中可以可以实现复杂的底层操作。像gglot、seaborn、plotnline 底层都是基于matplotlib 模块去封装不同风格的统计图表。

matplotlib 模块底层主要是由三部分组成脚本层、美工层和后端层。

  • 脚本层:为用户提供可视化编程的接口
  • 美工层:有大量绘制图表方法的接口
  • 后端:连接硬件,处理图像元素的接口

image.png

PS:matplotlib框架说明

  1. 脚本层(scripting)

脚本层属于matplotlib模块中最上层,主要为用户提供可视化编程的接口,代表pyplot模块。

对于普通用户,pyplot接口可以满足大多数文本的图像和坐标的生成,传给后端进行处理。

  • matplotlib.pyplot接口导入时,通常是import matplotlib.pyplot as plt
+ 导入pyplot 模块并重名为plt
+ pyplot 模块加载时,会对本地的配置文件进行分析
+ 同时会声明默认的后端,例如声明创建Figure对象
+ 将脚本深拷贝给后端后退出
  • pyplot 模块提调用matplotlib的方法
+ 供给用面向oo调用风格,显示创建图形和轴调用其方法
+ 依靠pyplot 自动创建和管理图形和轴,并使用pyplot函数进行绘图
  • 用户只需调用pyplot模块相关的方法,就可以绘制漂亮的图表啦
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
python复制代码
from matplotlib import pyplot

import pandas

pyplot.rcParams["font.sans-serif"]=['SimHei']
pyplot.rcParams["axes.unicode_minus"]=False

pyplot.bar([1,2,3,4,5,6],[45,20,19,56,35,69])

pyplot.title("data analyze")
pyplot.xlabel("元素 a")
pyplot.ylabel("元素 b")

pyplot.show()

image.png

  1. 美工层(artist)

在美工层位于matplotlib中间层,主要进行数据相关的绘制工作,绘制图表中的标题、直线、刻度等都是artist对象的实例。

  • artist 层特点

+ 脚本层创建的Figure对象是Artist对象实例
+ Artist的基类是matplotlib.artist.Artist,共享所有Artist属性包括从美工系统到画布坐标系统变化等
+ 提供处理用户交互动作的接口
  • matplotlib 图表对象

我们可以通过如下matplotlibe图表中可以看到一张图表由多个对象组合而成的。

image.png

  • matplotlib 图表对象说明

对象 说明
Figure 图形,弹出框口即是figure
axes 子图
title 标题
legend 图例
Major tick 大标尺刻度
Minor tick 小标尺刻度
Line 线型图
axis label 坐标指标说明
Marker 数据标准说明
* Artist 对象说明
———–

ArtIst 对象包含Figure、Axes、Axis对象,是它们的基类,其Artist对象都全部位于后端提供的canvas画布上。

+ Figure


    - 一个图表窗口即是一个figure对象
    - figure对象中至少要包含一个Axes对象子图
    - figure对象中可以包含title、label等Artist对象
    - figure对象中包含的不可见对象canvas。绘制图像时会进行调用
+ Axes


    - axes 是子图对象,子图对象指的是x和y轴.
    - axes 常用有set\_xlabel()、set\_ylabel()设置x和y轴坐标名字
+ Axis


    - axis 是代表数据轴的对象,主要用于表示刻度位置和显示数值
    - axis 包含用于控制刻度位置的Locator和显示刻度Formatter两个子对象
+ Artist对象层级结构图如下


![image.png](https://gitee.com/songjianzaina/juejin_p14/raw/master/img/e01d3712c31436687bef4b5affc57e53d86f66f23756ecc975769c950708816a)
  1. 后端层(backend)

后端层主要是matplotlib 模块底层实现,主要实现了三方面的抽象接口
4.

  • FigureCanvas:对Artist对象绘制提供画布功能进行封装

matplotlib 模块底层是基于硬的用户画面,FigureCaves接口主要完成前期初始化工作

+ 将自身嵌入到原生的QT视觉窗口(QtGui.QMainWindow)
+ 将matplotlib的绘制命令Render转换到canvas上(QtGui.QPainter)
+ 将原生Qt事件转成matplotlib的Event接口,Event接口接收到信息后进行处理
  • Renderer: 相当于画笔,执行绘制动作

Render 主要提供硬件底层的绘图接口,能对Artist绘制命令进行执行。

+ Render 接口最初源于GDK的Drawable接口,后来转换成独立后端的原生绘图命令。
+ matplotlib 是支持C++模块库基于像素点核心渲染器agg
+ 可以进行2d反锯齿渲染、PNG图片生成
  • Event: 处理用户键盘和鼠标输入事件

Event 框架是将key-press-event或者mouse-motion-event等UI事件映射到键盘或者鼠标事件类中。

+ 用户可以连接事件,使用函数进行回调
+ 图形与数据交互

总结

本期,对matplotlib模块底层实现进行深入地认识和学习。在matplotlib模块中底层是基于C++模板库Agg来渲染图片效果的,同时提高脚本层pyplot让非专业的人也能轻松处理数据展示数据。

以上是本期内容,欢迎大佬们点赞评论,下期见~

本文转载自: 掘金

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

Envoy Ingress:Contour基本原理和源码分析

发表于 2021-11-11

概述

  • contour 是一个开源的 k8s ingress controller
  • 使用 envoy 提供反向代理
  • 提供动态配置更新
  • 支持多集群网络代理

特性

  • 基于高性能的L7代理和负载均衡Envoy作为数据面
  • 灵活的架构
  • TLS安全代理

为什么选择Contour

  • 动态配置更新,最大限度减少连接中断
  • 安全的支持多集群,多配置
  • 使用 HTTPProxy CRD 增强 ingress 核心配置

安装

kubectl

1
2
3
4
bash复制代码# yaml 安装
kubectl apply -f https://projectcontour.io/quickstart/contour.yaml
# 验证
kubectl get pod -n projectcontour

helm

1
2
3
4
bash复制代码# 添加仓库
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
helm install contour --namespace projectcontour bitnami/contour

资源

部署完成后查看安装的资源信息,包括:

  • contour deployment:counter controller,作为控制面
  • envoy daemonset:envoy,作为数据面
1
2
3
4
5
6
7
8
9
10
bash复制代码$ kubectl get deploy,svc,ds,job -n projectcontour
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/contour 2/2 2 2 12d

NAME TYPE CLUSTER-IP EXTERNAL-IP
service/contour ClusterIP 172.20.126.255 <none> 8001/TCP 12d
service/envoy LoadBalancer 172.20.12.164 ...

NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE
daemonset.apps/envoy 10 10 10 10 10 <none> 12d

CRD

contour 安装完成后,查看 crd 有哪些:

1
2
3
4
5
6
bash复制代码$ kubectl get crd|grep contour
contourconfigurations.projectcontour.io 2021-11-04T13:30:06Z
contourdeployments.projectcontour.io 2021-11-04T13:30:06Z
extensionservices.projectcontour.io 2021-11-04T13:30:06Z
httpproxies.projectcontour.io 2021-11-04T13:30:06Z
tlscertificatedelegations.projectcontour.io 2021-11-04T13:30:06Z

其中最核心的 CRD 是 HTTPProxy,等效于 k8s 提供的原生 Ingress,提供路由配置功能。

看一个简单的 httpproxy 配置示例,当访问 foo1.bar.com 时,将代理到后端 s1 服务的 80 端口

将 foo1.bar.com 域名和任意一个Node节点(daemonset部署)的ip做域名绑定后,即可通过域名访问服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
yaml复制代码apiVersion: projectcontour.io/v1
kind: HTTPProxy
metadata:
name: name-example-foo
namespace: default
spec:
virtualhost:
fqdn: foo1.bar.com
routes:
- conditions:
- prefix: /
services:
- name: s1
port: 80

架构

概述

contour 主要包含两个组件:

  • Envoy:提供高性能的反向代理
  • Contour:充当 Envoy 的后台管理服务,提供相关配置

两个组件是分开部署的,Contour 以 Deployment 的方式部署,Envoy 以 DaemonSet 的方式部署

架构图

原理

数据面:envoy侧

envoy pod 中包含两个 container

  • initContainer container:执行 contour bootstrap 命令,生成一个配置文件到临时目录,envoy container共享这个目录,并作为启动的配置文件,里面配置了 contour server 地址,作为 envoy 的管理服务
  • envoy container:根据配置文件启动envoy,建立 GRPC连接,并从服务端接收配置请求

控制面:contour 侧

  • contour 是 k8s api 的一个客户端,通过 informer 机制监听 ingress,service,endpoint,secret等k8s原生资源,和httpproxy这个CRD
  • contour 缓存k8s的资源信息,并最终转换为 Envoy 需要的 XDS 信息。

原理图

其他

从图中我们还看到其他的一些信息

  • contour job:自动生成证书,使得grpc上的xds安全传输
  • contour 内部还提供了选主模式

HTTPProxy配置

概述

Ingress 资源在 k8s 1.1 版本引入,从那以后,Ingress API 保持相对稳定,需要使用特殊的能力需要借助于 Annotation 实现。

HttpProxy CRD 的目标是扩展 Ingress API 的能力,为用户提供更丰富的功能体验,以及解决 Ingress 在多租户环境下的限制。

对比 Ingress 的优势

  • 安全地支持多 Kubernetes 集群,能够限制哪些命名空间可以配置虚拟主机和 TLS 凭据。
  • 能够包括来自另一个 HTTPProxy 的路径或域的路由配置(这个HTTPProxy可能在另一个命名空间中)
  • 允许单个路由中接受多个服务并负载均衡它们之间的流量
  • 天生支持定义服务权重和负载策略,不需要使用注解
  • 支持创建时校验配置的合法性

配置文件对比

Ingress

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
yaml复制代码# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: basic
spec:
rules:
- host: foo-basic.bar.com
http:
paths:
- backend:
service:
name: s1
port:
number: 80

HTTPProxy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
yaml复制代码# httpproxy.yaml
apiVersion: projectcontour.io/v1
kind: HTTPProxy
metadata:
name: basic
spec:
virtualhost:
fqdn: foo-basic.bar.com
routes:
- conditions:
- prefix: /
services:
- name: s1
port: 80

源码分析

原理回顾

通过前面 contour 的基本原理和架构的介绍,总结如下:

数据流转原理

  • contour 通过 informer 机制监听 k8s 资源
  • 将k8s资源转换为 envoy 需要的 xds 资源
  • 通过grpc将xds配置下发到envoy节点

流量流转原理

  • 带有域名的请求经过任意一个Node节点,进入Envoy(以 hostNetWork=true 部署方式为例)
  • envoy将请求路由到 HTTPProxy 中配置的后端 service

数据流转实现

概述

实际上在将 k8s 资源转换为最终 xds 资源的过程中,还经过了其他的数据转换。

高清图

contour数据流转图

  • k8s资源变化,触发 informer 通知机制,注册的回调函数将事件放入 channel
  • EventHandler协程消费 channel,将channel中的每种资源以一个map形式保存在KubernetesCache中,map的key是该资源的namespace + name唯一确定的
  • KubenetesCache中的资源,经过一序列 processor 的处理之后,生成 DAG
  • DAG变更,会通知一序列 Observer,依次调用 OnChange方法,传入DAG,每个 Observer对应一个 XDS协议,将DAG中需要的资源转换为XDS配置,保存在Cache中
  • GRPC读取Cache中的数据,通过Stream将XDS下发到Envoy。下发这一步使用了go-control-plane这个开源框架

源码调用图

源码调用高清图

源码调用图

ResourceEventHandler

k8s 提供了 informer 机制,可以 watch kubernetes资源的变更。contour中 watch 的资源包括两类:

k8s原生资源:

  • ingress
  • service
  • endpoint
  • namespace

crd资源:

  • httpproxy
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
go复制代码// contour/cmd/contour/serve.go
func doServe(log logrus.FieldLogger, ctx *serveContext) error {
// 这里监听 contour 的 crd 资源, 比如:httpproxy
for _, r := range k8s.DefaultResources() {
inf, err := clients.InformerForResource(r)
......
inf.AddEventHandler(&dynamicHandler)
}
// 监听 ingress 资源
for _, r := range k8s.IngressV1Resources() {
if err := informOnResource(clients, r, &dynamicHandler); err != nil {
log.WithError(err).WithField("resource", r).Fatal("failed to create informer")
}
}
......
// 监听 secret 资源
for _, r := range k8s.SecretsResources() {
......
if err := informOnResource(clients, r, handler); err != nil {
log.WithError(err).WithField("resource", r).Fatal("failed to create informer")
}
}

// 监听 endpoint 资源
for _, r := range k8s.EndpointsResources() {
if err := informOnResource(clients, r, &k8s.DynamicClientHandler{
Next: &contour.EventRecorder{
Next: endpointHandler,
Counter: contourMetrics.EventHandlerOperations,
},
Converter: converter,
Logger: log.WithField("context", "endpointstranslator"),
}); err != nil {
log.WithError(err).WithField("resource", r).Fatal("failed to create informer")
}
}
......
}

不管是 原生资源,还是 crd 资源,Informer 监听的原理都是一样的,用户只需要编写事件处理函数,对应的接口是 ResourceEventHandler,该接口提供了增加、删除、更新回调函数。

1
2
3
4
5
6
go复制代码// k8s.io/client-go/tools/cache/controller.go
type ResourceEventHandler interface {
OnAdd(obj interface{})
OnUpdate(oldObj, newObj interface{})
OnDelete(obj interface{})
}

Contour 中所有的资源复用了两个 ResourceEventHandler 的实现类:

  • EndpointsTranslator:处理 endpoint 资源
  • EventHandler:处理除了 endpoint 外的其他资源。

EventHandler 将得到的资源信息,放入channel中,由单独的协程去消费以解耦

1
2
3
4
5
6
7
8
9
10
11
go复制代码func (e *EventHandler) OnAdd(obj interface{}) {
e.update <- opAdd{obj: obj}
}

func (e *EventHandler) OnUpdate(oldObj, newObj interface{}) {
e.update <- opUpdate{oldObj: oldObj, newObj: newObj}
}

func (e *EventHandler) OnDelete(obj interface{}) {
e.update <- opDelete{obj: obj}
}

放入channel的信息,由谁去消费呢?

监听 channel 的协程,拿到事件后,取出资源信息,保存到 KubernetesCache 中。

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
go复制代码// contour/cmd/contour/server.go
func doServe(log logrus.FieldLogger, ctx *serveContext) error {
......
// 启动协程
g.Add(eventHandler.Start())
......
}

// counter/internal/contour/handler.go
func (e *EventHandler) Start() func(<-chan struct{}) error {
e.update = make(chan interface{})
return e.run
}

func (e *EventHandler) run(stop <-chan struct{}) error {
......
// 死循环,一直监听 channel 中的事件
for {
select {
case op := <-e.update:
if e.onUpdate(op) {
......
}
......
}

// 处理事件
// 将资源保存到 e.Builder.Source
func (e *EventHandler) onUpdate(op interface{}) bool {
switch op := op.(type) {
case opAdd:
return e.Builder.Source.Insert(op.obj)
case opUpdate:
......
remove := e.Builder.Source.Remove(op.oldObj)
insert := e.Builder.Source.Insert(op.newObj)
return remove || insert
case opDelete:
return e.Builder.Source.Remove(op.obj)
case bool:
return op
default:
return false
}
}

EndpointsTranslator 监听的 endpoint 资源,处理比较麻烦一些,以 add 为例分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
go复制代码// contour/internal/xdscache/v3/endpointstranslator.go
func (e *EndpointsTranslator) OnAdd(obj interface{}) {
switch obj := obj.(type) {
case *v1.Endpoints:
// 更新本地缓存
e.cache.UpdateEndpoint(obj)
// 合并资源
e.Merge(e.cache.Recalculate())
e.Notify()
if e.Observer != nil {
// 刷新保存快照
e.Observer.Refresh()
}
default:
e.Errorf("OnAdd unexpected type %T: %#v", obj, obj)
}
}

KubernetesCache

类中定义了多个map类型的资源对象,分别存储不同的资源,key是 namespace 和 name 组成的 type.NamespacedName,将原生k8s中不同 namespace 的同一资源都存储到一个map中,多种资源的多个map,共同组成 KubernetesCache

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
go复制代码// contour/internal/dag/cache.go
type KubernetesCache struct {
......
ingresses map[types.NamespacedName]*networking_v1.Ingress
ingressclass *networking_v1.IngressClass
httpproxies map[types.NamespacedName]*contour_api_v1.HTTPProxy
secrets map[types.NamespacedName]*v1.Secret
tlscertificatedelegations map[types.NamespacedName]*contour_api_v1.TLSCertificateDelegation
services map[types.NamespacedName]*v1.Service
namespaces map[string]*v1.Namespace
gatewayclass *gatewayapi_v1alpha1.GatewayClass
gateway *gatewayapi_v1alpha1.Gateway
httproutes map[types.NamespacedName]*gatewayapi_v1alpha1.HTTPRoute
tlsroutes map[types.NamespacedName]*gatewayapi_v1alpha1.TLSRoute
tcproutes map[types.NamespacedName]*gatewayapi_v1alpha1.TCPRoute
udproutes map[types.NamespacedName]*gatewayapi_v1alpha1.UDPRoute
backendpolicies map[types.NamespacedName]*gatewayapi_v1alpha1.BackendPolicy
extensions map[types.NamespacedName]*contour_api_v1alpha1.ExtensionService
......
}

// map中key的数据类型:由 namespace 和 name 共同决定
type NamespacedName struct {
Namespace string
Name string
}

KubernetesCache 插入流程:

  • 首先判断资源类型
  • 将不同的资源放入不同的map
  • 有些资源的变更,还需要触发其他资源变更。比如:service变化了,对应的 ingress或者httpproxy也需要联动变化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
go复制代码// contour/internal/dag/cache.go
func (kc *KubernetesCache) Insert(obj interface{}) bool {
// 初始化 map,确保执行一次
kc.initialize.Do(kc.init)
// 根据不同的资源类型,放入不同的 map
switch obj := obj.(type) {
case *v1.Secret:
......
// 保存 secret 资源
kc.secrets[k8s.NamespacedNameOf(obj)] = obj
return kc.secretTriggersRebuild(obj)
case *v1.Service:
kc.services[k8s.NamespacedNameOf(obj)] = obj
return kc.serviceTriggersRebuild(obj)
case *v1.Namespace:
kc.namespaces[obj.Name] = obj
return true
......
}

return false
}

DAG

构造好 KubernetesCache 之后,contour 通过 一些列的 processor,将 KubernetesCache 变成 DAG,DAG 代表了 k8s 不同资源关系的一个有向无环图。核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
go复制代码// contour/internal/contour/handler.go
func (e *EventHandler) rebuildDAG() {
// 将 KubernetesCache 构建成 DAG
latestDAG := e.Builder.Build()
// DAG 变更时,通知所有的 Observer,
// 每种 Observer 是一个 XDS 对象,取出 DAG 资源转换为 XDS 配置
e.Observer.OnChange(latestDAG)
// 更新 snapshot 信息
for _, upd := range latestDAG.StatusCache.GetStatusUpdates() {
e.StatusUpdater.Send(upd)
}

}

DAG 数据结构如下,根节点 roots 是一个 Vertex 的列表,Vertex 只定义了Visit接口。即:所有实现了 Visit

1
2
3
4
5
6
7
8
9
10
11
12
13
go复制代码// contour/internal/dag/dag.go
type DAG struct {
// StatusCache holds a cache of status updates to send.
StatusCache status.Cache

// roots are the root vertices of this DAG.
roots []Vertex
}

// Vertex is a node in the DAG that can be visited.
type Vertex interface {
Visit(func(Vertex))
}

build过程分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
go复制代码// contour/internal/dag/builder.go
func (b *Builder) Build() *DAG {
// 先构造一个空的 DAG
dag := DAG{
StatusCache: status.NewCache(b.Source.ConfiguredGateway),
}

// 遍历所有的 processor,每个 processor 把不同的 k8s 资源变成 DAG 的一部分信息
// 所有的 processor 共同构成完整的 DAG
for _, p := range b.Processors {
p.Run(&dag, &b.Source)
}
return &dag
}

processor 列表包括:

  • IngressProcessor
  • ExtensionServiceProcessor
  • HTTPProxyProcessor
  • GatewayAPIProcessor
  • ListenerProcessor
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
go复制代码func getDAGBuilder(ctx *serveContext, clients *k8s.Clients, clientCert, fallbackCert *types.NamespacedName, 	log logrus.FieldLogger) dag.Builder {

dagProcessors := []dag.Processor{
&dag.IngressProcessor{
...
},
&dag.ExtensionServiceProcessor{
...
},
&dag.HTTPProxyProcessor{
...
},
}
if ctx.Config.GatewayConfig != nil && clients.ResourcesExist(k8s.GatewayAPIResources()...) {
dagProcessors = append(dagProcessors, &dag.GatewayAPIProcessor{
FieldLogger: log.WithField("context", "GatewayAPIProcessor"),
})
}
...
dagProcessors = append(dagProcessors, &dag.ListenerProcessor{})
...
}

每个processor 实现 Run 方法,接收 DAG 和 KubernetesCache 参数,通过 KubernetesCache 修改 DAG,以 HTTPProxyProcessor 为例分析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
go复制代码func (p *HTTPProxyProcessor) Run(dag *DAG, source *KubernetesCache) {
...
// 校验合法性
for _, proxy := range p.validHTTPProxies() {
// 计算路由规则
// 入参为 crd 资源 HttpProxy
// 内部经过各种计算,构造路由关系,最终保存在DAG中
p.computeHTTPProxy(proxy)
}

for meta := range p.orphaned {
proxy, ok := p.source.httpproxies[meta]
if ok {
pa, commit := p.dag.StatusCache.ProxyAccessor(proxy)
pa.ConditionFor(status.ValidCondition).AddError(contour_api_v1.ConditionTypeOrphanedError,
"Orphaned",
"this HTTPProxy is not part of a delegation chain from a root HTTPProxy")
commit()
}
}
}

DAG 中的节点(Vertex)有:

  • VirtualHost:对应 LDS
  • SecureVirtualHost:对应 LDS 和 CDS
  • ExtensionCluster:对应 CDS
  • ServiceCluster:对应 EDS
  • Cluster:对应 CDS,一部分SDS
  • Listener:对应 RDS

其中某些 Vectex 又包含子 Vectex,因此用图这种数据结构串联起来。

ResourceCache

前面分析 DAG 代码时,在生成 DAG 后,会通知所有的 Observer,即e.Observer.OnChange

1
2
3
4
5
6
7
8
9
10
11
12
go复制代码// contour/internal/contour/handler.go
func (e *EventHandler) rebuildDAG() {
// 将 KubernetesCache 构建成 DAG
latestDAG := e.Builder.Build()
// DAG 变更时,通知所有的 Observer,
// 每种 Observer 是一个 XDS 对象,取出 DAG 资源转换为 XDS 配置
e.Observer.OnChange(latestDAG)
// 更新 snapshot 信息
for _, upd := range latestDAG.StatusCache.GetStatusUpdates() {
e.StatusUpdater.Send(upd)
}
}

这里的 e.Observer 是 一堆 Observer的组合

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
go复制代码// contour/cmd/contour/serve.go
func doServe(log logrus.FieldLogger, ctx *serveContext) error {
...
// ResoureCache 列表
resources := []xdscache.ResourceCache{
xdscache_v3.NewListenerCache(listenerConfig, ctx.statsAddr, ctx.statsPort),
&xdscache_v3.SecretCache{},
&xdscache_v3.RouteCache{},
&xdscache_v3.ClusterCache{},
endpointHandler,
}
...
eventHandler := &contour.EventHandler{
...
// 注册一个组合的 Observer
Observer: dag.ComposeObservers(append(xdscache.ObserversOf(resources), snapshotHandler)...),
...
}
}

// 组合多个Observer
func ComposeObservers(observers ...Observer) Observer {
return ObserverFunc(func(d *DAG) {
for _, o := range observers {
o.OnChange(d)
}
})
}

ResourceCache 是一个接口,聚合了两个接口:dag.Observer 和 xds.Resource。该接口定义了如何处理 DAG,以及处理完成后如何返回给调用方。

  • Observer接口:
    • OnChange:参数为DAG,当 DAG 有任何变更,都会触发这个函数,可以拿到最新的DAG做处理
  • Resource接口:
    • Contents:返回xds资源的接口
    • Query:查询xds资源的接口
    • Register:注册xds资源的接口
    • TypeURL:xds资源类型
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
go复制代码// contour/internal/xdscache/resources.go
type ResourceCache interface {
dag.Observer
xds.Resource
}

// contour/internal/dag/dag.go
type Observer interface {
// 当 DAG变更时,触发相应业务逻辑
OnChange(*DAG)
}

// contour/internal/xds/resource.go
type Resource interface {
// 返回全部资源内容
// Contents returns the contents of this resource.
Contents() []proto.Message

// 通过给定的名称,返回对应的资源内容
// Query returns an entry for each resource name supplied.
Query(names []string) []proto.Message

// 注册资源
// Register registers ch to receive a value when Notify is called.
Register(chan int, int, ...string)

// 资源类型
// TypeURL returns the typeURL of messages returned from Values.
TypeURL() string
}

不同的 xds 资源类型都实现了ResourceCache 接口:

  • RouteCache:对应 RDS
  • ClusterCache:对应 CDS
  • ListenerCache:对应 LDS
  • SecretCache:对应 SDS
  • EndpointsTranslator:对应 EDS

转换后的 xds 资源都保存在 values 字段中。

  • Observer的OnChange接口:将 DAG 转换为 Envoy xds 配置保存在 values 中
  • Resource的接口:提供查询 values 的方法,即可得到 envoy 的xds配置
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
go复制代码// contour/internal/xdscache/v3/cluster.go
// ClusterCache manages the contents of the gRPC CDS cache.
type ClusterCache struct {
mu sync.Mutex
values map[string]*envoy_cluster_v3.Cluster
contour.Cond
}

// contour/internal/xdscache/v3/listener.go
// ListenerCache manages the contents of the gRPC LDS cache.
type ListenerCache struct {
mu sync.Mutex
values map[string]*envoy_listener_v3.Listener
staticValues map[string]*envoy_listener_v3.Listener

Config ListenerConfig
contour.Cond
}

// contour/internal/xdscache/v3/route.go
// RouteCache manages the contents of the gRPC RDS cache.
type RouteCache struct {
mu sync.Mutex
values map[string]*envoy_route_v3.RouteConfiguration
contour.Cond
}

// contour/internal/xdscache/v3/secret.go
// SecretCache manages the contents of the gRPC SDS cache.
type SecretCache struct {
mu sync.Mutex
values map[string]*envoy_tls_v3.Secret
contour.Cond
}

// contour/internal/xdscache/v3/endpointstranslator.go
// A EndpointsTranslator translates Kubernetes Endpoints objects into Envoy
// ClusterLoadAssignment resources.
type EndpointsTranslator struct {
// Observer notifies when the endpoints cache has been updated.
Observer contour.Observer

contour.Cond
logrus.FieldLogger

cache EndpointsCache

mu sync.Mutex // Protects entries.
entries map[string]*envoy_endpoint_v3.ClusterLoadAssignment
}

以 RouteCache 的 OnChange 为例,查看实现逻辑

1
2
3
4
5
6
7
go复制代码// contour/internal/xdscache/v3/route.go
func (c *RouteCache) OnChange(root *dag.DAG) {
// 读取 DAG 中的信息,生成 envoy 的 RDS 资源 envoy_route_v3.RouteConfiguration
routes := visitRoutes(root)
// 将配置文件放到 value 字段中,供 Query、Context 等方法调用时查看
c.Update(routes)
}

DAG 转换为 RDS 的逻辑实现

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
80
81
82
83
go复制代码// contour/internal/xdscache/v3/route.go
func visitRoutes(root dag.Vertex) map[string]*envoy_route_v3.RouteConfiguration {
// Collect the route configurations for all the routes we can
// find. For HTTP hosts, the routes will all be collected on the
// well-known ENVOY_HTTP_LISTENER, but for HTTPS hosts, we will
// generate a per-vhost collection. This lets us keep different
// SNI names disjoint when we later configure the listener.

// http 和 https 需要做不同的处理
rv := routeVisitor{
routes: map[string]*envoy_route_v3.RouteConfiguration{
ENVOY_HTTP_LISTENER: envoy_v3.RouteConfiguration(ENVOY_HTTP_LISTENER),
},
}

rv.visit(root)

for _, v := range rv.routes {
sort.Stable(sorter.For(v.VirtualHosts))
}

return rv.routes
}

// contour/internal/xdscache/v3/route.go
// 从根节点开始查找所有的 Route,查找顺序: Listener -> VirtualHost(SecureVirtualHost) -> Route
func (v *routeVisitor) visit(vertex dag.Vertex) {
switch l := vertex.(type) {
// 处理 DAG 中的 Listener
case *dag.Listener:
l.Visit(func(vertex dag.Vertex) {
switch vh := vertex.(type) {
// 处理 VirtualHost
case *dag.VirtualHost:
v.onVirtualHost(vh)
// 处理 SecureVirtualHost
case *dag.SecureVirtualHost:
v.onSecureVirtualHost(vh)
default:
// recurse
vertex.Visit(v.visit)
}
})
default:
// recurse
vertex.Visit(v.visit)
}
}

// 查找 Listener.VirtualHost.Route
func (v *routeVisitor) onVirtualHost(vh *dag.VirtualHost) {
var routes []*dag.Route

vh.Visit(func(v dag.Vertex) {
route, ok := v.(*dag.Route)
if !ok {
return
}
// 将所有的 route 收集到列表中
routes = append(routes, route)
})

if len(routes) == 0 {
return
}

// 定义了把 dag.Route 对象转换为 envoy_route_v3.Route 对象的方法
// 将两种数据格式进行转换
toEnvoyRoute := func(route *dag.Route) *envoy_route_v3.Route {
...
rt := &envoy_route_v3.Route{
Match: envoy_v3.RouteMatch(route),
Action: envoy_v3.RouteRoute(route),
}
...
return rt
}

// 对 route 排序
sortRoutes(routes)
// 这里生成的配置,存储到 routes 这个map中,key 是 ingress_http
v.routes[ENVOY_HTTP_LISTENER].VirtualHosts = append(v.routes[ENVOY_HTTP_LISTENER].VirtualHosts, toEnvoyVirtualHost(vh, routes, toEnvoyRoute))
}

GRPC 数据下发

从前面的分析我们知道,数据最终被转换为多个 ResourceCache 的列表,这些数据最终有两个用途:

  • 通过 GRPC 下发给数据面侧的 Envoy,最终达到流量路由的效果
  • 传给 SnapshotHander,用于生成快照
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
go复制代码func doServe(log logrus.FieldLogger, ctx *serveContext) error {
...
// 用于生成快照,这部分比较简单,不做介绍
snapshotHandler := xdscache.NewSnapshotHandler(resources, log.WithField("context", "snapshotHandler"))

...
// 注册服务到 GRPC
// 先通过 NewContourServer 生成一个 GRPC 服务,再注册到 grpcServer
switch ctx.Config.Server.XDSServerType {
// 根据启动参数的不同配置,实例化不同的对象

// serverType = "envoy"
case config.EnvoyServerType:
v3cache := contour_xds_v3.NewSnapshotCache(false, log)
snapshotHandler.AddSnapshotter(v3cache)
// 调用 NewServer 构造对象
// 这个方法是 go-control-plane 内置的方法
contour_xds_v3.RegisterServer(envoy_server_v3.NewServer(taskCtx, v3cache, contour_xds_v3.NewRequestLoggingCallbacks(log)), grpcServer)
// serverType = "contour",这个是默认配置
case config.ContourServerType:
// 调用 NewContourServer 构造对象
contour_xds_v3.RegisterServer(contour_xds_v3.NewContourServer(log, xdscache.ResourcesOf(resources)...), grpcServer)
...
}

下面分析数据经过 grpc 下发的流程,这部分主要是使用 go-control-plane sdk

注册 ADS、SDS、CDS、EDS、LDS、RDS 等都使用了 srv 对象,可以推断 Server 继承了 各种 XDS的接口

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
go复制代码// contour/internal/xds/v3/server.go

// 以下注册方法均调用的 go-control-plane sdk
// 包括注册 ADS、SDS、CDS、EDS、LDS、RDS
func RegisterServer(srv Server, g *grpc.Server) {
// register services
envoy_service_discovery_v3.RegisterAggregatedDiscoveryServiceServer(g, srv)
envoy_service_secret_v3.RegisterSecretDiscoveryServiceServer(g, srv)
envoy_service_cluster_v3.RegisterClusterDiscoveryServiceServer(g, srv)
envoy_service_endpoint_v3.RegisterEndpointDiscoveryServiceServer(g, srv)
envoy_service_listener_v3.RegisterListenerDiscoveryServiceServer(g, srv)
envoy_service_route_v3.RegisterRouteDiscoveryServiceServer(g, srv)
}

// 真正实例化的 Server 对象是 `contourServer`
// 将前面准备好的 ResourceCache 列表传进来
func NewContourServer(log logrus.FieldLogger, resources ...xds.Resource) Server {
c := contourServer{
FieldLogger: log,
resources: map[string]xds.Resource{},
}
// 按照 TypeURL 作为 key,保存资源
for i, r := range resources {
c.resources[r.TypeURL()] = resources[i]
}

return &c
}

contourServer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
go复制代码// contour/internal/xds/v3/contour.go
type contourServer struct {
// Since we only implement the streaming state of the world
// protocol, embed the default null implementations to handle
// the unimplemented gRPC endpoints.
envoy_service_discovery_v3.UnimplementedAggregatedDiscoveryServiceServer
envoy_service_secret_v3.UnimplementedSecretDiscoveryServiceServer
envoy_service_route_v3.UnimplementedRouteDiscoveryServiceServer
envoy_service_endpoint_v3.UnimplementedEndpointDiscoveryServiceServer
envoy_service_cluster_v3.UnimplementedClusterDiscoveryServiceServer
envoy_service_listener_v3.UnimplementedListenerDiscoveryServiceServer

logrus.FieldLogger
// 所有生成的 Envoy XDS 信息都传进来保存到这个字段
resources map[string]xds.Resource
connections xds.Counter
}

把 ResourceCache 传给 contourServer,contourServer作为GRPC的server注册到GRPC上,go-control-plane 框架封装了各个XDS数据传输的接口,contourServer 实现这些接口,从而实现数据下发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
go复制代码func (s *contourServer) StreamClusters(srv envoy_service_cluster_v3.ClusterDiscoveryService_StreamClustersServer) error {
return s.stream(srv)
}

func (s *contourServer) StreamEndpoints(srv envoy_service_endpoint_v3.EndpointDiscoveryService_StreamEndpointsServer) error {
return s.stream(srv)
}

func (s *contourServer) StreamListeners(srv envoy_service_listener_v3.ListenerDiscoveryService_StreamListenersServer) error {
return s.stream(srv)
}

func (s *contourServer) StreamRoutes(srv envoy_service_route_v3.RouteDiscoveryService_StreamRoutesServer) error {
return s.stream(srv)
}

func (s *contourServer) StreamSecrets(srv envoy_service_secret_v3.SecretDiscoveryService_StreamSecretsServer) error {
return s.stream(srv)
}

所有的 XDS Stream方法内部都调用了 s.stream 方法

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
go复制代码func (s *contourServer) stream(st grpcStream) error {
...
// 持续监听连接,并处理请求
// now stick in this loop until the client disconnects.
for {
// 获取 envoy 发送来的请求
req, err := st.Recv()
...
// 根据请求的 TypeURL,找到匹配的 ResourceCache
r, ok := s.resources[req.GetTypeUrl()]
...
// 注册监听器
r.Register(ch, last, req.ResourceNames...)
select {
case last = <-ch:

var resources []proto.Message
switch len(req.ResourceNames) {
// 如果没有指定资源名称,返回所有资源
case 0:
// 调用前面介绍过的 Contents 方法,从 ResourceCache 的 value 中取到 envoy 配置
resources = r.Contents()
// 如果指定资源名称,查找特定资源
default:
// resource hints supplied, return exactly those
resources = r.Query(req.ResourceNames)
}
// 将 ResourceCache 转换成 PB 数据
any := make([]*any.Any, 0, len(resources))
for _, r := range resources {
a, err := anypb.New(proto.MessageV2(r))
if err != nil {
return done(log, err)
}
any = append(any, a)
}
// 构造 grpc response 对象
resp := &envoy_service_discovery_v3.DiscoveryResponse{
VersionInfo: strconv.Itoa(last),
Resources: any,
TypeUrl: req.GetTypeUrl(),
Nonce: strconv.Itoa(last),
}

// 将 response 通过 grpc 发送给客户端 envoy
if err := st.Send(resp); err != nil {
return done(log, err)
}

case <-ctx.Done():
return done(log, ctx.Err())
}
}
}

框架server VS contourServer

contour会根据 ctx.Config.Server.XDSServerType 属性的值,初始化不同的 server。

  • envoy:初始化框架默认的server
  • contour:初始化 contourServer

Go-control-plane 提供的 pb 接口包括三类:

  • StreamXXX
  • DeltaXXX
  • FetchXXX

XXX 代表不同的 XDS,比如 StreamClusters。

contourServer 只实现了所有 XDS 的 Stream 相关方法,并没有实现 Delta,Fetch 相关方法

Envoy 连接到 Contour服务端

前面介绍了 contour 下发数据到 envoy 的流程,那最开始 envoy是如何连接上 contour的?

initContainer 生成配置文件

查看 envoy daemonset yaml文件,可以看到,内部配置了一个 initContainer,用于生成本地配置文件,并和 envoy 业务容器共享,envoy 启动时根据配置文件启动。启动命令中还指定了 envoy 连接的 grpc server 地址,即 contour的地址,这里配置的是 k8s svc 名称

1
2
3
4
5
6
7
8
9
10
11
yaml复制代码initContainers:
- args:
- bootstrap
- /config/envoy.json
- --xds-address=contour
- --xds-port=8001
- --xds-resource-version=v3
- --resources-dir=/config/resources
- --envoy-cafile=/certs/ca.crt
- --envoy-cert-file=/certs/tls.crt
- --envoy-key-file=/certs/tls.key

envoy启动配置文件内容

进入 envoy 容器可以查看生成的配置内容

1
bash复制代码kubectl -n projectcontour exec -it envoy-65qfq -c envoy -- cat /config/envoy.json
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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
yaml复制代码{
"static_resources": {
"clusters": [
{
"name": "contour",
"alt_stat_name": "projectcontour_contour_8001",
"type": "STRICT_DNS",
"connect_timeout": "5s",
"load_assignment": {
"cluster_name": "contour",
"endpoints": [
{
"lb_endpoints": [
{
"endpoint": {
"address": {
"socket_address": {
"address": "contour",
"port_value": 8001
}
}
}
}
]
}
]
},
"circuit_breakers": {
"thresholds": [
{
"priority": "HIGH",
"max_connections": 100000,
"max_pending_requests": 100000,
"max_requests": 60000000,
"max_retries": 50
},
{
"max_connections": 100000,
"max_pending_requests": 100000,
"max_requests": 60000000,
"max_retries": 50
}
]
},
"typed_extension_protocol_options": {
"envoy.extensions.upstreams.http.v3.HttpProtocolOptions": {
"@type": "type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions",
"explicit_http_config": {
"http2_protocol_options": {}
}
}
},
"transport_socket": {
"name": "envoy.transport_sockets.tls",
"typed_config": {
"@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext",
"common_tls_context": {
"tls_params": {
"tls_maximum_protocol_version": "TLSv1_3"
},
"tls_certificate_sds_secret_configs": [
{
"name": "contour_xds_tls_certificate",
"sds_config": {
"path": "/config/resources/sds/xds-tls-certificate.json",
"resource_api_version": "V3"
}
}
],
"validation_context_sds_secret_config": {
"name": "contour_xds_tls_validation_context",
"sds_config": {
"path": "/config/resources/sds/xds-validation-context.json",
"resource_api_version": "V3"
}
}
}
}
},
"upstream_connection_options": {
"tcp_keepalive": {
"keepalive_probes": 3,
"keepalive_time": 30,
"keepalive_interval": 5
}
}
},
{
"name": "envoy-admin",
"alt_stat_name": "projectcontour_envoy-admin_9001",
"type": "STATIC",
"connect_timeout": "0.250s",
"load_assignment": {
"cluster_name": "envoy-admin",
"endpoints": [
{
"lb_endpoints": [
{
"endpoint": {
"address": {
"pipe": {
"path": "/admin/admin.sock",
"mode": 420
}
}
}
}
]
}
]
}
}
]
},
"dynamic_resources": {
"lds_config": {
"api_config_source": {
"api_type": "GRPC",
"transport_api_version": "V3",
"grpc_services": [
{
"envoy_grpc": {
"cluster_name": "contour"
}
}
]
},
"resource_api_version": "V3"
},
"cds_config": {
"api_config_source": {
"api_type": "GRPC",
"transport_api_version": "V3",
"grpc_services": [
{
"envoy_grpc": {
"cluster_name": "contour"
}
}
]
},
"resource_api_version": "V3"
}
},
"admin": {
"access_log": [
{
"name": "envoy.access_loggers.file",
"typed_config": {
"@type": "type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog",
"path": "/dev/null"
}
}
],
"address": {
"pipe": {
"path": "/admin/admin.sock",
"mode": 420
}
}
}
}

总结

k8s本身没有内置 ingress 控制器,ingress 控制器呈现百花齐放的状态,参考官方文档:ingress 控制器。

不同的控制器,底层使用的网络代理不同,主要有:nginx、envoy、kong、haproxy等。envoy 作为云原生时代的网络代理,被越来越多的开源项目使用。比如:istio、contour、gloo、。这些项目的本质,都是简化 envoy 的配置,封装更简单的路由规则,通过内部的逻辑转换,变成envoy 的xds配置。

go-control-plane 框架封装了基于grpc下发xds配置,可以方便开发者开发自己的控制面。

本文转载自: 掘金

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

Mysql基础篇:必知必会(上)

发表于 2021-11-11

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

一、数据库操作

1.显示数据库

1
sql复制代码mysql> SHOW DATABASES;

2.创建数据库

CREATE DATABASE 数据库名 CHARSET='编码格式'

1
sql复制代码mysql> CREATE DATABASE create_test CHARSET = 'utf8';

3.使用数据库

1
sql复制代码mysql> USE create_test;

4.查看当前数据库

使用 SELECT DATABASE() 查看当前使用的数据库。

1
2
3
4
5
6
sql复制代码mysql> SELECT DATABASE();
+-------------+
| DATABASE() |
+-------------+
| create_test |
+-------------+

5.删除数据库

1
sql复制代码mysql> DROP DATABASE create_test;

二、表操作

创建表

代码格式:

1
2
3
4
5
6
sql复制代码CREATE TABLE  [IF NOT EXISTS] `表名` (
`字段名` 列类型 [属性] [索引] [注释],
`字段名` 列类型 [属性] [索引] [注释],
.......
`字段名` 列类型 [属性] [索引] [注释]
) [表类型] [字符集设置] [注释]

使用下面的语句创建示例中的 one_piece 表。

1
2
3
4
5
6
7
8
sql复制代码mysql> CREATE TABLE one_piece
-> (
-> id CHAR(10) NOT NULL COMMENT '海贼团id',
-> pirates CHAR(10) NOT NULL COMMENT '海贼团名称',
-> name CHAR(10) NOT NULL COMMENT '海贼名',
-> age INT(11) NOT NULL COMMENT '海贼年龄',
-> post VARCHAR(10) NULL COMMENT '海贼团职位'
-> );

注意:创建表时,指定的表名必须不存在,否则会出错。

更新表

1.添加列

在刚才创建的 one_piece 表中添加一列 bounty (赏金)。

1
2
sql复制代码mysql> ALTER TABLE one_piece
-> ADD bounty INT(15);

2.删除列
删除 bounty 列。

1
2
sql复制代码mysql> ALTER TABLE one_piece
-> DROP COLUMN bounty;

查看表结构

1
2
3
4
5
6
7
8
9
10
sql复制代码mysql> DESC one_piece;
+---------+-------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+---------+-------------+------+-----+---------+-------+
| id | char(10) | NO | | NULL | |
| pirates | char(10) | NO | | NULL | |
| name | char(10) | NO | | NULL | |
| age | int(11) | YES | | NULL | |
| post | varchar(10) | YES | | NULL | |
+---------+-------------+------+-----+---------+-------+

查看表详细信息

\G 后面不能加“ ; ”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
sql复制代码mysql> SHOW TABLE STATUS LIKE 'one_piece' \G
*************************** 1. row ***************************
Name: one_piece
Engine: InnoDB
Version: 10
Row_format: Dynamic
Rows: 0
Avg_row_length: 0
Data_length: 16384
Max_data_length: 0
Index_length: 0
Data_free: 0
Auto_increment: NULL
Create_time: 2021-11-08 15:20:13
Update_time: NULL
Check_time: NULL
Collation: utf8mb4_0900_ai_ci
Checksum: NULL
Create_options:
Comment:

重命名表

两种方法:

  • ALTER TABLE 表名 RENAME [TO | AS] 新表名;
  • RENAME TABLE 表名 TO 新表名;

用方法一将 Products 表更名为 new_Products ,再用方法二改回来。

1
2
3
4
sql复制代码-- 方法一
mysql> ALTER TABLE one_piece RENAME TO new_one_piece;
-- 方法二
mysql> RENAME TABLE new_one_piece TO one_piece;

删除表

DROP TABLE 表名

1
sql复制代码mysql> DROP TABLE one_piece;

注意:在该表与其他表有关联时,Mysql 会阻止该表的删除。

3. 查询

查询多列

同时输出 name, age 列。

1
2
sql复制代码mysql> SELECT name, age
-> FROM one_piece;

检索唯一值

使用 DISTINCT 关键字,查询字段 age 的唯一值。

1
2
sql复制代码mysql> SELECT DISTINCT age
-> FROM one_piece;

限制输出

在 Mysql 中使用 LIMIT 关键字限制输出的数据。LIMIT 有两种常见用法:

1
2
sql复制代码SELECT * FROM table  LIMIT [offset], rows    -- LIMIT 单独使用
SELECT * FROM table LIMIT rows OFFSET [offset] -- 配合 OFFSET 使用

offset:行开始的行的索引。0表示从第1行 开始显示(包括第1行),以此类推。

rows:数据显示的条数。

示例:

1
2
3
4
5
sql复制代码SELECT * FROM one_piece LIMIT 5;    -- 检索前5条数据
--相当于
SELECT * from one_piece LIMIT 0,5; -- 从第0行开始检索5条数据
--相当于
SELECT * FROM one_piece LIMIT 5 OFFSET 0; -- 从第0行开始检索5条数据,注意这里的LIMIT的5指代的是数量

注:如果表中数据不足,即LIMIT设定的数过大,则只会检索到最后一行。

注释

三种注释方式

1
2
3
sql复制代码-- 单行注释
# 单行注释
/* 多行注释 */

四、ORDER BY 排序

单列排序

使用 ORDER BY 子句。 ORDER BY 子句取一个或多个列的名字,据此对输出进行排序(默认升序)。

1
2
3
sql复制代码mysql> SELECT name, age
-> FROM one_piece
-> ORDER BY age;

注意:在指定一条 ORDER BY 子句时,应该保证它是 SELECT 语句中最后一条子句。

多列排序

1
2
3
sql复制代码mysql> SELECT A, B
-> FROM test
-> ORDER BY A, B;

在按多列排序时,仅在多个行具有相同的 A 值时 才按 B 进行排序。如果 A 列中所有的值都是 唯一的,则不会按 B 排序。

指定排序方向

ORDER 默认升序(从A到Z)排序。指定 DESC 关键字进行降序(从Z到 A)排序。

1
2
3
sql复制代码mysql> SELECT age
-> FROM one_piece
-> ORDER BY age DESC;

多列指定排序方向时,要使用逗号分隔。

1
2
3
sql复制代码mysql> SELECT name, age
-> FROM one_piece
-> ORDER BY name DESC, age;

五、WHERE 过滤数据

WHERE 子句操作符

操作符 说明 操作符 说明
= 等于 大于
<>、!= 不等于 >= 大于等于
< 小于 !> 不大于
<= 小于等于 BETWEEN 在两值之间(包含边界)
!< 不小于 IS NULL 是NULL值

范围值检查

使用 WHERE 关键字和 BETWEEN AND 进行范围值检查(前闭后闭)。

1
2
3
sql复制代码mysql> SELECT age
-> FROM one_piece
-> WHERE A BETWEEN 5 AND 10;

查询 字段 age 中 >=5 并且 <= 10 的数据。

空值检查

使用 WHERE 关键字和 IS NULL 进行范围值检查。如果没有 NULL 值就不返回数据。

1
2
3
sql复制代码mysql> SELECT name
-> FROM one_piece
-> WHERE name IS NULL;

WHERE 组合过滤

使用 AND 、OR 操作符给 WHERE 子句添加附加条件。 AND 的优先级比 OR 要高,优先级高低 () 、 AND 、 OR。在使用的过程中要注意各个优先级的影响。

1
2
3
4
sql复制代码mysql> SELECT name, age
-> FROM one_piece
-> WHERE(name = '索隆' OR name = '路飞')
-> AND age >= 18;

IN 操作符

IN 操作符用来指定条件范围,范围中的每个条件都可以进行匹配。(与 OR 的功能相同,但速度比 IN 慢)

1
2
3
sql复制代码mysql> SELECT name, age
-> FROM one_piece
-> WHERE name IN ('索隆', '路飞')

NOT 操作符

WHERE 子句中的 NOT 操作符有且只有一个功能,那就是否定其后所跟的任何条件。

1
2
3
sql复制代码mysql> SELECT name
-> FROM one_piece
-> WHERE name NOT IN ('索隆', '路飞')

七、通配符过滤

通配符搜索只能用于文本字段(字符串),非文本数据类型字段不能使用 通配符搜索。

在使用通配符过滤之前要先了解 LIKE , LIKE 操作符用于在 WHERE 子句中搜索列中的指定模式或取值。

% 通配符

% 表示任何字符出现任意次数。例如,为了找出所有以 路 开始的 name。

1
2
3
sql复制代码mysql> SELECT name, age
-> FROM one_piece
-> WHERE name LIKE '路%';

_ 通配符

通配符 _ 的用途与 % 一样也是匹配任意字符,但它只匹配单个字符,而不是多个字符。

1
2
3
sql复制代码mysql> SELECT name, age
-> FROM one_piece
-> WHERE name LIKE '乌_普';

这就是今天要分享的内容,微信搜 Python新视野,每天带你了解更多有用的知识。

本文转载自: 掘金

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

正确的Redis数据类型,让访问时间提高几十倍

发表于 2021-11-11

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

问题背景

接到了一个需求,是通过物品查询分类中涨价排行最高的N个子类,再查询每个子类对应的涨价排行最高的N个物品。由于数据结构是hash类型,全部物品、子类物品-价格、物品-子类、子类排行。
看到了物品对应子类集合和子类-价格集合,通过取交集,能将物品对应的子类排行查出来,尝试使用redis的zset来实现。

但是并发量上来之后,发现redis取交集这个操作很耗时。于是用hash结构保存数据,再用java进行排序,代码多了好几行,但是并发查询的时间大幅度缩短。

Redis的数据类型可参考: # 【简约入门】Redis的数据类型及常用命令https

原始代码

1
2
3
4
5
6
java复制代码	// 取goods与category的交集,结果放到result中		
redisTemplate.opsForZSet().intersectAndStore(goods,category,result);
int start = (pageNum-1) * pageSize;
// 分页获取集合
Set<ZSetOperations.TypedTuple<Object>> rangeWithScores = redisTemplate.opsForZSet().reverseRangeWithScores(result, start, pageSize - 1);
System.out.println(rangeWithScores);

优化后代码

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
java复制代码        // 查询goodId对应的类别
List<Object> categoryList = (List<Object>) redisHash.hGet("good:categorys",goodId);
// 查询上面查出类别的价格
List idxPriceList = redisHash.multiGet("category",categoryList);

List<Map> idxList = new ArrayList<>();
for (int i = 0;i<boardList.size();i++){
Map<String, Object> idxM = new HashMap<>();
idxM.put("k",boardList.get(i));
idxM.put("v",idxPriceList.get(i));
idxList.add(idxM);
}
// 种类按价格排序
idxList.sort((o1, o2) -> {
return (o1.get("v").toString()).compareTo(o2.get("v").toString());
});
System.out.println(idxList);
// 获取最高物品的代码
String code = idxList.get(0).get("k").toString();
// 种类下的物品
List<Object> sList = (List<Object>) redisHash.hGet("category:goods",code);

// 查找物品对应价格
List codeList = redisHash.multiGet("good:price",sList);

List<Map> codeIdxList = new ArrayList<>();
for (int i = 0;i<sList.size();i++){
Map<String, Object> idxM = new HashMap<>();
if(null != codeList.get(i)){
idxM.put("k",sList.get(i));
idxM.put("v",codeList.get(i));
codeIdxList.add(idxM);
}
}

// 按价格排序
codeIdxList.sort((o1, o2) -> {
return (o1.get("v").toString()).compareTo(o2.get("v").toString());
});
// 分页获取数据
codeIdxList = codeIdxList.stream().limit(pageSize).collect(Collectors.toList());

System.out.println(codeIdxList);

小结

当并发30、50的时候,使用redisTemplate.opsForZSet().intersectAndStore取交集的平均耗时是超过500ms的,并且尝试使用spring注解,加载多个redisTemplate来取交集,发现每个执行时间还是一样,如果只是取交集还可以,由于数据结构复杂需要多个交集操作,多个查询操作,使得整体的查询时间很长,2-3s。

改变java代码排序,取出最高的种类,再取出这个种类下的物品,效率更高,查询结果可控制在100ms左右。

本文转载自: 掘金

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

Go语言从INI配置文件中读取需要的值 题外话 你以为结束了

发表于 2021-11-11

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

生命不息,学习不止

题外话

下班到家,打开我这999纯金视网膜扫描认证大门,就看到我的宠物大萝卜向我奔来,大萝卜只是一直简简单单的纯白西伯利亚虎而已,哎,又是乏味的一天…… 养个宠物就是这么简简单单
话说今天是双十一,更冷了(一个人的寂寞,不知道是谁的错……)

在这里插入图片描述
废话不多说,上货
在这里插入图片描述

INI配置文件

.ini 文件是Initialization File的缩写,即初始化文件,是windows的系统配置文件所采用的存储格式,统管windows的各项配置,一般用户就用windows提供的各项图形化管理界面就可实现相同的配置了。但在某些情况,还是要直接编辑ini才方便,一般只有很熟悉windows才能去直接编辑。
举个例子
在这里插入图片描述
在这里插入图片描述

ini的文件格式就长这样,一般用于操作系统、虚幻游戏引擎、GIT 版本管理中,这种配置文件的文件扩展名为.ini。
INI 文件由多行文本组成,整个配置由[ ]拆分为多个“段”(section)。每个段中又以=分割为“键”和“值”。
INI 文件以;置于行首视为注释,注释后将不会被处理和识别
上图第一行就是注释

从 INI 文件中取值

我们创建一个 woner.ini 文件,将上方内容复制到该文件中。
内容如下

; for 16-bit app support
[fonts]
[extensions]
[mci extensions]
[files]
[Mail]
MAPI=1
CMCDLLNAME32=mapi32.dll
CMC=1
MAPIX=1
MAPIXVER=1.0.0.1
OLEMessaging=1

准备好 woner.ini 文件后,下面我们开始尝试读取该 INI 文件,并从文件中获取需要的数据,
我们的目的是获取CMCDLLNAME32=mapi32.dll这行的 mapi32.dll
完整的示例代码如下所示:

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
go复制代码package main

import (
"bufio"
"fmt"
"os"
"strings"
)

// 根据文件名,段名,键名获取ini的值
func getValue(filename, expectSection, expectKey string) string {
// 打开文件
file, err := os.Open(filename)
// 文件找不到,返回空
if err != nil {
return "123"
}
// 在函数结束时,关闭文件
defer file.Close()
// 使用读取器读取文件
reader := bufio.NewReader(file)
// 当前读取的段的名字
var sectionName string
for {
// 读取文件的一行
linestr, err := reader.ReadString('\n')
if err != nil {
break
}
// 切掉行的左右两边的空白字符
linestr = strings.TrimSpace(linestr)
// 忽略空行
if linestr == "" {
continue
}
// 忽略注释
if linestr[0] == ';' {
continue
}
// 行首和尾巴分别是方括号的,说明是段标记的起止符
if linestr[0] == '[' && linestr[len(linestr)-1] == ']' {
// 将段名取出
sectionName = linestr[1 : len(linestr)-1]
// 这个段是希望读取的
} else if sectionName == expectSection {
// 切开等号分割的键值对
pair := strings.Split(linestr, "=")
// 保证切开只有1个等号分割的简直情况
if len(pair) == 2 {
// 去掉键的多余空白字符
key := strings.TrimSpace(pair[0])
// 是期望的键
if key == expectKey {
// 返回去掉空白字符的值
return strings.TrimSpace(pair[1])
}
}
}
}
return "123123"
}

func main() {
fmt.Println(getValue("woner.ini", "Mail", "CMCDLLNAME32"))
}

保姆式注解让你一眼就能看懂

运行结果如下

在这里插入图片描述

getValue() 函数

本例并不是将整个 INI 文件读取保存后再获取需要的字段数据并返回,这里使用 getValue() 函数,每次从指定文件中找到需要的段(Section)及键(Key)对应的值。

getValue() 函数的声明如下:
func getValue(filename, expectSection, expectKey string) string

参数说明如下。
filename:INI 文件的文件名。
expectSection:期望读取的段。
expectKey:期望读取段中的键。

你以为结束了

ini文件内容
; for 16-bit app support
[fonts]
[extensions]
[mci extensions]
[files]
[Mail]
MAPI=1
CMCDLLNAME32=mapi32.dll
CMC=1
MAPIX=1
MAPIXVER=1.0.0.1
OLEMessaging=1

1
go复制代码fmt.Println(getValue("woner.ini", "Mail", "CMCDLLNAME32")) //调用函数

方法中第二个参数 “Mail” 对应的是ini文件中的[Mail]块,CMCDLLNAME32 表示 INI 文件中[Mail]块中键名,我们通过这个键名获取到我们想要的值。

小问题:如何通过键去修改值呢?

在这里插入图片描述

大家看完发现有什么错误,写在下面吧!跟我黑虎阿福比划比划!
在这里插入图片描述

本文转载自: 掘金

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

提升效率小工具,有几款你用过?

发表于 2021-11-11

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

前言

  作为一个开发者,正确的利用工具能够让你更快的适应工作内容和提高开发的工作效率,下面总结的14款工具都是作为一个职场老人在工作中最常使用的。

  获取方式: 进入作者主页查看

一 : smartgit/sourcetree

   免费的一款git图形化操作工具。现在公司中,代码管理平台基本都是使用git,想要拉取或者更新代码时,难免每次都需要通过命令行的方式,这样会显得比较繁琐。

   直接通过图形化操作工具则可以更加便捷,简单,解决冲突时也更直观,开发者必备工具之一,同时,在此处也推荐使用IDEA中自带的代码管理工具,也是非常简单,后面会专门写一遍文章来解释。

smartgit

sourcetree

二: everything

   一款号称速度最快的的文件搜索工具,通过everything能够快速帮你定位在系统中任何一个文件的位置,简单又方便。

everything

三: 软媒魔方 & 元气壁纸

   作为一个开发者,怎么能容忍界面杂乱无章呢!通过软媒魔方可以让你的界面变得井然有序,通过元气壁纸让你拥有高逼格的电脑壁纸,时刻彰显着程序员的”牛逼”!

image.png

image.png

四: utools

   我愿称之为最牛逼的工具,讲真,用它之后我斗图没输过,它里面包含了各种学习工具、斗图工具(表情包太多了)、谁用谁知道,这个工具可以让你分分种秒杀大多数老员工,最重要还是免费,太强了!!!

utools

五: 有道云笔记

   免费、一个简单便捷的在线笔记本,手机和电脑版的都有。开发者总离不开日报、周报,通过它可以编辑记录,每天下班直接粘贴复制即可完成日报、周报的编写,太便捷了!!!

有道云笔记

六: 向日葵/TeamView

   远程控制工具,可以在任何有网络的地方远程控制电脑,加班狗必备(哭…)

向日葵

TeamView

七: 护眼宝

   调节电脑亮度,保护眼睛,如果你对电脑的亮度不适应,可以动态调节,同时,它会定时提示你注意眼睛休息,工作之余,也要注意眼睛的保护哦。

在这里插入图片描述

八: MindMast

   思维导图绘制管理工具,开发过程中,遇到难题时,最重要的是要将自己的思路捋清楚,这样才能快速定位问题,MindMast支持各种类型的思维导图,能够帮助你快速滤清思路。

MindMast

九: sublime Text

   一款强调文本编辑器,工作中可能要遇到各种类型的文档操作,使用它能够轻松搞定(有人会问,为什么不推荐使用notepad++,我只能说,它不配,它的开发者是个台湾人,是一个反华言论者,凡妄图分裂国家的恶人坏人必将受到惩罚,维护中国领土完整,是我们每个人应尽的义务,况且能替代这款工具的太多,何需使用它)

sublime Text

十: RedisPlus

  Redis图形化管理工具,如果大家开发中有使用到Redis,通过它可以更加直观的管理到Redis数据库的数据,和navicat类似,但是它只针对Redis。

RedisPlus

十一: IDEA

  主要的开发工具,各种插件主题应有尽有,同时可以根据个人喜好进行个性化的设置,强烈推荐(很多人可能会疑问,为什么不推荐eclipse,说实话,作为一个从Eclipse转到IDEA的人来说,IDEA确实比Eclipse要更香,如果你作为一个职场新人,个人是推荐IDEA,IDEA同时支持一键配置Eclipse使用方式,非常方便)。

在这里插入图片描述

十二 : Navicat

  平常开发肯定离不开与数据库交互,Navicat是一款图形化数据库交互工具,使用它可以轻松与各种数据库进行连接,轻松使用。

在这里插入图片描述

十三 : PostMan

  作为一个自我要求极高的开发者(笑…),怎么可能运行代码没有调试过就提供给前端呢?要是轻易被前端发现缺陷那脸还要不要了,Postman作为一款接口调试工具,轻松实现接口调用模拟,前端想找茬也没有那么容易,同时可以进行请求分组归类,方便不同项目测试用例的管理。

在这里插入图片描述

十四 : Xshell

  与数据库交互完,总免不了与服务器交互,Xshell作为一个连接服务器的工具,使用方式非常简单,通过它可以轻松和服务器”交流”。

在这里插入图片描述

  讲完开发者工具后,再来着重推荐一下IDEA常用的插件,通过他们能够让你的代码更加规范,工作效率更高。

一: IDEA插件安装的步骤

  操作路径:File -> setting -> plugins -> 输入需要安装的插件名称 -> install -> apply -> 重启IDEA既可生效

IDEA插件安装的步骤

IDEA插件安装的步骤

一: Alibaba java coding guideline插件

   阿里巴巴代码规范插件,支持代码的静态检查,作为一个有高要求的开发者,垃圾代码我们要坚决抵制,通过这个插件,能够让你的代码更加规范,可以扫描出代码可能存在的缺陷,推荐必备!!

  它扫描的规则主要是根据阿里巴巴开发规范文档中的原则,对这个文档感兴趣可以关注后回复【JAVA开发文档】即可获取,强烈推荐,这个文档里面很多优化知识。

Alibaba java coding guideline使用步骤

扫描结果

二: Codota插件

   代码分析插件,可以帮你整行代码自动补全,基于海量Java代码和你的代码上下文给予整行的代码建议,帮助你更快地编写错误更少的代码,是提高工作效率的一个很好的插件。

Codota插件

三: GitToolBox插件

   代码提交记录工具。现在的项目代码基本都是托管到git仓库,一个项目可能存在多个开发者,通过GitToolBox插件,非常直观显示当前项目分支,及代码未更新,未提交数目。省去查询分支和最新代码等不必要的麻烦,可以让你知道每行代码的提交者是谁,是找背锅侠的必备工具啊(bushi)。

GitToolBox

四: JRebel插件

   通过它可以支持代码修改后自动重新加载编译,无需重启项目,提高效率工具之一。

在这里插入图片描述

五: Lombok插件

  支持常用实体类get/set等属性方法生成,这个插件仁者见仁智者见智,如果是多人合作的项目,则必须每个人都使用,否则程序将会报错,所以是否需要使用需要根据项目的情况而定。

Lombok插件

五: RestfulToolKit插件

  通过url快速定位到实现的代码,支持的是restful风格的url,非常便捷。

RestfulToolKit

六: Translation插件

  项目中遇到英文在所难免,Translation可以帮助你翻译代码中任何的注释,协助你读懂各种源码API注释,开发者必备插件之一。

Translation插件

翻译结果

七: Rainbow Brackets插件

  括号层级区分插件,在代码中,随着逻辑的复杂,各种括号嵌套也是常有的事,通过Rainbow Brackets插件,能够帮助你快速识别不同层级的代码,推荐使用。

  高亮效果(默认快捷键 mac : command+鼠标右键单击, windows : ctrl+鼠标右键单击)

效果

  工具的设计最终的作用都是为了简化我们的操作,提高工作效率。所以,如果工作中发现很多重复性的操作,那么肯定会有相应的工具去帮助转换,而不是通过人力一遍遍重复的操作。

  最后,感谢大家的阅读,如果觉的文章对你有帮助,不要忘记一键三连哦,你的支持是我创建更多优质文章的动力,非常感谢。

  工具查看博主主页获取,感谢您的支持!

本文转载自: 掘金

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

1…368369370…956

开发者博客

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