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

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


  • 首页

  • 归档

  • 搜索

❤️程序猿必备的数电知识,快来看看你掌握多少!❤️ 🔞零、前

发表于 2021-10-01

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

🔞零、前言

  1. 👻👻某些大牛曾说过:一个优秀的程序猿, 他不仅软件层面玩的好;而且硬件层面也玩的花。 👻👻

「华为天才少年」——稚晖君的光辉事迹想必大家有有所耳闻:

就比如前一段时间网上爆火他的一件神仙之作——耗时仅四个月,开发出的一款完美的自动驾驶自行车!

  需要注意的是:这个作品从构思到实物产出(CAD车体建模,载板PCB设计,手工焊接,总线控制,RPC通信,电机控制,传感器数据,ROS信息分发,SLAM建图,图像分类,PID,卡尔曼滤波数据融合,参数整定…)全都经他一人之手。这是货真价实的全栈工程师,一个人堪比一整个技术团队!!!

  1. 😬😬而且当年他去面试OPPO的时候拿到了两份Super Special的offer:一个是硬件岗、一个是算法岗。 可能有的小伙伴要说——“这是天才,我们常人无法企及!”😬😬

普通人经过不懈努力——最后变得不普通的例子也不计其数!

比如最鲜为人知的例子:前乒乓球奥运冠军邓亚萍,从小个子长得慢,胳膊短,为了能提高乒乓球技能加倍苦练,成为乒乓球大满贯得主。

  1. 😜😜“确实,不得不承认人与人之间是有差距的”,但是我想说的是:“我们可能做不到他做的那么好,但是最基本的硬件知识/数电知识我们还是要了解的。”——正所谓:知其然,更要知其所以然!😜😜

知道的越少,不知道的就越少;知道的越多,不知道的就越多!

  1. 🐌🐌我会尽量把技术文写的通俗易懂/生动有趣,保证每一个想要学习知识&&认认真真读完本文的读者们能够有所获,有所得。当然,如果你读完感觉本文写的还可以,真正学习到了东西,希望给我个「 赞 」 和 「 收藏,评论 」,这个对我很重要,谢谢了!🐌🐌

 

🔞一、常见进制介绍:

🎈(1)十进制:

在十进制数中,每一位有0-9十个数码,所以计数的基数是10。超过9的数必须用多位数表示,其中低位和相邻高位之间的关系是:逢十进一,故称为十进制。

①示例:

在这里插入图片描述

②可知一个任意多位的十进制数D均可展开为如下形式:

在这里插入图片描述

③拓展—— 若以N取代式中的10,即可得到多位任意进制(N进制)数展开式的普遍形式:

式中i的取值与十进制展开式的规定相同。

「变量详解」:

N称为计数的基数;

k为第i位的系数;

N称为第i位的权。

在这里插入图片描述

🎈(2)二进制:

目前在数字电路(我们生活在0和1组成的世界里!)中应用最广泛的是二进制。在二进制数中,每一位仅有0和1两个可能的数码,所以计数基数为2。其中低位和相邻高位之间的进位关系是:“逢二进一”, 故称为二进制。

①根据N进制数展开的普遍形式可得任意一个二进制数均可展开为:在这里插入图片描述

②并可利用上式计算出任一二进制数所表达的十进制数的大小:

在这里插入图片描述

上式中分别使用下脚注2和10表示括号里的数是二进制数和十进制数。/有时也用B( Bima-ry)和D( Decimal)代替2和10这两个脚注。

🎈(3)八进制:

八进制数的每一位有0~7 八个不同的数码,在二进制数中,计数的基数为8。其中低位和相邻高位之间的进位关系是:“逢八进一”, 故称为八进制。

①根据N进制数展开的普遍形式可得任意一个八进制数均可展开为:

在这里插入图片描述

②并可利用上式计算出任一八进制数所表达的十进制数的大小:

在这里插入图片描述

有时也用O(Oetal)代替下脚注8,表示八进制数。

🎈(4)十六进制:

十六进制数的每一位有十六个不同的数码,分别用0~9.A(10)、B(11) .C(12)、D(13)、E(14)、P(15)表示。在十六进制数中,计数的基数为16。其中低位和相邻高位之间的进位关系是:“逢十六进一”, 故称为十六进制。

①根据N进制数展开的普遍形式可得任意一个十六进制数均可展开为:

在这里插入图片描述

②并可利用上式计算出任一十六进制数所表达的十进制数的大小:

在这里插入图片描述

式中的下脚注16表示括号里的数是十六进制数,有时也用H( Hexadecimal)代替这个脚注,0X表示前缀。

🎈(5)不同进制数的对照表:

在这里插入图片描述

小拓展:

  1. 一位八进制可以表示三位二进制数:
    解读:
      因为三位二进制最小是000b,最大是111b,其范围恰好在0-7,构成了八进制一位。
  2. 一位十六进制可以表示为四位二进制:
    解读:
      十六进制数的进率是16,二进制数的进率是2,且16=2^4,说明二进制数连续进位4次,等效于16进制数进1位。这么说可能不好理解,那么举个例子吧,比如15+1=16,用二进制表示就是1111+1=10000,用十六进制表示就是F+1=10。这也就说明了一位十六进制数对应四位二进制数了

🔞二、不同进制间的转换:

🎈(1)八进制,二进制,十六进制转换为十进制:

都可根据上述介绍十进制的时候讲解的——多位任意进制数展开式的普遍形式进行转换,即按位权展开式。

 

🎈(2)十进制转换为二进制,八进制,十六进制:

十进制整数转换R进制(R可以是任何整数,比如2,8,16)整数,方法就是除R取余。

①十进制转换为二进制:

十进制整数转换为二进制方法:除二取余,从下往上倒序排序!

在这里插入图片描述

十进制小数转换为二进制方法:乘二取整,从上向下顺序排序!

在这里插入图片描述

②十进制转换为十六进制:

十进制数为整数时,除16取余;
  十进制数为小数时,乘16取整。
    (具体步骤拟同十进制转换为二进制!)

③十进制转换为八进制:

十进制为整数时,除八取余;
  十进制为小数时,乘八取整。
    (具体步骤拟同十进制转换为二进制!)

 

🎈(3)二进制转换为十六进制:

只要从低位到高位将整数部分每4位二进制数分为一组并代之以等值的十六进制数,同时从高位到低位将小数部分的每4位数分为一组并代之以等值的十六进制数,即可得到对应的十六进制数。

在这里插入图片描述

注意:

| | 若二进制数整数部分最高一组不足4位时,用0补足4位; | | — | | 小数部分最低一组不足 4位时,也需用0补足4位。 | | — |   🎈(4)二进制转换为八进制:   只要将二进制数的整数部分从低位到高位每3位分为一组并代之以等值的八进制数,同时将小数部分从高位到低位每3位分为一组并代之以等值的八进制数,即可得到对应的八进制数。 在这里插入图片描述 注意: | 二进制数最高一组不足3位或小数部分最低一组不足3位时,仍需以0补足三位! | | — | 🎈(5)十六进制转换为二进制:   转换时只需将十六进制数的每一位用等值的4位二进制数代替即可! 在这里插入图片描述  🎈(6)八进制转换为二进制:   转换时只需将八进制数的每一位用等值的3位二进制数代替即可! 在这里插入图片描述   🎈(7)八进制与十六进制之间的转换:   第一种:先转成二进制然后再相互转换;   第二种:先转成十进制然后再相互转换! | 建议:先将八进制转换为对应的二进制,再将二进制转换为十六进制! | | — | — 三、In The End! 请添加图片描述 | 从现在做起,坚持下去,一天进步一小点,不久的将来,你会感谢曾经努力的你! | | — |  本博主会持续更新爬虫基础分栏及爬虫实战分栏,认真仔细看完本文的小伙伴们,可以点赞收藏并评论出你们的读后感。并可关注本博主,在今后的日子里阅读更多技术文! 如有错误或者言语不恰当的地方可在评论区指出,谢谢! 如转载此文请联系我征得本人同意,并标注出处及本博主名,谢谢!! |
| — | — | — | — | — | — |

本文转载自: 掘金

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

【良心推荐】手摸手带你做Springboot + mybat

发表于 2021-10-01

前情提要(都是干货)

项目从最初的SpringBoot + mybatis 进行整合,慢慢增加shiro、redis框架
由浅入深的进行整合,一点一点讲解,内容可能稍微啰嗦,慢慢学还是有收获的。项目截图在下面

  • 第一章: 搭建环境以及整合mybatis plus
  • 第二章: 登陆和用户管理
  • 第三章: 整合shiro 设置用户密码加密
  • 第四章: 整合redis和动态权限以及动态目录
  • 第五章: 整合knife4j 生成好看的开发文档
  • 第六章: Linux环境搭建与发布上线

工具

  • idea
  • JDK8

后端使用技术

在这里插入图片描述

前端使用技术

在这里插入图片描述

项目截图

在这里插入图片描述
在这里插入图片描述
请添加图片描述

结构

在这里插入图片描述

开始正文

创建一个Maven项目

在这里插入图片描述
修改pom文件
添加如下

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
xml复制代码	<packaging>jar</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.4</version>
</parent>
<properties>
<lombok.version>1.18.16</lombok.version>
<mybatis.version>2.1.3</mybatis.version>
<fastjson.version>1.2.74</fastjson.version>
<plus.version>3.3.2</plus.version>
<druid.version>1.1.13</druid.version>
<pagehelper.version>1.2.13</pagehelper.version>
<hutool-version>5.7.13</hutool-version>
</properties>

<!--放入一些后面项目都会使用的东西
比如:springboot spring基本架子
mybatis 数据库操作,编写sql
mybatis plus 对mybatis进行增强,免写简单sql
pageHelper 分页插件
devtools 代码热更新
lombok 用于get set,免手动操作
mysql 数据库连接驱动
json json转换
thymeleaf 前端模版引擎
druid 连接池
-->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${plus.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.version}</version>
</dependency>
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>${pagehelper.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool-version}</version>
</dependency>
</dependencies>

在demo中添加以下文件

ApplicationBoot在macro包下

在这里插入图片描述

controller代码

1
2
3
4
5
6
7
8
9
10
11
java复制代码import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class IndexController {

@GetMapping(value = {"index","","/"})
public String index(){
return "你好,我是无术同学!";
}
}

ApplicationBoot代码

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ApplicationBoot {

public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(ApplicationBoot.class);
//从yml中读取项目的启动端口,若是填写项目名称,也可获取
String port = context.getEnvironment().getProperty("server.port");
System.out.println("访问地址: http://127.0.0.1:" + port);
}
}

application.yml (目前只是在本地新建一个csdn的库,没添加表)

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
yml复制代码server:
port: 8086
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/csdn?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
username: root
password: 123456
initial-size: 10
max-active: 100
min-idle: 10
max-wait: 60000
pool-prepared-statements: true
max-pool-prepared-statement-per-connection-size: 20
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
test-while-idle: true
test-on-borrow: false
test-on-return: false
stat-view-servlet:
enabled: true
url-pattern: /druid/*
filter:
stat:
log-slow-sql: true
slow-sql-millis: 1000
merge-sql: false
wall:
config:
multi-statement-allow: true

启动运行,看一下项目是否成功
访问地址: http://127.0.0.1:8086/ 看到一下文字则是项目运行成功!

整合框架

上面的pom文件中已经加入了mysql驱动和mybatis、mybatis plus框架
整体目录:

1
2
3
4
5
6
7
8
9
10
11
12
13
text复制代码  ─src
└─main
├─java
│ └─com
│ └─macro
│ ├─controller 存放controller
│ ├─dao 数据库操作接口
│ ├─entity 存放实体类
│ ├─service service接口
│ │ └─impl service接口的实现类
│ └─utils 存放工具类
└─resources
└─mapper 存放xml,映射dao层

数据库新增系统管理人员sys_user表,字段如下:
在这里插入图片描述
id键别忘加自动递增、主键唯一、不为null

配置文件和添加文件

完整yml文件

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
yml复制代码server:
port: 8086
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/csdn?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
username: root
password: 123456
initial-size: 10
max-active: 100
min-idle: 10
max-wait: 60000
pool-prepared-statements: true
max-pool-prepared-statement-per-connection-size: 20
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
test-while-idle: true
test-on-borrow: false
test-on-return: false
stat-view-servlet:
enabled: true
url-pattern: /druid/*
filter:
stat:
log-slow-sql: true
slow-sql-millis: 1000
merge-sql: false
wall:
config:
multi-statement-allow: true
# mybatis映射的xml文件
mybatis:
mapper-locations: classpath:mapper/*.xml


# springboot 中打印,不需要添加新依赖
logging:
level:
com.macro.dao : debug

修改ApplicationBoot

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication
@MapperScan("com.macro.dao")
public class ApplicationBoot {

public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(ApplicationBoot.class);
//从yml中读取项目的启动端口,若是填写项目名称,也可获取
String port = context.getEnvironment().getProperty("server.port");
System.out.println("访问地址: http://127.0.0.1:" + port);
}
}

新增以下类
在这里插入图片描述
UserEntity类

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
java复制代码import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;

@Data
//定义插入哪张表
@TableName("sys_user")
@NoArgsConstructor
@AllArgsConstructor
public class UserEntity implements Serializable {

@TableId(value = "id",type = IdType.AUTO)
private Integer id;

//N大写会被mybatis plus转化为_n
private String niceName;

private String username;

private String password;

private String avatar;

private Integer sex;

}

UserDao 接口继承BaseMapper

1
2
3
4
5
6
java复制代码import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.macro.entity.UserEntity;

public interface UserDao extends BaseMapper<UserEntity> {

}

UserService接口继承IService

1
2
3
4
5
java复制代码import com.baomidou.mybatisplus.extension.service.IService;
import com.macro.entity.UserEntity;

public interface UserService extends IService<UserEntity> {
}

UserService接口的实现类UserServiceImpl

1
2
3
4
5
6
7
8
9
10
java复制代码import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.macro.dao.UserDao;
import com.macro.entity.UserEntity;
import com.macro.service.UserService;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl extends ServiceImpl<UserDao, UserEntity> implements UserService {

}

Result工具类

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复制代码@Data
public class Result {
private Integer code;
private String msg;
private Object data;

Result(Integer code, String msg, Object data){
this.code = code;
this.msg = msg;
this.data = data;
}

public static Result success(Object data){
return new Result(200,"成功",data);
}
public static Result success(){
return new Result(200,"成功",null);
}

public static Result error(Integer code, String msg){
return new Result(code,msg,null);
}

public static Result error(String msg){
return new Result(400,msg,null);
}
}

数据库中添加一条数据

IndexController添加一条查询方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码import com.macro.entity.UserEntity;
import com.macro.service.UserService;
import com.macro.utils.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class IndexController {

@Autowired
private UserService userService;

@GetMapping("info/{id}")
public Result info(@PathVariable Integer id){
UserEntity entity = userService.getById(id);
return Result.success(entity);
}




}

启动访问 http://127.0.0.1:8086/info/1
1就是数据的id值

在这里插入图片描述

总结

1
2
3
4
5
6
7
8
9
10
11
text复制代码第一篇项目的架子已经搭建起来了

好多东西没有都没有细细说明

像 @RestController 注解,就是@Controller ,@ResponseBody 的组合体,返回json格式数据就不用加@ResponseBody了

但是跳转页面不可使用此注解,谨记!

还有service接口中的IService以及service的实现类中集成的ServiceImpl 都是mybatis plus 提供的类,包含一些增删改查方法

省去一部分时间写代码,很实用,还有UserDao集成的接口也是mybatis plus 提供的类

源码

在公众号内发送后台即可获取源码和数据库

本文转载自: 掘金

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

【mongo 系列】mongodb 学习一,基本 nosql

发表于 2021-10-01

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

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

咱们工作或者学习的过程中,接到一个需求,或者学习一个技能的时候,我们是如何去学习的呢?

我想大概分成如下几步吧:

  • 了解背景,了解这个技术或者需求的背景,特性,定律等等
  • 对比学习,进行同类事物对比
  • 关联学习,关联已知的知识进行学习

一起来看看 NOSQL 是什么

这里来推荐一个看数据排名的地址:

DB-Engines

这里可以看到各种类型的数据库排名,数据库选型的时候这个网址就很香了

NOSQL 是什么

咱们先来列举一下传统型数据库的特点:

  • 结构化
  • 二维表
  • E-R关系(实体-关系模型)
  • sql 标准化
  • 支持事务(ACID)
  • 锁
  • 索引

sql ,是结构化查询语言,泛指关系型数据库

nosql (not noly sql),不仅仅是 sql ,这泛指不提供 sql 功能的非关系型数据库

它不遵循 sql 的标准,acid 特性,表结构等特性。

最开始 nosql 实际上是 not sql ,后面慢慢发展成 not only sql

简述 nosql 的发展历史:

列式存储 – 键值对存储 – 文档存储 – 图形存储

为什么需要 NOSQL?

大致列举如下几点:

  • 由于现代网络的发展,大多是超大规模高并发的 web 2.0 动态网站
  • 对于大量数据,关系型数据库已经遇到瓶颈,性能方面和扩展性方面的瓶颈
  • 如何解决大规模数据集合,多重数据种类带来的挑战,这就需要 nosql 来处理了
  • mysql 等关系型数据库应用在大数据上面,显然是一个难题了

常用的四大类 NOSQL 数据库的优缺点对比

分类 优势 劣势 场景 代表
键值对 查找速度快 数据无结构化,通常只是用来作为字符串或者二进制 内容缓存,主要用于处理大量数据的高频访问负载 reids
列式存储 查找速度快,支持分布式横向扩展,数据压缩率搞 功能相对受限 用于分布式文件系统 HBase
文档存储 数据结构要求不严格,表结构可变 查询性能不高,缺乏统一的查询语法 用于web 应用等 MongoDB
图形数据库 可以利用图结构相关等算法 需要对整个图做计算,不利于图数据的分布式存储 用于社交网络,推荐系统,意向图,兴趣图,关系图等等 Neo4J

我们可以知道 es 也是 文档存储的 nosql ,那么 es 和 mongodb 有什么异同的呢?

mongodb 和 elasticsearch 相同点:

  • 文档结构化
  • 都有自定义的一套操作语法
  • 有全文检索 (es 更多是用在搜索引擎上面)
  • 索引

不同点:

  • mongodb 有 MapReduce , es 没有
  • 全文检索实现的方式不一样

nosql 和 关系型数据库对比

特点 NoSQL 关系型数据库
数据一致性上面 运用CAP定理,保证最终一致性,非ACID属性 严格的一致性,ACID
数据表的形式 键-值对存储,列存储,文档存储,图形数据库 二维表,数据和关系都存储在单独的表中
是否结构化 非结构化的、半结构化的,没有声明性查询语言 高度组织化结构化数据,结构化查询语言 sql
事务方面 属于 弱 事务 基础事务
Join 方面 弱,没有预定义的模式 强,数据操作语言,数据定义语言
成本代价 低 高(硬件方面和软件方面)
扩展性方面 强,高性能,高可用,可伸缩性强 弱

什么是 mongodb ?

mongodb 是基于 C++ 开发的 NOSQL 开源文档数据库 ,是最像关系型数据库的 nosql,功能也是最丰富的 nosql

它具有所以的可伸缩性,灵活性,高性能,高扩展性的优势,大致有如下特性:

  • 面向集合文档的存储,存储 Bson (json的扩展)
  • 格式自由,数据格式自由,生产环境下面修改数据表结构对程序没有影响
  • 查询语句强大,面向对象查询语句,覆盖了 sql 语言的能力
  • 完善的索引支持,支持查询计划
  • 支持复制和自动故障转移 (这里有点像 redis)
  • 支持二进制数据和大型对象文件的高效存储
  • 使用分片集群提升系统的扩展性
  • 使用内存映射存储引擎,把磁盘的 IO 操作转换成内存的操作

什么业务场景需要使用 mongodb ?

mongodb 数据库,并不是说适合每一种场景的,咱们需要人尽其才,物尽其用,技术选型,我们也是要选择最合适的技术来解决实际的业务问题或者是场景问题

如下的场景就适合使用 mongodb:

  • 不需要事务及复杂的 join 支持
  • 要应对 2k - 3k 以上的读写 QPS 的时候
  • 存储的数据达到 TB 或者 PB
  • 新的服务,数据结构会变,类型会变,模型也会变的情况
  • 要求存储的数据不丢失
  • 要求 4 个 9 的高可用
  • 需要服务水平扩展,持续迭代的
  • 大量的地理位置查询,文本查询的

实际过程中,咱们会在哪些成场景使用到 mongodb 呢?

mongodb 应用的场景可以说是非常的多,大致有游戏,物流,内容管理,物联网,电商,社交,视频直播等等

如物流场景:

mongodb 存储订单信息,订单在运送的过程中,订单信息会不断的更新,这个时候使用mongodb 内嵌的数据形式来存储就非常方便,一次查询就可以将所有的物流信息全部取出来

再例如社交场景:

mongodb 存储用户信息,存储用户发表的朋友圈信息,那么就可以通过地理位置索引到附近的人,地点,以及相关的配套功能

不适合使用 mongodb 的场景

不适合使用 mongodb 的场景,即是 mongodb 自身的劣势场景,例如:

  • 高度的事务性系统,例如做银行等金融业务的,要求高度的一致性,mongodb 就不合适
  • 使用 sql 方便,数据结构相对固定的场景,这个使用使用 sql 标准成本会更低

最后贴一下 mongodb 的官方文档地址,学习任何一门技术,都是看官网的一手资料才是正确的

欢迎点赞,关注,收藏

朋友们,你的支持和鼓励,是我坚持分享,提高质量的动力

好了,本次就到这里

技术是开放的,我们的心态,更应是开放的。拥抱变化,向阳而生,努力向前行。

我是小魔童哪吒,欢迎点赞关注收藏,下次见~

本文转载自: 掘金

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

❤️ Go 有别于其他语言的九个特性 ❤️

发表于 2021-10-01

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

随着编程语言的发展,Go 还很年轻。它于 2009 年 11 月 10 日首次发布。其创建者Robert Griesemer
Rob Pike 和 Ken Thompson在 Google 工作,在那里大规模扩展的挑战激励他们将 Go 设计为一种快速有效的编程解决方案,用于具有大型代码库、管理由多个开发人员,具有严格的性能要求,并跨越多个网络和处理核心。
Go 的创始人在创建他们的新语言时也借此机会学习了其他编程语言的优点、缺点和漏洞。结果是一种干净、清晰和实用的语言,具有相对较少的命令和功能集。

在本文中,今天这篇文章将给大家介绍一下 Go 与其他语言不同的 9 个特性。

  1. Go 总是在构建中包含二进制文件

Go 运行时提供内存分配、垃圾收集、并发支持和网络等服务。它被编译到每个 Go 二进制文件中。这与许多其他语言不同,其中许多语言使用需要与程序一起安装才能正常工作的虚拟机。

将运行时直接包含在二进制文件中使得分发和运行 Go 程序变得非常容易,并避免了运行时和程序之间的不兼容问题。Python、Ruby 和 JavaScript 等语言的虚拟机也没有针对垃圾收集和内存分配进行优化,这解释了 Go 相对于其他类似语言的优越速度。例如,Go 将尽可能多的存储在堆栈中,其中数据按顺序排列以便比堆更快地访问。稍后会详细介绍。

关于 Go 的静态二进制文件的最后一件事是,因为不需要运行外部依赖项,所以它们启动得非常快。如果您使用Google App Engine 之类的服务,这是一种在 Google Cloud 上运行的平台即服务,它可以将您的应用程序缩减到零实例以节省云成本,这将非常有用。当收到新请求时,App Engine 可以在眨眼间启动 Go 程序的一个实例。在 Python 或 Node 中的相同体验通常会导致 3-5 秒(或更长时间)的等待,因为所需的虚拟环境也会与新实例一起启动。

  1. Go 没有针对程序依赖的集中托管服务

为了访问已发布的 Go 程序,开发人员不依赖集中托管的服务,例如Java 的Maven Central或JavaScript的NPM注册表。相反,项目通过其源代码存储库(最常见的是 Github)共享。在go install命令行允许以这种方式下载库。
为什么我喜欢这个功能?我一直认为像 Maven Central、PIP 和 NPM 这样的集中托管的依赖服务有点令人生畏的黑盒子,也许可以抽象出下载和安装依赖项的麻烦,但不可避免地会在依赖项错误时引发可怕的心跳停止发生。

此外,将的的模块提供给其他人就像将其放入版本控制系统一样简单,这是分发程序的一种非常简单的方式。

  1. Go 是按值调用的

在 Go 中,当你提供一个原始值(数字、布尔值或字符串)或一个结构体(类对象的粗略等价物)作为函数的参数时,Go 总是会复制变量的值。

在 Java、Python 和 JavaScript 等许多其他语言中,原语是按值传递的,但对象(类实例)是按引用传递的,这意味着接收函数实际上接收的是指向原始对象的指针,而不是其副本。在接收函数中对对象所做的任何更改都会反映在原始对象中。

在 Go 中,结构体和原语默认按值传递,可以选择传递指针,通过使用星号运算符:

1
2
3
4
5
6
go复制代码// 按值传递
func MakeNewFoo(f Foo ) (Foo, error) {
f.Field1 = "New val"
f.Field2 = f.Field2 + 1
return f, nil
}

上述函数接收 Foo 的副本并返回一个新的 Foo 对象。

1
2
3
4
5
6
go复制代码// 通过引用传递
func MutateFoo(f *Foo ) error {
f.Field1 = "New val"
f.Field2 = 2
return nil
}

上面的函数接收一个指向 Foo 的指针并改变原始对象。

按值调用与按引用调用的这种明显区别使您的意图显而易见,并降低了调用函数无意中改变传入对象的可能性(当它不应该发生时(许多初学者开发人员很难做到这一点)握紧)。

正如麻省理工总结的:“可变性使得理解你的程序在做什么变得更加困难,并且更难以执行契约”

此外,按值调用显着减少了垃圾收集器的工作,这意味着应用程序更快、内存效率更高。这篇文章得出的结论是,指针追踪(从堆中检索指针值)比从连续堆栈中检索值慢 10 到 20 倍。要记住的一个很好的经验法则是:从内存中读取的最快方法是顺序读取,这意味着将随机存储在 RAM 中的指针数量减少到最少。

  1. ‘defer’ 关键字

在NodeJS 中,在我开始使用knex.js之前,我会通过创建一个数据库池来手动管理我的代码中的数据库连接,然后在每个函数中从池中打开一个新连接,一旦所需的数据库 CRUD 功能已完成。

这有点像维护噩梦,因为如果我没有在每个函数结束时释放连接,未释放的数据库连接的数量会慢慢增长,直到池中没有更多可用连接,然后中断应用程序。

现实情况是,程序经常需要释放、清理和拆除资源、文件、连接等,因此 Go 引入了defer关键字作为管理这些的有效方式。

任何以defer开头的语句都会延迟对它的调用,直到周围的函数退出。这意味着您可以将清理/拆卸代码放在函数的顶部(很明显),知道一旦函数完成它就会如此。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
go复制代码func main() {                        
if len(os.Args) < 2 {
log.Fatal("no file specified")
}
f, err := os.Open(os.Args[1])
if err != nil {
log.Fatal(err)
}
defer f.Close()
data := make([]byte, 2048)
for {
count, err := f.Read(data)
os.Stdout.Write(data[:count])
if err != nil {
if err != io.EOF {
log.Fatal(err)
}
break
}
}
}

在上面的例子中,文件关闭方法被推迟了。我喜欢这种在函数顶部声明你的内务处理意图的模式,然后忘记它,知道一旦函数退出它就会完成它的工作。

  1. Go 采用了函数式编程的最佳特性

函数式编程是一种高效且富有创造性的范式,幸运的是 Go 采用了函数式编程的最佳特性。在Go中:

  • 函数是值,这意味着它们可以作为值添加到映射中,作为参数传递给其他函数,设置为变量,并从函数返回(称为“高阶函数”,在 Go 中经常使用装饰器创建中间件图案)。
  • 可以创建和自动调用匿名函数。
  • 在其他函数内声明的函数允许闭包(在函数内声明的函数能够访问和修改在外部函数中声明的变量)。在惯用的 Go 中,闭包被广泛使用来限制函数的范围,并设置函数然后在其逻辑中使用的状态。
1
2
3
4
5
6
7
8
9
10
11
12
13
go复制代码func StartTimer (name string) func(){
t := time.Now()
log.Println(name, "started")
return func() {
d := time.Now().Sub(t)
log.Println(name, "took", d)
}
}
func RunTimer() {
stop := StartTimer("My timer")
defer stop()
time.Sleep(1 * time.Second)
}

上面是一个闭包的例子。’StartTimer’ 函数返回一个新函数,它通过闭包可以访问在其出生范围内设置的 ‘t’ 值。然后,此函数可以将当前时间与“t”的值进行比较,从而创建一个有用的计时器。感谢Mat Ryer的这个例子。

  1. Go 有隐式接口

任何阅读过有关SOLID编码和设计模式的文献的人都可能听说过“优先组合胜过继承”的口头禅。简而言之,这表明您应该将业务逻辑分解为不同的接口,而不是依赖于来自父类的属性和逻辑的分层继承。

另一个流行的方法是“为接口编程,而不是实现”: API 应该只发布其预期行为的契约(其方法签名),而不是有关如何实现该行为的详细信息。

这两者都表明接口在现代编程中的重要性。

因此,Go 支持接口也就不足为奇了。事实上,接口是 Go 中唯一的抽象类型。

然而,与其他语言不同,Go 中的接口不是显式实现的,而是隐式实现的。具体类型不声明它实现了接口。相反,如果为该具体类型设置的方法集包含底层接口的所有方法集,则Go 认为该对象实现了 interface。

这种隐式接口实现(正式称为结构类型)允许 Go 强制执行类型安全和解耦,保持动态语言中表现出的大部分灵活性。

相比之下,显式接口将客户端和实现绑定在一起,例如,在 Java 中替换依赖项比在 Go 中困难得多。

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复制代码// 这是一个接口声明(称为Logic)
type Logic interface {
Process (data string) string
}

type LogicProvider struct {}
// 这是 LogicProvider 上名为“Process”的方法 struct
func (lp LogicProvider) Process (data string) string {
// 业务逻辑
}
// 这是具有 Logic 接口作为属性的客户端结构
type Client struct {
L Logic
}
func(c Client) Program() {
// 从某处获取数据
cLProcess(data)
}
func main() {
c := Client {
L: LogicProvider{},
}
c.Program()
}

LogicProvider 中没有任何声明表示它符合Logic接口。这意味着客户端将来可以轻松替换其逻辑提供程序,只要该逻辑提供程序包含底层接口 ( Logic ) 的所有方法集。

7.错误处理

Go 中的错误处理方式与其他语言大不相同。简而言之,Go 通过返回一个 error 类型的值作为函数的最后一个返回值来处理错误。

当函数按预期执行时,错误参数返回nil,否则返回错误值。调用函数然后检查错误返回值,并处理错误,或抛出自己的错误。

1
2
3
4
5
6
7
8
9
go复制代码// 函数返回一个整数和一个错误
func calculateRemainder(numerator int, denominator int) ( int, error ) {
//
if denominator == 0 {
return 9, errors.New("denominator is 0"
}
// 没有错误返回
return numerator / denominator, nil
}

Go 以这种方式运行是有原因的:它迫使编码人员考虑异常并正确处理它们。传统的 try-catch 异常还会在代码中添加至少一个新的代码路径,并以难以遵循的方式缩进代码。Go 更喜欢将“快乐路径”视为非缩进代码,在“快乐路径”完成之前识别并返回任何错误。

8.并发

可以说是 Go 最著名的特性,并发允许处理在机器或服务器上的可用内核数量上并行运行。当单独的进程不相互依赖(不需要顺序运行)并且时间性能至关重要时,并发性最有意义。这通常是 I/O 要求的情况,其中读取或写入磁盘或网络的速度比除最复杂的内存中进程之外的所有进程慢几个数量级。
函数调用前的“ go ”关键字将同时运行该函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
go复制代码func process(val int) int { 
// 用 val 做一些事情
}
// 对于 'in' 中的每个值,同时运行 process 函数,
// 并将 process 的结果读取到 'out'
func runConcurrently(in <-chan int, out chan<- int){
go func() {
for val := range in {
result := process(val)
out <- result
}
}
}

Go 中的并发是一项深入且相当高级的功能,但在有意义的地方,它提供了一种有效的方法来确保程序的最佳性能。

  1. Go 标准库

Go 有一个“包含电池”的理念,现代编程语言的许多要求都被纳入标准库,这使程序员的生活变得更加简单。
如前所述,Go 是一种相对年轻的语言,这意味着标准库中可以满足现代应用程序的许多问题/要求。

一方面,Go 为网络(特别是 HTTP/2)和文件管理提供了世界一流的支持。它还提供原生 JSON 编码和解码。因此,设置服务器来处理 HTTP 请求并返回响应(JSON 或其他)非常简单,这解释了 Go 在基于 REST 的 HTTP Web 服务开发中的流行。

正如Mat Ryer还指出的那样,标准库是开源的,是学习 Go 最佳实践的绝佳方式。


🛬 wuhu ! 起飞 !

我已经写了很长一段时间的技术博客,并且主要通过掘金发表,这是我的一篇 Go 有别于其他语言的九个特性教程。我喜欢通过文章分享技术与快乐。你可以访问我的博客: juejin.cn/user/204034… 以了解更多信息。希望你们会喜欢!😊

如果你真的从这篇文章中学到了一些新东西,喜欢它,收藏它并与你的小伙伴分享。🤗最后,不要忘了❤或📑支持一下哦

本文转载自: 掘金

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

python实现万年历的查询

发表于 2021-10-01

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

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

今天要用python做一个小功能,那就是实现万年历的查询。

image.png

1
2
3
4
5
6
7
sql复制代码def is_leap_year(year):
   if year/4==0 and  year/400 !=0:
       return True
   elif year/100 == 0 and year/400 ==0 :
       return True
  else:
        return False

首先判断是否是闰年,因为计算2月是否有29天有用。这里面能够整除4,并且不能整除400的是闰年。能够整除100,并且能够整除400的是闰年,否则都是非闰年。

1
2
3
4
5
6
7
8
9
10
11
ini复制代码def getMonthDays(year,month):

    days = 31        #31天居多,设置为默认值
    if month == 2 :    #2月份要判断是否是闰年
        if is_leap_year(year):
            days=29
        else:
            days=28;
    elif month in [4,6,9,11]:     #判断小月,只有30天
        days=30
    return days

由年和月份获取指定年月的月份有多少天。这里面要注意的是2月需要计算是否是闰年,如果是闰年哪就有29天,如果不是那2月就只有28天。然后[4,6,9,11]是小月,只有30天,其他的都是大月,有31天。

1
2
3
4
5
6
7
8
9
10
11
sql复制代码def getTotalDays(year,month):
 
    totalDays=0
    for i in range(1990,year):     #使用range来循环,算出多少年多少天
        if is_leap_year(i):        #判断是否是闰年
            totalDays += 366
        else:
            totalDays += 365
    for i in range(1,month):       #使用range循环,算出今年前面几个月过了多少天
        totalDays +=getMonthDays(year,i)
    return totalDays

获取1990-01-01离现在这个月一共有多少天。首先这里是不能输入比1990少的月份,不然都是返回0天。我们需要先用年份判断一共过去了多少天,如果是闰年,那就+366,如果不是闰年那就+365。最后再把这个月的前面月份的所有天数加上,这样就可以求得距离1990-01-01有多少天了。

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
python复制代码if __name__ == '__main__':
    while True:                               
        print "××××××××××python实现万年历××××××××"
        year = raw_input("请输入年份(如:1990):")
        month = raw_input("请输入月份:如:1")
        try:                                   
            year = int(year)
            month = int(month)
            if month <1 or month >12:          
                print "年份或者月份输入错误,请重新输入!"
                continue
        except:                                
            print "年份或者月份输入错误,请重新输入!"   
            continue
        break   

    print "日\t一\t二\t三\t四\t五\t六"
    iCount = 0      #计数器来判断是否换行
    for i in range(getTotalDays(year,month)%7):
        print '\t',                 #输出空不换行
        iCount+=1
    for i in range(1,getMonthDays(year,month)):
        print i,
        print '\t',
        iCount +=1
        if iCount%7 == 0 :           #计数器取余为0,换行
            print ''

最后只需要输入年份和月份,就能把完整的整个月份的日历打出来。无论是查询以前的日历还是查询未来的日历,都是可以计算出来的。这里还需要做一下容错处理,让用户不输入错的数据。这里用%7就是星期1~星期日每7天就分行。这是完完全全按照日历的格式打印出来的。

欢迎和我讨论有关程序的问题,也可以答疑。关注公众号:诗一样的代码,交一个朋友。

本文转载自: 掘金

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

单元测试框架 Mockito 注解 – Mock, Sp

发表于 2021-10-01

转载翻译自 howtodoinjava.com/mockito/moc…

这篇 mockito 的教程文章能够帮助你了解更多 mockito 注解,比如 @Mock, @Spy, @Captor, @InjectMocks,来写出更好的单元测试。

1. Mockito 注解

1.1. @Mock

@Mock 注解被往往用来创建以及注入模拟实例。我们会用 mockito 框架创建一个模拟的实例类,而不是去真的创建需要的对象。

@Mock 注解也可以用 var somethingYouWantMock = Mockito.mock(classToMock) 这种函数方式赋值来替代,它们的效果是一样的,而用 @Mock 注解通常能能看起来代码 “整洁” 一点,因为我们不会想用一堆看起来一样的 Mockito.mock 函数在代码里重复得到处都是。

@Mock 注解的优势:

  • 能够快速创建测试所需的对象
  • 重复的模拟创建代码很少
  • 测试类可读性更好
  • 更容易看懂验证报错,因为注解的模拟类直接是类的属性,可以用字段名去赋予含义。

在下面给出的示例中,我们模拟了HashMap类。在实际测试中,我们将模拟实际的应用类。我们在map中放入一个键值对,然后验证,确认方法调用是在模拟的map实例上执行的。

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码@Mock
HashMap<String, Integer> mockHashMap;

@Test
public void saveTest()
{
mockHashMap.put("A", 1);

Mockito.verify(mockHashMap, times(1)).put("A", 1);
Mockito.verify(mockHashMap, times(0)).get("A");

assertEquals(0, mockHashMap.size());
}

译者补充:mock意思就是造一个假的模拟对象,不会去调用这个真正对象的方法,这个mock对象里的所有行为都是未定义的,属性也不会有值,需要你自己去定义它的行为。比如说,你可以mock一个假的size(), 使其返回100,但实际上并没有真的创建一个 size 为100的 Map)

1
2
3
4
5
6
java复制代码...
when(mockHashMap.size())
.thenReturn(100);

assertEquals(100, mockHashMap.size());
...

1.2. @Spy

@Spy注释用于创建一个真实对象并监视这个真实对象。@Spy对象能够调用所监视对象的所有正常方法,同时仍然跟踪每一次交互,就像我们使用mock一样,可以自己定义行为。

可以看到,在下面给的示例中,由于我们向它添加了一个 key-value 键值对,size 变成了 1。我们也能够得到真正的通过键 key 去拿到 value 值的结果。这在 Mock 注解的例子中是不可能的。

译者补充: 因为mock是模拟整个生成一个假对象,spy像是间谍潜伏在真实对象里去篡改行为。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码@Spy
HashMap<String, Integer> hashMap;

@Test
public void saveTest()
{
hashMap.put("A", 10);

Mockito.verify(hashMap, times(1)).put("A", 10);
Mockito.verify(hashMap, times(0)).get("A");

assertEquals(1, hashMap.size());
assertEquals(new Integer(10), (Integer) hashMap.get("A"));
}

@Mock和@Spy的区别

在使用@Mock时,mockito创建了类的一个基础套壳实例,完全用于跟踪与它的全部交互行为。这不是一个真正的对象,并且不维护状态,不存在更改。

当使用@Spy时,mockito创建一个类的真实实例,可以跟踪与它的每个交互行为,这个真实类能维护类状态的变化。

1.3 @Captor

@Captor注释用于创建ArgumentCaptor实例,该实例用于捕获方法参数值,来用于进一步做断言验证。

注意mockito使用参数类的equals()方法验证参数值是否相同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码@Mock
HashMap<String, Integer> hashMap;

@Captor
ArgumentCaptor<String> keyCaptor;

@Captor
ArgumentCaptor<Integer> valueCaptor;

@Test
public void saveTest()
{
hashMap.put("A", 10);

Mockito.verify(hashMap).put(keyCaptor.capture(), valueCaptor.capture());

assertEquals("A", keyCaptor.getValue());
assertEquals(new Integer(10), valueCaptor.getValue());
}

1.4 @InjectMocks

在mockito中,我们需要创建被测试的类对象,然后插入它的依赖项(mock)来完全测试行为。因此,我们要用到 @InjectMocks 注释。

@InjectMocks 标记了一个应该执行注入的字段。Mockito会按照下面优先级通过构造函数注入、setter注入或属性注入,来尝试注入你标识的mock。如果上面三种任何给定的注入策略注入失败了,Mockito不会报错。

更多信息: Mock和@ initmock注释的区别

译者补充: @InjectMocks 一般是你要测的类,他会把要测类的mock属性自动注入进去。@Mock 则是你要造假模拟的类。

本文转载自: 掘金

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

云原生(三)k8s 之服务、负载均衡和网络

发表于 2021-09-30

Service

Service 是应用服务的抽象,它定义了一组逻辑 Pod 和访问它们的策略。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
yaml复制代码apiVersion: v1
kind: Service
metadata:
name: my-service
namespace: test
spec:
# 选择符
selector:
app: personal
ports:
- protocol: TCP
# 集群给 service 开的端口,节点ip:port,集群内可访问该service
port: 80
# pod的端口,对应 Pod 中的 containerPort
targetPort: 9376

上述配置创建了一个名称为 my-service 的服务,其所属的 namespace 为 test。该服务会将请求代理到使用 TCP 端口9376,并且具有标签 app = personal 的Pod上。

服务选择符的控制器不断扫描与其选择符匹配的 Pod,然后发布到 也称为“my-service”的 Endpoint 对象。

需要注意的是,Service 能够将一个接收 port 映射到任意的 targetPort。默认情况下,targetPort 将会设置为与 port 字段相同的值。

1. 虚拟IP 和 Service代理

集群内服务之间的访问,以及 Pod 访问服务,都是由 kube-proxy 负责。

kube-proxy 为 Service 分配了一个 VIP(Virtual IP),当有流量打到 VIP 时,kube-proxy 会将流量代理到某个 Pod 中。所以,集群内微服务的负载均衡是由 kube-proxy 提供的。

1.1 iptables 代理模式

在该模式下,kube-proxy 对每个 service 都会配置 iptables 规则,从而捕获到达该 Service 的 clusterIP 和端口的请求,进而将请求重定向到 Service 的某个 Pod 上。如果第一个Pod没有响应,则连接失败,kube-proxy 会自动选择其他 Pod 重试。

image.png

iptables 规则默认的策略是,随机选择一个后端处理请求。

使用 iptables 处理流量具有较低的系统开销,因为流量由 Linux netfilter 处理,无需在用户空间和内核空间之间切换。

1.2 IPVS 代理模式(NAT模式)

在 ipvs 模式下,kube-proxy 监视服务和端点,调用 netlink 接口(netlink 是一种用于内核态和用户态进程之间进行数据传输的特殊的 IPC 机制)相应的创建 IPVS 规则,并定期将 IPVS 规则与 service 和 endpoints 同步,确保 IPVS 状态与所需状态匹配。

IPVS 代理模式与 iptables 类似的是,IPVS 也基于 netfilter,但是使用哈希表作为基础数据结构。

iptables 模式采用一条条的规则列表,又是为了防火墙设计的,集群数量越多 iptable 规则也越多,而且是从上到下匹配。所以,IPVS 模式下(hash 查表)的 kube-proxy 重定向延时要短。

image.png

除此之外,ipvs 支持比 iptables 更复杂的负载均衡算,比如轮替、最少链接、从不排队等等。

2. 服务发现

k8s 支持两种基本的服务发现模式——环境变量和DNS。

服务发现完成的工作是什么?

2.1 环境变量

当 Pod 运行在 Node 上,kubelet 会为每个活跃的 Service 添加一组环境变量。

比如,一个 Service 名称为“redis-master”,暴露了8888端口,同时给它分配的 ClusterIP 为 10.0.0.11,这个 Service 会生成如下环境变量:

1
2
3
4
5
6
7
yaml复制代码REDIS_MASTER_SERVICE_HOST=10.0.0.11
REDIS_MASTER_SERVICE_PORT=8888
REDIS_MASTER_PORT=tcp://10.0.0.11:8888
REDIS_MASTER_PORT_6379_TCP=tcp://10.0.0.11:8888
REDIS_MASTER_PORT_6379_TCP_PROTO=tcp
REDIS_MASTER_PORT_6379_TCP_PORT=8888
REDIS_MASTER_PORT_6379_TCP_ADDR=10.0.0.11

需要注意的是:当我们使用环境变量的方法访问服务的 Pod 时,那我们必须在 Pod 出现之前创建服务,否则不会自动生成 Service 的环境变量。

2.2 DNS

k8s 支持集群的 DNS 服务器(如 CoreDNS)监视集群中的新服务,并为每个服务创建一组 DNS 纪录。

例如,如果你在 Kubernetes 命名空间 my-ns 中有一个名为 my-service 的服务,则 DNS 服务共同为 my-service.my-ns 创建 DNS 记录。 my-ns 命名空间中的 Pod 能够通过按名检索 my-service 来找到服务 (my-service.my-ns 也可以工作)。

其他命名空间中的 Pod 必须将名称限定为 my-service.my-ns。 这些名称将被解析为对应的 Service IP。

k8s DNS 服务器是唯一一种能够访问 ExternalName 类型的 Service 的方式。

3. 服务类型

对于集群中的某些 Pod,可能希望将其暴露到集群外部访问,如前端。

k8s 允许指定 Service 类型,默认是 ClusterIP。

  • ClusterIP:通过集群内部的 IP 暴露服务,但服务只能在集群内部访问。
  • NodePort:在节点(虚拟机)上开放一个端口,任何到达该端口的的流量都可以转发到对应服务。 通过请求 <节点 IP>:<节点端口>,你可以从集群的外部访问一个 NodePort 服务。
  • LoadBalancer:使用云提供商的负载均衡器向外部暴露服务。外部负载均衡器可以将流量路由到自动创建的 NodePort和ClusterIP上。
  • ExternalName:通过返回 CNAME 和对应值,可以将服务映射到 externalName字段的内容。

也可以使用 Ingress 来暴露自己的服务。Ingress 不是一种服务类型,可充当集群的请求入口。Ingress 可以在同一 IP 地址下公开多个服务。

3.1 NodePort

如果将 type 字段设置为 NodePort,则 K8s 控制平面将在 --service-node-port-range 标志指定的范围内分配端口(默认值:30000-32767)。 每个节点将那个端口(每个节点上的相同端口号)代理到服务中。 除此之外,可以在服务的 .spec.ports[*].nodePort 字段中手动分配端口。

大多数时候我们可以让 k8s 自动分配端口,端口范围为 30000-32767。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
yaml复制代码apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
type: NodePort
selector:
app: MyApp
ports:
- port: 80
targetPort: 80
# 可选字段
# 节点ip:端口,可从集群外部访问该service
nodePort: 30007

3.2 LoadBalancer

设置 type 的值为 "LoadBalancer", 将为 Service 提供负载均衡器。负载均衡器是异步创建的,关于被提供的负载均衡器的信息将会通过 Service 的 status.loadBalancer 字段发布出去。

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
yaml复制代码apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
selector:
app: MyApp
ports:
- protocol: TCP
port: 80
targetPort: 9376
clusterIP: 10.0.171.239
type: LoadBalancer
status:
loadBalancer:
ingress:
- ip: 192.0.2.127

来自集群外部的负载均衡器将流量定向到 service 上,一般用作三层负载均衡。

某些云提供商的负载均衡器允许设置 loadBalancerIP ,将根据用户设置的 loadBalancerIP 来创建负载均衡器。如果没有设置 localBalancerIP 字段,将会给负载均衡器指派一个临时 IP。

3.2 ExternalName

类型为 ExternalName 的服务会将服务映射到 DNS 名称,实现暴露服务的目的。

以下 Service 将名称为my-service的服务映射到 my.database.example.com:

1
2
3
4
5
6
7
8
yaml复制代码apiVersion: v1
kind: Service
metadata:
name: my-service
namespace: prod
spec:
type: ExternalName
externalName: my.database.example.com

当查找服务 my-service.prod.svc.cluster.local 时,集群 DNS 服务返回 CNAME 记录, 其值为 my.database.example.com。 访问 my-service 的方式与其他服务的方式相同,但主要区别在于重定向发生在 DNS 级别,而不是通过代理或转发。

4. Ingress

Ingress 管理集群中服务的外部访问,典型的访问方式是 HTTP。可用作七层负载均衡,基于域名或路径将流量路由到后端服务。

需要特别注意的是:当使用 Nginx Ingress 作为 Ingress 控制器时,Nginx Ingress 在流量路由时不会使用 service,会使用 Endpoints 绕过 kube-proxy 将流量路由到 Pod,以实现负载均衡。

Ingress 不会公开任意端口或协议。如果想将 HTTP 和 HTTPS 以外的服务公开到外部,通常使用 Service.Type=NodePort 或 Service.Type=LoadBalancer 类型的服务。

如下路由:

image.png

基于七层负载均衡生成的 Ingress 对象的 YAML 文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
yaml复制代码apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: my-ingress
spec:
backend:
serviceName: other
servicePort: 8080
rules:
- host: foo.mydomain.com
http:
paths:
- backend:
serviceName: foo
servicePort: 8080
- host: mydomain.com
http:
paths:
- path: /bar/*
backend:
serviceName: bar
servicePort: 8080

本文转载自: 掘金

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

kettle 教程(一):简介及入门

发表于 2021-09-30

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

介绍
kettle 是纯 java 开发,开源的 ETL工具,用于数据库间的数据迁移 。可以在 Linux、windows、unix 中运行。有图形界面,也有命令脚本还可以二次开发。

kettle 的官网是 community.hitachivantara.com/docs/DOC-10… 地址是 github.com/pentaho/pen…

安装

这边以 windows 下的配置为例,linux 下配置类似。

jdk 安装及配置环境变量

由于 kettle 是基于 java 的,因此需要安装 java 环境,并配置 JAVA_HOME 环境变量。

建议安装 JDK1.8 及以上,7.0以后版本的 kettle 不支持低版本 JDK。

下载 kettle

从 官网 下载 kettle ,解压到本地即可。

下载相应的数据库驱动

由于 kettle 需要连接数据库,因此需要下载对应的数据库驱动。

例如 MySQL 数据库需要下载 mysql-connector-java.jar,oracle 数据库需要下载 ojdbc.jar。下载完成后,将 jar 放入 kettle 解压后路径的 lib 文件夹中即可。

注意:本文基于 pdi-ce-7.0.0.0-25 版本进行介绍,低版本可能有区别。

启动

双击 Spoon.bat 就能启动 kettle 。

转换

转换包括一个或多个步骤,步骤之间通过跳(hop)来连接。跳定义了一个单向通道,允许数据从一个步骤流向另一个步骤。在Kettle中,数据的单位是行,数据流就是数据行从一个步骤到另一个步骤的移动。

1、打开 kettle,点击 文件->新建->转换。

在这里插入图片描述

2、在左边 DB 连接处点击新建。

在这里插入图片描述

3、根据提示配置数据库,配置完成后可以点击测试进行验证,这边以 MySQL 为例。

在这里插入图片描述

4、在左侧找到表输入(核心对象->输入->表输入),拖到右方。

在这里插入图片描述

5、双击右侧表输入,进行配置,选择数据源,并输入 SQL。可以点击预览进行预览数据。

在这里插入图片描述

在这里插入图片描述

6、在左侧找到插入/更新(核心对象->输出->插入/更新),拖到右方。

在这里插入图片描述

7、按住 Shift 键,把表输入和插入/更新用线连接起来。

在这里插入图片描述

8、双击插入/更新进行配置。

在这里插入图片描述

9、点击运行,就可以运行这一个转换。

在这里插入图片描述

10、运行结束后,我们可以在下方看到运行结果,其中有日志,数据预览等,我们可以看到一共读取了多少条数据,插入更新了多少数据等等。

在这里插入图片描述

这样就完成了一个最简单的转换,从一个表取数据,插入更新到另一个表。

作业

如果想要定时运行这个转换,那么就要用到作业。

1、新建一个作业。

在这里插入图片描述

2、从左侧依次拖动 START 、转换、成功到右侧,并用线连接起来。

在这里插入图片描述

3、双击 START,可以配置作业的运行间隔,这边配置了每小时运行一次。

在这里插入图片描述

4、双击转换,选择之前新建的那个转换。

在这里插入图片描述

5、点击运行,就能运行这次作业,点击停止就能停止。在下方执行结果,可以看到运行的日志。

在这里插入图片描述

这样就完成了一个最简单的作业,每隔1小时,将源表的数据迁移到目标表。

总结
kettle 是一个非常强大的 ETL 工具,通过图形化界面的配置,可以实现数据迁移,并不用开发代码。

通过它的作业,kettle 能自动地运行转换。

参考 blog.csdn.net/qqfo24/arti…

本文转载自: 掘金

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

图谱相关技术在风控反作弊中的应用和探索

发表于 2021-09-30

‍‍

导读:互联网黑产不断发展壮大,作弊模式逐渐变得规模化、产业化,团伙作弊行为日益猖獗。为了进一步提升百度账号的安全和用户体验,维护公司核心利益,百度账号安全策略团队结合自身在账号安全领域的优势,构建了可以处理海量数据、具备丰富扩展性的关联图谱黑产团伙挖掘能力,充分实践应用并不断拓展落地场景,同时也在图神经网络等前沿领域探索相关技术在风控反作弊场景中的应用价值,致力于构建高效、完备的基于图谱的风控反作弊能力。

全文3770字,预计阅读时间14分钟。

一、简介

《中国互联网络发展状况统计报告》显示,截止2021年6月,中国网民规模达到了10.11亿。基于如此庞大的用户群体,使得互联网业务不断飞速发展。围绕着不断成长的互联网生态,自然而然也就催生出了一系列躲藏在隐秘角落里的黑灰产业务。随着技术的进步发展,黑灰产从最初的作坊式的作弊方式也转变成了流程化、规模化、产业化作弊模式,目前网络黑灰产规模也已超千亿,并且已经深入到多种业务场景进行作弊,首当其冲的即是账号系统,接着进入到具体业务场景,从事刷单、薅羊毛、引流、诈骗、洗钱等等欺诈作弊行为。黑灰产行为不仅使互联网公司蒙受金钱损失,长此以往更会影响用户的服务体验和财产安全,威胁到业务持续健康发展。

为了有效打击黑产作弊团伙,保障公司的基础安全,安全策略团队从账号维度出发,积极构建基于图谱的反黑产作弊架构,不断探索图谱相关技术在风控反作弊场景的应用落地。目前基于图谱技术构建了主要包括团伙挖掘能力、图谱节点表示能力。

二、团伙挖掘

众所周知,在实际业务场景下的黑产作弊团伙通常受限于资源、成本等现实条件,往往会出现共用资源的情况。这就成了挖掘黑产团伙入手的点,传统的方法是:可以通过统计特征因子筛选方式来筛选出一部分相关的账号,但这种方法很难进一步挖掘出整个相关的作弊团伙,只能case by case的处理这种问题。下面对比了传统手段与基础的关联图谱挖掘作弊团伙的差异(案例相关数据已做脱敏处理):

图片

_图-_1 案例__分析

如图-1左侧所示,图表中可以看出该案例账号使用过相当数量的特征因子和一个设备,这些关系转变为图结构即是图片右侧的图谱结构(账号:蓝色标记,特征因子:红色标记,设备:绿色标记)。通过这些关联因子进行挖掘,可以得到一批使用过这些可疑因子的账号,这其实也是图谱团伙挖掘的核心思想,但是想要通过这种方法挖掘出整个团伙却需要耗费大量时间。事实上,如图-2所示,该帐号只是整个黑产团伙中的冰山一角,传统方法挖掘的难度可想而知。

图片

图-2 案例所属团伙

上述的例子展现了关联图谱在团伙挖掘中的优势。结合现有业务场景,团队构建了覆盖不同场景、不同粒度(天、周、月)、不同特征关系类型(同构图、异构图)相结合的关联图谱框架,涉及到了多种不同类型的节点、多种复杂边关系特征。关联图谱框架如图-3所示。

图片

图-3 关联图谱基础架构

实际生产环境中的图谱都要处理几十亿节点和边数据,这是一个巨大的挑战,经过重新设计优化整个算法计算流程,该架构能够处理海量的数据并且具有丰富的扩展性,通过简单配置即可以挖掘不同异构情况下的团伙,也支持拓展新业务场景,通过跨场景的融合,在原有业务数据的基础上结合账号系统特有的账号安全信息,可以更全面挖掘分析黑产团伙。此外,利用关联图谱进行CASE分析扩召的能力也已经落地到实际业务中了。

实际业务中,使用关联图谱进行团伙挖掘可以找出CASE相关的可疑团伙,也可以监控业务中出现的异常团伙作弊行为,在新接入的业务场景中,通过关联图谱挖掘出来的可疑团伙都存在不同程度的团伙作弊行为。然而,新的技术也会带来一些新的挑战,正是因为基于特征的关联关系即绑定将不同账号进行绑定,这也使得账号之间的相关性并不牢靠,往往会存在以下一些问题:

  1. 通过设备信息等硬关联关系关联出的团伙也并非一定都是黑产作弊团伙,普通账号也可能存在共用设备、使用公共网络等情况,并不是关联出挖掘得到的所有团伙都是黑产团伙,所以需要对团伙进行分类定性;
  2. 实际业务中会因为脏数据、长时间跨度、黑产团伙间资源交叉、账号买卖等因素而产生规模巨大的团伙图谱,团伙图谱中可能会包含一些正常账号或者不同团伙的账号;

因此,也就有了更多的图谱相关的实践和探索。

三、团伙节点表示

针对关联图谱中所存在的问题,虽然有些可以通过一些条件限制、定义权重等进行过滤来减缓上述问题对整个关联图谱的影响,但是,这种一刀切的做法对于处理复杂边关系、多种节点类型的图谱很难做到恰到好处。因此,也就有了关于图谱技术更深入的探索——团伙中节点的表示。

节点表示,即将单个账号节点的特征信息通过深度学习的方法抽象为一个固定维度的向量,这个向量就表示这个账号,通过将账号特征向量化后,可以进一步做更多的下游工作,比如:节点间相关性的预测、节点的聚类、节点的分类等等。而图谱中的节点表示,不仅仅只考虑了该账号节点本身的特征信息,更包含了账号节点所处图谱中的结构信息,主要是节点的邻居信息和边关系信息。

团队调研了多种节点表示模型的方法,比如:Deepwalk[1]、LINE[2]、node2vec[3]等基于随机游走的方法,也包括GCN[4]、GAT[5]、GraphSAGE[6]、PinSAGE[7]等方法。

结合账号业务场景账号特征稀疏、节点规模庞大且没有显式标签的特点,所以通过链接预测任务来训练节点表示模型,考虑到整个数据的量级以及动态变化的问题,改进了GraphSAGE模型用于节点间链接预测,首先对目标节点进行基于随机游走的局部采样得到其邻居节点,通过两层GraphSAGE结构聚合目标节点两跳的邻居信息,结合两目标节点的表示向量交叉得到预测结果。通过半监督学习的方式,使用交叉熵作为损失函数,结合mini-batch的训练方式训练模型。模型架构如下图-4所示。

图片

图-4 链接预测框架

如公式(1)所示模型输入的节点特征,此外还需要目标节点的子图结构和目标节点关系对。通过公式(2-4)是模型第层节点融合其邻居节点的过程。

图片

模型通过生成目标节点关系对的表示向量做点积得到最终的链接预测结果,通过随机梯度下降优化模型参数。即公式(5)所示。

score = \sigma(e_i \bullet e_j), (5)

图片

为了进行对比,同时实现了MLP、GCN进行向量表示的基础模型,相同超参数的条件下,分别生成了同一组账号的表示向量,为了直观展示模型生成表示向量的区分性,这里选取关联图谱中账号节点所属的TOP25团伙的节点,其节点编号作为颜色标签,分别通过T-SNE、UMAP降维进行可视化对比,T-SNE可视化结果如下。图-5是基于GraphSAGE-sum生成的节点表示向量的T-SNE降维后的三维空间分布,相比图-6和图-7分别是基于MLP和GCN生成节点表示向量的三维分布,可以看出GraphSAGE-sum节点表示向量的区分性明显优于其他,相同颜色编号的属于同一团伙(因为使用的关联图谱中的团伙标签作为参考,图中可能存在不同标签的团伙实际是同一团伙,即不同颜色编号发生重叠),GraphSAGE图中各团伙颜色标签相同的聚集更加紧凑,不同的团伙区分较为明显,发生颜色标签重叠的团伙也更少。(注:团伙标签过多,颜色有限,需要结合颜色和标签编号共同区分不同团伙)

图片

图-5 基于GraphSAGE生成的节点表示T-SNE降维展示

图片

图-6 基于MLP生成的节点表示T-SNE降维展示

图片

图-7基于GCN生成的节点表示T-SNE降维展示

在得到节点表示模型后,基于此可以进行多种下游任务的应用,包括节点间相关性的预测、节点分类、生成团伙的表示向量、节点聚类等等。以实际业务中的团伙定性需求为例,相比只使用基础账号维度特征统计的XGboost分类模型,模型进一步增加节点表示向量特征后,其初步测试分类效果达到了90+%的水平,相信经过全量团伙数据的训练,模型实际的团伙分类定性效果会得到进一步提升。

四、展望

本文介绍了图谱相关技术在风控反作弊中的实践和探索,有些已经落地应用并取得了很好的效果,相应的也或多或少存在一些问题需要进一步解决探讨。

  1. 针对关联图谱中存在的特大团伙和团伙定性问题,可以基于节点表示模型设计什么样的下游任务;
  2. 节点表示模型目前受GPU限制比较大,如何能高效产出图谱中节点表示向量,以及如何进一步提升模型的泛化效果;

整个基于图谱技术的风控反作弊框架还需要不断完善,不仅仅以上提到的技术能力,还有更多技术需要进行深入探索、研究和落地应用。比如图采样技术、图表示能力、图的可视化、实时的图处理能力等等。

参考文献:

[1] Perozzi B, Al-Rfou R, Skiena S. Deepwalk: Online learning of social representations[C]//Proceedings of the 20th ACM SIGKDD international conference on Knowledge discovery and data mining. 2014: 701-710.

[2] Tang J, Qu M, Wang M, et al. Line: Large-scale information network embedding[C]//Proceedings of the 24th international conference on world wide web. 2015: 1067-1077.

[3] Grover A, Leskovec J. node2vec: Scalable feature learning for networks[C]//Proceedings of the 22nd ACM SIGKDD international conference on Knowledge discovery and data mining. 2016: 855-864.

[4] Kipf T N, Welling M. Semi-supervised classification with graph convolutional networks[J]. arXiv preprint arXiv:1609.02907, 2016.

[5] Veličković P, Cucurull G, Casanova A, et al. Graph attention networks[J]. arXiv preprint arXiv:1710.10903, 2017.

[6] Hamilton W L, Ying R, Leskovec J. Inductive representation learning on large graphs[C]//Proceedings of the 31st International Conference on Neural Information Processing Systems. 2017: 1025-1035.

[7] Ying R, He R, Chen K, et al. Graph convolutional neural networks for web-scale recommender systems[C]//Proceedings of the 24th ACM SIGKDD International Conference on Knowledge Discovery & Data Mining. 2018: 974-983.

[8] Chen T, He T, Benesty M, et al. Xgboost: extreme gradient boosting[J]. R package version 0.4-2, 2015, 1(4): 1-4.


推荐阅读:

|好看视频Android重构——围绕于播放器的重构实践

|浅谈百度阅读/文库NA端排版技术

|云原生架构下的持续交付实践

|一年数十万次实验背后的架构与数据科学

———- END ———-

百度Geek说

百度官方技术公众号上线啦!

技术干货 · 行业资讯 · 线上沙龙 · 行业大会

招聘信息 · 内推信息 · 技术书籍 · 百度周边

欢迎各位同学关注

本文转载自: 掘金

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

大厂都是如何解决Java日志级别,重复记录、丢日志问题? 1

发表于 2021-09-30

1 SLF4J

日志行业的现状

  • 框架繁
    不同类库可能使用不同日志框架,兼容难,无法接入统一日志,让运维很头疼!
  • 配置复杂
    由于配置文件一般是 xml 文件,内容繁杂!很多人喜欢从其他项目或网上闭眼copy!
  • 随意度高
    因为不会直接导致代码 bug,测试人员也难发现问题,开发就没仔细考虑日志内容获取的性能开销,随意选用日志级别!

Logback、Log4j、Log4j2、commons-logging及java.util.logging等,都是Java体系的日志框架。
不同的类库,还可能选择使用不同的日志框架,导致日志统一管理困难。

  • SLF4J(Simple Logging Facade For Java)就为解决该问题而生
  • 提供统一的日志门面API
    图中紫色部分,实现中立的日志记录API
  • 桥接功能
    蓝色部分,把各种日志框架API桥接到SLF4J API。这样即使你的程序使用了各种日志API记录日志,最终都可桥接到SLF4J门面API
  • 适配功能
    红色部分,绑定SLF4J API和实际的日志框架(灰色部分)

SLF4J只是日志标准,还是需要实际日志框架。日志框架本身未实现SLF4J API,所以需要有个前置转换。
Logback本身就按SLF4J API标准实现,所以无需绑定模块做转换。

虽然可用log4j-over-slf4j实现Log4j桥接到SLF4J,也可使用slf4j-log4j12实现SLF4J适配到Log4j,也把它们画到了一列,但是它不能同时使用它们,否则就会产生死循环。jcl和jul同理。

虽然图中有4个灰色的日志实现框架,但业务开发使用最多的还是Logback和Log4j,都是同一人开发的。Logback可认为是Log4j改进版,更推荐使用,已是社会主流。

Spring Boot的日志框架也是Logback。那为什么我们没有手动引入Logback包,就可直接使用Logback?

spring-boot-starter模块依赖spring-boot-starter-logging模块,而
spring-boot-starter-logging自动引入logback-classic(包含SLF4J和Logback日志框架)和SLF4J的一些适配器。

2 异步日志就肯定能提高性能?

如何避免日志记录成为系统性能瓶颈呢?
这关系到磁盘(比如机械磁盘)IO性能较差、日志量又很大的情况下,如何记录日志。

2.1 案例

定义如下的日志配置,一共有两个Appender:

  • FILE是一个FileAppender,用于记录所有的日志
  • CONSOLE是一个ConsoleAppender,用于记录带有time标记的日志

把大量日志输出到文件中,日志文件会非常大,若性能测试结果也混在其中,就很难找到那条日志了。
所以,这里使用EvaluatorFilter对日志按照标记进行过滤,并将过滤出的日志单独输出到控制台。该案例中给输出测试结果的那条日志上做了time标记。

配合使用标记和EvaluatorFilter,可实现日志的按标签过滤。

  • 测试代码:实现记录指定次数的大日志,每条日志包含1MB字节的模拟数据,最后记录一条以time为标记的方法执行耗时日志:

执行程序后发现,记录1000次日志和10000次日志的调用耗时,分别是5.1s和39s
)
对只记录文件日志的代码,这耗时过长了。

2.2 源码解析

FileAppender继承自OutputStreamAppender

在追加日志时,是直接把日志写入OutputStream中,属同步记录日志

所以日志大量写入才会旷日持久。如何才能实现大量日志写入时,不会过多影响业务逻辑执行耗时而影响吞吐量呢?

2.3 AsyncAppender

使用Logback的AsyncAppender,即可实现异步日志记录。

AsyncAppender类似装饰模式,在不改变类原有基本功能情况下,为其增添新功能。这便可把AsyncAppender附加在其他Appender,将其变为异步。

定义一个异步Appender ASYNCFILE,包装之前的同步文件日志记录的FileAppender, 即可实现异步记录日志到文件

  • 记录1000次日志和10000次日志的调用耗时,分别是537ms和1019ms
    )
    异步日志真的如此高性能?并不,因为它并没有记录下所有日志。

3 AsyncAppender异步日志的天坑

  • 记录异步日志撑爆内存
  • 记录异步日志出现日志丢失
  • 记录异步日志出现阻塞。

3.1 案例

模拟个慢日志记录场景:
首先,自定义一个继承自ConsoleAppender的MySlowAppender,作为记录到控制台的输出器,写入日志时睡1s。

  • 配置文件中使用AsyncAppender,将MySlowAppender包装为异步日志记录
  • 测试代码
  • 耗时很短但出现日志丢失:要记录1000条日志,最终控制台只能搜索到215条日志,而且日志行号变问号。
  • 原因分析
  • AsyncAppender*提供了一些配置参数,而当前没用对。

源码解析

  • includeCallerData
    默认false:方法行号、方法名等信息不显示
  • queueSize
    控制阻塞队列大小,使用的ArrayBlockingQueue阻塞队列,默认容量256:内存中最多保存256条日志
  • discardingThreshold
    丢弃日志的阈值,为防止队列满后发生阻塞。默认队列剩余容量 < 队列长度的20%,就会丢弃TRACE、DEBUG和INFO级日志
  • neverBlock
    控制队列满时,加入的数据是否直接丢弃,不会阻塞等待,默认是false
    • 队列满时:offer不阻塞,而put会阻塞
    • neverBlock为true时,使用offer
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
java复制代码public class AsyncAppender extends AsyncAppenderBase<ILoggingEvent> {
// 是否收集调用方数据
boolean includeCallerData = false;
protected boolean isDiscardable(ILoggingEvent event) {
Level level = event.getLevel();
// 丢弃 ≤ INFO级日志
return level.toInt() <= Level.INFO_INT;
}
protected void preprocess(ILoggingEvent eventObject) {
eventObject.prepareForDeferredProcessing();
if (includeCallerData)
eventObject.getCallerData();
}
}
public class AsyncAppenderBase<E> extends UnsynchronizedAppenderBase<E> implements AppenderAttachable<E> {

// 阻塞队列:实现异步日志的核心
BlockingQueue<E> blockingQueue;
// 默认队列大小
public static final int DEFAULT_QUEUE_SIZE = 256;
int queueSize = DEFAULT_QUEUE_SIZE;
static final int UNDEFINED = -1;
int discardingThreshold = UNDEFINED;
// 当队列满时:加入数据时是否直接丢弃,不会阻塞等待
boolean neverBlock = false;

@Override
public void start() {
...
blockingQueue = new ArrayBlockingQueue<E>(queueSize);
if (discardingThreshold == UNDEFINED)
//默认丢弃阈值是队列剩余量低于队列长度的20%,参见isQueueBelowDiscardingThreshold方法
discardingThreshold = queueSize / 5;
...
}

@Override
protected void append(E eventObject) {
if (isQueueBelowDiscardingThreshold() && isDiscardable(eventObject)) { //判断是否可以丢数据
return;
}
preprocess(eventObject);
put(eventObject);
}

private boolean isQueueBelowDiscardingThreshold() {
return (blockingQueue.remainingCapacity() < discardingThreshold);
}

private void put(E eventObject) {
if (neverBlock) { //根据neverBlock决定使用不阻塞的offer还是阻塞的put方法
blockingQueue.offer(eventObject);
} else {
putUninterruptibly(eventObject);
}
}
//以阻塞方式添加数据到队列
private void putUninterruptibly(E eventObject) {
boolean interrupted = false;
try {
while (true) {
try {
blockingQueue.put(eventObject);
break;
} catch (InterruptedException e) {
interrupted = true;
}
}
} finally {
if (interrupted) {
Thread.currentThread().interrupt();
}
}
}
}

默认队列大小256,达到80%后开始丢弃<=INFO级日志后,即可理解日志中为什么只有两百多条INFO日志了。

queueSize 过大

可能导致OOM

queueSize 较小

默认值256就已经算很小了,且discardingThreshold设置为大于0(或为默认值),队列剩余容量少于discardingThreshold的配置就会丢弃<=INFO日志。这里的坑点有两个:

  1. 因为discardingThreshold,所以设置queueSize时容易踩坑。
    比如本案例最大日志并发1000,即便置queueSize为1000,同样会导致日志丢失
  2. discardingThreshold参数容易有歧义,它不是百分比,而是日志条数。对于总容量10000队列,若希望队列剩余容量少于1000时丢弃,需配置为1000

neverBlock 默认false

意味总可能会出现阻塞。

  • 若discardingThreshold = 0,那么队列满时再有日志写入就会阻塞
  • 若discardingThreshold != 0,也只丢弃≤INFO级日志,出现大量错误日志时,还是会阻塞

queueSize、discardingThreshold和neverBlock三参密不可分,务必按业务需求设置:

  • 若优先绝对性能,设置neverBlock = true,永不阻塞
  • 若优先绝不丢数据,设置discardingThreshold = 0,即使≤INFO级日志也不会丢。但最好把queueSize设置大一点,毕竟默认的queueSize显然太小,太容易阻塞。
  • 若兼顾,可丢弃不重要日志,把queueSize设置大点,再设置合理的discardingThreshold

以上日志配置最常见两个误区

再看日志记录本身的误区。

4 如何选择日志级别?

使用{}占位符,就不用判断log level了吗?

据不知名网友说道:SLF4J的{}占位符语法,到真正记录日志时才会获取实际参数,因此解决了日志数据获取的性能问题。
是真的吗?

  • 验证代码:返回结果耗时1s

若记录DEBUG日志,并设置只记录>=INFO级日志,程序是否也会耗时1s?
三种方法测试:

  • 拼接字符串方式记录slowString
  • 使用占位符方式记录slowString
  • 先判断日志级别是否启用DEBUG。



前俩方式都调用slowString,所以都耗时1s。且方式二就是使用占位符记录slowString,这种方式虽允许传Object,不显式拼接String,但也只是延迟(若日志不记录那就是省去)日志参数对象.toString()和字符串拼接的耗时。

本案例除非事先判断日志级别,否则必调用slowString。所以使用{}占位符不能通过延迟参数值获取,来解决日志数据获取的性能问题。

除事先判断日志级别,还可通过lambda表达式延迟参数内容获取。但SLF4J的API还不支持lambda,因此需使用Log4j2日志API,把Lombok的@Slf4j注解替换为**@Log4j2**注解,即可提供lambda表达式参数的方法:

这样调用debug,签名Supplier<?>,参数就会延迟到真正需要记录日志时再获取:



所以debug4并不会调用slowString方法

只是换成Log4j2 API,真正的日志记录还是走的Logback,这就是SLF4J适配的好处。

总结

  • SLF4J统一了Java日志框架。在使用SLF4J时,要理清楚其桥接API和绑定。若程序启动时出现SLF4J错误提示,可能是配置问题,可使用Maven的dependency:tree命令梳理依赖关系。
  • 异步日志解决性能问题,是用空间换时间。但空间毕竟有限,当空间满,要考虑阻塞等待or丢弃日志。若更希望不丢弃重要日志,那么选择阻塞等待;如果更希望程序不要因为日志记录而阻塞,那么就需要丢弃日志。
  • 日志框架提供的参数化记录方式不能完全取代日志级别的判断。若日志量很大,获取日志参数代价也很大,就要判断日志级别,避免不记录日志也要耗时获取日志参数!

本文转载自: 掘金

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

1…511512513…956

开发者博客

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