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

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


  • 首页

  • 归档

  • 搜索

Golang Gin 框架入门介绍(一)

发表于 2021-11-13

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

目录

  • 前言
  • 正文
  • 结尾

前言

Gin 是使用纯 Golang 语言实现的 HTTP Web 框架,Gin 的接口设计简洁,性能极高,现在被广泛使用。我司的 Web 后端服务基本是都是基于 Gin 开发的。

正文

安装

首次使用 Gin 框架时,需要先进行安装,命令如下:

go get -u github.com/gin-gonic/gin

可能会出现如下报错信息:

1
2
3
4
js复制代码# cd .; git clone -- https://github.com/gin-contrib/sse /Users/lz/go/src/github.com/gin-contrib/sse
Cloning into '/Users/lz/go/src/github.com/gin-contrib/sse'...
fatal: unable to access 'https://github.com/gin-contrib/sse/': LibreSSL SSL_connect: SSL_ERROR_SYSCALL in connection to github.com:443
package github.com/gin-contrib/sse: exit status 128

一般都是网络问题,可以多重试几次。

使用

在使用 Gin 的工具库之前,先要导入依赖库,具体代码如下:

1
go复制代码import "github.com/gin-gonic/gin"

具体使用如下:

1
go复制代码r := gin.Default()

实例演示

编写一段示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
go复制代码package main

import "github.com/gin-gonic/gin"

func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
r.Run()
}

通过代码可以知道,我们定义了 API 为 /ping 的服务接口。

然后我们编译代码,命令:

go build

编译成功后,会生成可执行程序 gin-demo,运行可执行程序,具体执行过程如下:

1
2
3
4
5
6
7
8
9
10
js复制代码liuzhen-3:gin-demo lz$ ./gin-demo
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET /ping --> main.main.func1 (3 handlers)
[GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
[GIN-debug] Listening and serving HTTP on :8080

通过日志,我们可以看到,服务默认启动了 8080 端口。

接下来,我们测试一下这个服务是不是好用,在浏览器中输入如下地址:

http://localhost:8080/ping

运行结果如下图所示:

image.png

通过结果可以知道,服务是正常的。

结尾

综上所述,Gin 框架使用起来还是非常简单的,上手也非常方便。感兴趣的话,小伙伴们自己就动手试试吧!下面的内容我们继续深入分析 Gin 框架的源码部分,敬请期待!

作者简介:大家好,我是 liuzhen007,是一位音视频技术爱好者,同时也是CSDN博客专家、华为云社区云享专家、签约作者,欢迎关注我分享更多干货!

本文转载自: 掘金

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

ORACLE分区表转换之在线重定义(DBMS_REDEFIN

发表于 2021-11-13

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

一、DBMS_REDEFINITION(在线重定义)

参考MOS文档:_How To Partition Existing Table Using DBMS_REDEFINITION (Doc ID 472449.1) _

支持的数据库版本***:Oracle Database - Enterprise Edition - Version 9.2.0.4 and later***

在线重定义是通过 物化视图 实现的。

使用在线重定义的一些限制条件:

1、必须有足够的表空间来容纳表的两倍数据量。

2、主键列不能被修改。

3、表必须有主键。

4、必须在同一个用户下进行在线重定义。

5、SYS和SYSTEM用户下的表无法进行在线重定义。

6、在线重定义无法采用nologging。

7、如果中间表有新增列,则不能有NOT NULL约束

DBMS_REDEFINITION包:

  • ABSORT_REDEF_TABLE:清理重定义的错误和中止重定义;
  • CAN_REDEF_TABLE:检查表是否可以进行重定义,存储过程执行成功代表可以进行重定义;
  • COPY_TABLE_DEPENDENTS:同步索引和依赖的对象(包括索引、约束、触发器、权限等);
  • FINISH_REDEF_TABLE:完成在线重定义;
  • REGISTER_DEPENDENTS_OBJECTS:注册依赖的对象,如索引、约束、触发器等;
  • START_REDEF_TABLE:开始在线重定义;
  • SYNC_INTERIM_TABLE:增量同步数据;
  • UNREGISTER_DEPENDENT_OBJECT:不注册依赖的对象,如索引、约束、触发器等;
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
sql复制代码CREATE OR REPLACE PACKAGE SYS.dbms_redefinition AUTHID CURRENT_USER IS
------------
-- OVERVIEW
--
-- This package provides the API to perform an online, out-of-place
-- redefinition of a table

--- =========
--- CONSTANTS
--- =========
-- Constants for the options_flag parameter of start_redef_table
cons_use_pk CONSTANT PLS_INTEGER := 1;
cons_use_rowid CONSTANT PLS_INTEGER := 2;

-- Constants used for the object types in the register_dependent_object
cons_index CONSTANT PLS_INTEGER := 2;
cons_constraint CONSTANT PLS_INTEGER := 3;
cons_trigger CONSTANT PLS_INTEGER := 4;
cons_mvlog CONSTANT PLS_INTEGER := 10;

-- constants used to specify the method of copying indexes
cons_orig_params CONSTANT PLS_INTEGER := 1;

PRAGMA SUPPLEMENTAL_LOG_DATA(default, AUTO_WITH_COMMIT);

-- NAME: can_redef_table - check if given table can be re-defined
-- INPUTS: uname - table owner name
-- tname - table name
-- options_flag - flag indicating user options to use
-- part_name - partition name
PROCEDURE can_redef_table(uname IN VARCHAR2,
tname IN VARCHAR2,
options_flag IN PLS_INTEGER := 1,
part_name IN VARCHAR2 := NULL);
PRAGMA SUPPLEMENTAL_LOG_DATA(can_redef_table, NONE);

-- NAME: start_redef_table - start the online re-organization
-- INPUTS: uname - schema name
-- orig_table - name of table to be re-organized
-- int_table - name of interim table
-- col_mapping - select list col mapping
-- options_flag - flag indicating user options to use
-- orderby_cols - comma separated list of order by columns
-- followed by the optional ascending/descending
-- keyword
-- part_name - name of the partition to be redefined
PROCEDURE start_redef_table(uname IN VARCHAR2,
orig_table IN VARCHAR2,
int_table IN VARCHAR2,
col_mapping IN VARCHAR2 := NULL,
options_flag IN BINARY_INTEGER := 1,
orderby_cols IN VARCHAR2 := NULL,
part_name IN VARCHAR2 := NULL);

-- NAME: finish_redef_table - complete the online re-organization
-- INPUTS: uname - schema name
-- orig_table - name of table to be re-organized
-- int_table - name of interim table
-- part_name - name of the partition being redefined
PROCEDURE finish_redef_table(uname IN VARCHAR2,
orig_table IN VARCHAR2,
int_table IN VARCHAR2,
part_name IN VARCHAR2 := NULL);

-- NAME: abort_redef_table - clean up after errors or abort the
-- online re-organization
-- INPUTS: uname - schema name
-- orig_table - name of table to be re-organized
-- int_table - name of interim table
-- part_name - name of the partition being redefined
PROCEDURE abort_redef_table(uname IN VARCHAR2,
orig_table IN VARCHAR2,
int_table IN VARCHAR2,
part_name IN VARCHAR2 := NULL);

-- NAME: sync_interim_table - synchronize interim table with the original
-- table
-- INPUTS: uname - schema name
-- orig_table - name of table to be re-organized
-- int_table - name of interim table
-- part_name - name of the partition being redefined
PROCEDURE sync_interim_table(uname IN VARCHAR2,
orig_table IN VARCHAR2,
int_table IN VARCHAR2,
part_name IN VARCHAR2 := NULL);

-- NAME: register_dependent_object - register dependent object
--
-- INPUTS: uname - schema name
-- orig_table - name of table to be re-organized
-- int_table - name of interim table
-- dep_type - type of the dependent object
-- dep_owner - name of the dependent object owner
-- dep_orig_name- name of the dependent object defined on table
-- being re-organized
-- dep_int_name - name of the corressponding dependent object on
-- the interim table
PROCEDURE register_dependent_object(uname IN VARCHAR2,
orig_table IN VARCHAR2,
int_table IN VARCHAR2,
dep_type IN PLS_INTEGER,
dep_owner IN VARCHAR2,
dep_orig_name IN VARCHAR2,
dep_int_name IN VARCHAR2);

-- NAME: unregister_dependent_object - unregister dependent object
--
-- INPUTS: uname - schema name
-- orig_table - name of table to be re-organized
-- int_table - name of interim table
-- dep_type - type of the dependent object
-- dep_owner - name of the dependent object owner
-- dep_orig_name- name of the dependent object defined on table
-- being re-organized
-- dep_int_name - name of the corressponding dependent object on
-- the interim table
PROCEDURE unregister_dependent_object(uname IN VARCHAR2,
orig_table IN VARCHAR2,
int_table IN VARCHAR2,
dep_type IN PLS_INTEGER,
dep_owner IN VARCHAR2,
dep_orig_name IN VARCHAR2,
dep_int_name IN VARCHAR2);

-- NAME: copy_table_dependents
--
-- INPUTS: uname - schema name
-- orig_table - name of table to be re-organized
-- int_table - name of interim table
-- copy_indexes - integer value indicating whether to
-- copy indexes
-- 0 - don't copy
-- 1 - copy using storage params/tablespace
-- of original index
-- copy_triggers - TRUE implies copy triggers, FALSE otherwise
-- copy_constraints - TRUE implies copy constraints, FALSE
-- otherwise
-- copy_privileges - TRUE implies copy privileges, FALSE
-- otherwise
-- ignore errors - TRUE implies continue after errors, FALSE
-- otherwise
-- num_errors - number of errors that occurred while
-- cloning ddl
-- copy_statistics - TRUE implies copy table statistics, FALSE
-- otherwise.
-- If copy_indexes is 1, copy index
-- related statistics, 0 otherwise.
-- copy_mvlog - TRUE implies copy table's MV log, FALSE
-- otherwise.
PROCEDURE copy_table_dependents(uname IN VARCHAR2,
orig_table IN VARCHAR2,
int_table IN VARCHAR2,
copy_indexes IN PLS_INTEGER := 1,
copy_triggers IN BOOLEAN := TRUE,
copy_constraints IN BOOLEAN := TRUE,
copy_privileges IN BOOLEAN := TRUE,
ignore_errors IN BOOLEAN := FALSE,
num_errors OUT PLS_INTEGER,
copy_statistics IN BOOLEAN := FALSE,
copy_mvlog IN BOOLEAN := FALSE);

END;

二、在线重定义表的步骤

**1.**创建未分区的表,如果存在,就不需要操作。

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
sql复制代码--前置准备:创建用户,表空间,授权用户。
SQL> create tablespace PARTITION;
SQL> create user par identified by par;
SQL> grant dba to par;

--创建表,索引,授权,同义词
SQL> conn par/par
Connected.
-- Create table
create table student(
s_id number(8) PRIMARY KEY,
s_name varchar2(20) not null,
s_sex varchar2(8),
s_birdate date,
constraint u_1 unique(s_name),
constraint c_1 check (s_sex in ('MALE','FEMALE')))
tablespace PARTITION;
-- Add comments to the table
comment on table STUDENT is '学生表';
-- Add comments to the columns
comment on column STUDENT.s_name is '姓名';
comment on column STUDENT.s_sex is '性别';
comment on column STUDENT.s_birdate is '出生日期';
-- Create/Recreate indexes
create index S_NAME_IDX on STUDENT (S_NAME, S_SEX) tablespace PARTITION;
-- Create SYNONYM
CREATE SYNONYM stu FOR student;
-- Grant/Revoke object privileges
grant select, insert, delete on STUDENT to SCOTT;

--查看表结构
SQL> desc stu
Name Null? Type
----------------------------------------- -------- ----------------------------
S_ID NOT NULL NUMBER(8)
S_NAME NOT NULL VARCHAR2(20)
S_SEX VARCHAR2(8)
S_BIRDATE DATE

--插入数据
begin
for i in 0 .. 24 loop
insert into student values
(i,
'student_' || i,
decode(mod(i, 2), 0, 'MALE', 'FEMALE'),
add_months(to_date('2019-1-1', 'yyyy-mm-dd'), i));
end loop;
commit;
end;
/

2.确认表是否存在主键,表空间是否足够,收集表统计信息。

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
sql复制代码--查看表主键
SQL> select cu.* from user_cons_columns cu, user_constraints au where cu.constraint_name = au.constraint_name and au.constraint_type = 'P' and au.table_name = 'STUDENT';

--查看表大小和表空间
--查看表空间
SQL> select tablespace_name from dba_segments where segment_type= 'TABLE' and segment_name='STUDENT' and owner='PAR';

--查看表大小
SQL> select sum(bytes/1024/1024) from dba_segments where segment_type= 'TABLE' and segment_name='STUDENT' and owner='PAR';

--查看表空间
select tbs_used_info.tablespace_name,
tbs_used_info.alloc_mb,
tbs_used_info.used_mb,
tbs_used_info.max_mb,
tbs_used_info.free_of_max_mb,
tbs_used_info.used_of_max || '%' used_of_max_pct
from (select a.tablespace_name,
round(a.bytes_alloc / 1024 / 1024) alloc_mb,
round((a.bytes_alloc - nvl(b.bytes_free,
0)) / 1024 / 1024) used_mb,
round((a.bytes_alloc - nvl(b.bytes_free,
0)) * 100 / a.maxbytes) used_of_max,
round((a.maxbytes - a.bytes_alloc + nvl(b.bytes_free,
0)) / 1048576) free_of_max_mb,
round(a.maxbytes / 1048576) max_mb
from (select f.tablespace_name,
sum(f.bytes) bytes_alloc,
sum(decode(f.autoextensible,
'YES',
f.maxbytes,
'NO',
f.bytes)) maxbytes
from dba_data_files f
group by tablespace_name) a,
(select f.tablespace_name,
sum(f.bytes) bytes_free
from dba_free_space f
group by tablespace_name) b
where a.tablespace_name = b.tablespace_name(+)) tbs_used_info
order by tbs_used_info.used_of_max desc;

--如果表空间不够,提前增加表空间大小
alter tablespace PARTITION add datafile;

--收集统计信息(可忽略)
EXEC DBMS_STATS.gather_table_stats('PAR', 'STUDENT', cascade => TRUE);

3.调用DBMS_REDEFINITION.CAN_REDEF_TABLE()过程,确认表是否满足重定义的条件。

1
2
3
sql复制代码SQL> EXEC Dbms_Redefinition.can_redef_table('PAR', 'STUDENT');

PL/SQL procedure successfully completed.

4.在用一个用户中建立一个空的中间表,根据重定义后你期望得到的结构建立中间表。比如:采用分区表(间隔分区),增加了COLUMN等。

在中间表上建立触发器、索引和约束,并进行相应的授权。任何包含中间表的完整性约束应将状态置为disabled。(此步骤也可以放在同步数据后操作)

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
sql复制代码--创建间隔分区(增加列s_phone)
create table STUDENT_PAR
(
s_id NUMBER(8) not null,
s_name VARCHAR2(20) not null,
s_sex VARCHAR2(8),
s_birdate DATE,
s_phone number
)
tablespace PARTITION
PARTITION BY RANGE(s_birdate)
INTERVAL (NUMTOYMINTERVAL(1,'MONTH')) STORE IN (partition)
(PARTITION STUDENT_201901 VALUES LESS THAN (TO_DATE('2019-02-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIAN')));

--临时中间表上创建如下:
--创建主键约束
alter table STUDENT_PAR add primary key (S_ID) using index tablespace PARTITION;
--创建唯一索引约束
alter table STUDENT_PAR add constraint U_1_PAR unique (S_NAME) using index tablespace PARTITION;
--创建check约束
alter table STUDENT_PAR add constraint C_1_PAR check (s_sex in ('MALE','FEMALE'));
--创建索引
CREATE INDEX S_NAME_IDX_PAR ON STUDENT_PAR (S_NAME,S_SEX) tablespace PARTITION;
--创建同义词
CREATE SYNONYM stu_par FOR STUDENT_PAR;
--添加描述
COMMENT ON TABLE STUDENT_PAR IS '学生表';
COMMENT ON COLUMN STUDENT_PAR.s_name IS '姓名';
COMMENT ON COLUMN STUDENT_PAR.s_sex IS '性别';
COMMENT ON COLUMN STUDENT_PAR.s_birdate IS '出生日期';
--授权
GRANT SELECT,INSERT,DELETE ON STUDENT_PAR TO scott;

5.调用DBMS_REDEFINITION.START_REDEF_TABLE()过程,并提供下列参数:被重定义的表的名称、中间表的名称、列的映射规则、重定义方法。

如果映射方法没有提供,则认为所有包括在中间表中的列用于表的重定义。如果给出了映射方法,则只考虑映射方法中给出的列。如果没有给出重定义方法,则默认使用主键方式。

1
2
3
4
5
6
7
8
9
sql复制代码SQL> BEGIN
DBMS_REDEFINITION.start_redef_table(
uname => 'PAR',
orig_table => 'STUDENT',
int_table => 'STUDENT_PAR');
END;
/

PL/SQL procedure successfully completed.

6.(可选)在创建索引之前将新表与临时名称同步。

Notes:如果在执行DBMS_REDEFINITION.START_REDEF_TABLE()过程和执行DBMS_REDEFINITION.FINISH_REDEF_TABLE()过程直接在重定义表上执行了大量的DML操作,那么可以选择执行一次或多次的SYNC_INTERIM_TABLE()过程,此操作可以减少最后一步执行FINISH_REDEF_TABLE()过程时的锁定时间。

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
sql复制代码--模拟业务不停,DML表数据写入
insert into STUDENT values(25,'student_25','MALE',to_date('2020-8-1', 'yyyy-mm-dd'));
update student set s_sex='FEMALE' where s_id = 20;
commit;

--比对student和student_par数据
select s_id,s_name,s_sex,s_birdate from student
minus
select s_id,s_name,s_sex,s_birdate from student_par;

S_ID S_NAME S_SEX S_BIRDATE
---------- -------------------- -------- ------------------
20 student_20 FEMALE 01-SEP-20
25 student_25 MALE 01-AUG-20

--同步数据到临时表
BEGIN
dbms_redefinition.sync_interim_table(
uname => 'PAR',
orig_table => 'STUDENT',
int_table => 'STUDENT_PAR');
END;
/

--数据已全部同步到临时表
select s_id,s_name,s_sex,s_birdate from student
minus
select s_id,s_name,s_sex,s_birdate from student_par;

no rows selected

7.执行DBMS_REDEFINITION.FINISH_REDEF_TABLE()过程完成表的重定义。这个过程中,原始表会被独占模式锁定一小段时间,具体时间和表的数据量有关。

执行完FINISH_REDEF_TABLE()过程后,原始表重定义后具有了中间表的属性、索引、约束、授权和触发器。中间表上disabled的约束在原始表上处于enabled状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
sql复制代码--收集par table的统计信息
EXEC DBMS_STATS.gather_table_stats('PAR', 'STUDENT_PAR', cascade => TRUE);

--结束在线重定义过程
BEGIN
dbms_redefinition.finish_redef_table(
uname => 'PAR',
orig_table => 'STUDENT',
int_table => 'STUDENT_PAR');
END;
/

SQL> select table_name,PARTITION_NAME from user_tab_partitions where table_name in ('STUDENT','STUDENT_PAR');

SQL> select table_name,index_name from user_indexes where table_name in ('STUDENT','STUDENT_PAR');

此时,临时表(及其索引)已成为“真实”表,并且它们的名称已在名称词典中切换。

8.重命名所有约束和索引以匹配原始名称

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
sql复制代码--drop中间表或者rename原来的约束
a.drop table STUDENT_PAR;
b.
ALTER TABLE STUDENT_PAR RENAME CONSTRAINT U_1 TO U_1_20210411;
ALTER TABLE STUDENT_PAR RENAME CONSTRAINT C_1 TO C_1_20210411;
ALTER INDEX S_NAME_IDX RENAME TO S_NAME_IDX_20210411;
ALTER INDEX U_1 RENAME TO U_1_20210411;


--rename 新分区表的约束和索引
ALTER TABLE STUDENT RENAME CONSTRAINT U_1_PAR TO U_1;
ALTER TABLE STUDENT RENAME CONSTRAINT C_1_PAR TO C_1;
ALTER INDEX S_NAME_IDX_PAR RENAME TO S_NAME_IDX;
ALTER INDEX U_1_PAR RENAME TO U_1;

--查看索引,约束名称是否正确
select table_name,index_name from user_indexes where table_name in ('STUDENT','STUDENT_PAR') order by table_name;
TABLE_NAME INDEX_NAME
------------------------------ ------------------------------
STUDENT S_NAME_IDX
STUDENT SYS_C0011401
STUDENT U_1
STUDENT_PAR S_NAME_IDX_20210411
STUDENT_PAR U_1_20210411
STUDENT_PAR SYS_C0011395

SQL> desc stu
Name Null? Type
----------------------------------------- -------- ----------------------------
S_ID NOT NULL NUMBER(8)
S_NAME NOT NULL VARCHAR2(20)
S_SEX VARCHAR2(8)
S_BIRDATE DATE
S_PHONE NUMBER

本文转载自: 掘金

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

工具篇:apache-httpClient 和 jdk11-

发表于 2021-11-13

关注公众号,一起交流,微信搜一搜: 潜行前行

HttpClient (apache)

apache HttpClient 是 java项目里 较为常用的组件之一;对接外部服务时,各个商家提供的接口是各式各样的,有自己的要求,因此要定制对应的请求客户端。httpClient是一个不错的选择

  • apache HttpClient 实现了 HTTP 1.0 和 HTTP 1.1。支持 HTTP 全部的方法(GET, POST, PUT, DELETE, HEAD, OPTIONS, and TRACE)
    • GET, POST 的实现是继承 HttpRequestBase,HttpRequestBase 实现 HttpUriRequest,HttpUriRequest 继承 HttpRequest;GET, POST 方法对应 java 类的 HttpGet 和 HttpPost
  • 支持 TLS,SSL 的 HTTPS。支持多线程操作
  • 基于阻塞的 I/0 实现,也就是说使用 HttpClient 的线程会被阻塞
  • 头部信息设置
1
2
3
java复制代码HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/v3/pay/transactions/app");
httpPost.addHeader("Accept", "application/json");
httpPost.addHeader("Content-type","application/json; charset=utf-8");
  • 证书信息设置
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
java复制代码private static SSLContext getSslContext() throws Exception {
//自身私钥
KeyStore identityKeyStore = KeyStore.getInstance("jks");
FileInputStream identityKeyStoreFile = new FileInputStream("/root/myServer.jks");
identityKeyStore.load(identityKeyStoreFile, "password1".toCharArray());
//服务端信任证书
KeyStore trustKeyStore = KeyStore.getInstance("jks");
FileInputStream trustKeyStoreFile = new FileInputStream("/root/trustKeyStore.jks");
trustKeyStore.load(trustKeyStoreFile, "password".toCharArray());
//构建SSLContexts
return SSLContexts.custom()
.loadKeyMaterial(identityKeyStore, "password1".toCharArray()) // load identity keystore
.loadTrustMaterial(trustKeyStore, null) // load trust keystore
.build();
}
public static void postWithSSL(String url, String jsonBody) throws Exception {
SSLContext sslContext = getSslContext();
SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory(
sslContext, new String[]{"TLSv1.2", "TLSv1.1", "TLSv1"}, null,
SSLConnectionSocketFactory.getDefaultHostnameVerifier());
CloseableHttpClient client = HttpClients.custom()
.setSSLSocketFactory(sslConnectionSocketFactory)
.build();
/**
// HttpClients 产生的 client 都共用相同的证书秘钥
Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
.register("http", PlainConnectionSocketFactory.INSTANCE)
.register("https", new SSLConnectionSocketFactory(sslcontext))
.build();
HttpClients.custom().setConnectionManager(connManager);
*/
....
}
  • 缓存 cookie 设置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码//自定义 cookie
CookieStore cookieStore = new BasicCookieStore();
BasicClientCookie cookie = new BasicClientCookie("csc", "lwl");
cookieStore.addCookie(cookie);
// 从上一次请求获取
HttpPost httppost = ...
DefaultHttpClient httpclient = new DefaultHttpClient();
HttpResponse response = httpclient.execute(httppost);
CookieStore cookiestore=httpclient.getCookieStore();
// DefaultHttpClient 使用 cookie
HttpPost httppost2 = ...
DefaultHttpClient httpclient2 = new DefaultHttpClient();
httpclient2.setCookieStore(cookiestore);
response = httpclient2.execute(httppost2);
  • RequestConfig 的使用
1
2
3
4
5
6
7
8
9
10
java复制代码RequestConfig defaultRequestConfig = RequestConfig.custom()
.setConnectTimeout(5000)
.setSocketTimeout(5000)
.setConnectionRequestTimeout(5000)
.setRedirectsEnabled(true)
.build();
//使用
CloseableHttpClient httpclient = HttpClients.custom()
.setDefaultRequestConfig(defaultRequestConfig)
.build();
  • HttpEntity 是对《请求或者响应》对象的封装,具体实现类有
    • BasicHttpEntity,InputStreamEntity:操作对象是数据流
    • BufferedHttpEntity:带缓冲区的 HttpEntity,其他HttpEntity的包装类,将内容存入一缓存区 可以重复读
    • FileEntity:文件对应的Entity FileEntity entity = new FileEntity(new File(""), "application/java-achive");
    • StringEntity:字符串 Entity。一般用 json ,text/plain,text/xml 类型的post请求
    • UrlEncodedFormEntity,一般用于 application/x-www-form-urlencoded 类型的post请求
  • HttpContext:它是 Http 请求上下文类,如果是同一个上下文,则两次请求间可以共享这个上线文的信息。虽然 HttpClient 本身就具备维护cookies的功能,但 HttpContext 的好处是在于多个 HttpClient 实例之间可以共享 HttpContext

一些建议

  • 1 释放资源:读取完响应后,我们需要尽快释放response本身和响应实体本身的流来对资源进行回收
  • 2 有时可能需要多次读取返回的响应内容,将响应内容进行缓冲。最简单的方法是用BufferedHttpEntity 类包装原始实体。这会让原始实体的内容被读入内存缓冲区
1
2
java复制代码CloseableHttpResponse response = ...
HttpEntity entity = new BufferedHttpEntity(response.getEntity());
  • 3 HttpClient 的线程安全:使用同一个HttpClient的实例即可做到线程安全,因为 HttpClient 内部就有一个池化机制,支持多线程
  • 4 EntityUtils.toString(entity) : 把内容转成字符串

CloseableHttpClient 是 HttpClient 的子类。mvn 引入

1
2
3
4
5
6
7
8
9
10
mvn复制代码<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.5</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpmime</artifactId>
<version>4.5.5</version>
</dependency>

HttpClient 的API

1
2
3
4
5
6
7
8
java复制代码HttpResponse execute(HttpUriRequest request)
HttpResponse execute(HttpUriRequest request, HttpContext context)
HttpResponse execute(HttpHost target, HttpRequest request)
HttpResponse execute(HttpHost target, HttpRequest request, HttpContext context)
<T> T execute(HttpUriRequest request, ResponseHandler<? extends T> responseHandler)
<T> T execute(HttpHost target,HttpRequest request, ResponseHandler<? extends T> responseHandler)
<T> T execute(HttpHost target, HttpRequest request,
ResponseHandler<? extends T> responseHandler, HttpContext context)

get 请求

1
2
3
4
5
6
java复制代码CloseableHttpClient httpclient = HttpClients.createDefault();
HttpGet httpGet = new HttpGet("http://localhost:8080/content/lwl");
CloseableHttpResponse httpResponse = httpClient.execute(httpGet);
HttpEntity httpEntity = httpResponse.getEntity();
System.out.println(EntityUtils.toString(httpEntity));// 输出请求结果
httpResponse.close();

post 请求

1
2
3
4
5
6
7
8
9
10
java复制代码CloseableHttpClient httpclient = HttpClients.createDefault();
HttpPost httpPost = new HttpPost("https://www.baidu.com");
ArrayList<NameValuePair> params = new ArrayList<NameValuePair>();
params.add(new BasicNameValuePair("username", "csc"));
params.add(new BasicNameValuePair("password", "lwl"));
httpPost.setEntity(new UrlEncodedFormEntity(params));
CloseableHttpResponse httpResponse = httpClient.execute(httpPost);
HttpEntity httpEntity = httpResponse.getEntity();
System.out.println(EntityUtils.toString(httpEntity));// 输出请求结果
httpResponse.close();

文件上传

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码CloseableHttpClient httpClient = HttpClientBuilder.create().build();
HttpPost httpPost = new HttpPost("http://localhost:8080/lwl/upload");

MultipartEntityBuilder multipartEntityBuilder = MultipartEntityBuilder.create();
File file1 = new File("C:\\Users\\csc\\Desktop\\data.jpg"); // 第一个文件
multipartEntityBuilder.addBinaryBody("files", file1);
File file2 = new File("C:\\Users\\csc\\Desktop\\头像.jpg"); // 第二个文件
// 为避免中文乱码问题,可以对文件名 urlDecode
multipartEntityBuilder.addBinaryBody("files", file2, ContentType.DEFAULT_BINARY, URLEncoder.encode(file2.getName(), "utf-8"));

// 其它参数
multipartEntityBuilder.addTextBody("name", "lwl", ContentType.create("text/plain", Charset.forName("UTF-8")));

HttpEntity httpEntity = multipartEntityBuilder.build();
httpPost.setEntity(httpEntity);
CloseableHttpResponse response = httpClient.execute(httpPost);
HttpEntity responseEntity = response.getEntity();
System.out.println(response.getStatusLine());
response.close();

HttpClient (jdk11)

java.net.http.HttpClient 是 jdk11 中正式启用的一个 http 工具类(在 jdk9 的时候就已经存在),官方想要取代 HttpURLConnection 和 Apache HttpClient 等比较古老的开发工具

HttpClient 的API

1
2
3
4
5
6
7
8
9
10
11
java复制代码//创建一个 HttpClient
public static Builder newBuilder()
public static HttpClient newHttpClient() // HttpClient.newBuilder().build()
//webSocket协议的请求客户端的构建者
public WebSocket.Builder newWebSocketBuilder()
public abstract Optional<CookieHandler> cookieHandler() // 获取 CookieHandler
public abstract Optional<Duration> connectTimeout()
public abstract Redirect followRedirects()
public abstract Optional<ProxySelector> proxy()
public abstract SSLContext sslContext()
public abstract Optional<Executor> executor()
  • HttpClient.Builder 的 API
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码//缓存cookie设置
public Builder cookieHandler(CookieHandler cookieHandler);
//连接超时时间
public Builder connectTimeout(Duration duration);
// 证书信息设置
public Builder sslContext(SSLContext sslContext);
// SSL / TLS / DTLS连接的参数 设置
public Builder sslParameters(SSLParameters sslParameters);
//涉及到异步操作用到的 线程池
public Builder executor(Executor executor);
// 是否支持重定向 Redirect.SAME_PROTOCOL
public Builder followRedirects(Redirect policy);
// 协议版本,HTTP/1.1 还是 HTTP/2
public Builder version(HttpClient.Version version);
public Builder priority(int priority);
//配置代理
public Builder proxy(ProxySelector proxySelector);
//认证 Authenticator.getDefault()
public Builder authenticator(Authenticator authenticator);
  • HttpClient 调用 API
1
2
3
4
5
6
java复制代码//阻塞调用
<T> HttpResponse<T> send(HttpRequest request, HttpResponse.BodyHandler<T> responseBodyHandler)
//相当于使用了多路复用I/O
<T> CompletableFuture<HttpResponse<T>> sendAsync(HttpRequest request, BodyHandler<T> responseBodyHandler)
abstract <T> CompletableFuture<HttpResponse<T>> sendAsync(HttpRequest request,
BodyHandler<T> responseBodyHandler, PushPromiseHandler<T> pushPromiseHandler)

HttpRequest 构建的 API

对于请求内容可以使用 BodyPublishers 封装的函数生成
image.png

HttpResponse 的API

对于响应的解析读取可以使用 BodyHandlers 或者 BodySubscribers 封装的函数处理
image.png

get 请求

1
2
3
4
5
6
7
java复制代码HttpRequest request = HttpRequest.newBuilder(URI.create("http://localhost:8080/content/lwl"))
.GET()
.timeout(Duration.ofSeconds(10)) // 设置响应超时时间
.build();
HttpClient httpClient = HttpClient.newHttpClient();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());

post 请求

1
2
3
4
5
6
7
8
9
java复制代码String data = .....// json 请求数据
HttpRequest request = HttpRequest.newBuilder(URI.create("https://www.baidu.com"))
.POST(HttpRequest.BodyPublishers.ofString(data, Charset.defaultCharset()))
.header("Content-Type", "application/json") //设置头部信息
.timeout(Duration.ofSeconds(10)) // 设置响应超时时间
.build();
HttpClient httpClient = HttpClient.newHttpClient();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());

欢迎指正文中错误

参考文章

  • HttpClient用法–这一篇全了解
  • HttpClient详细使用示例
  • Java9之HttpClientAPI实战详解
  • JDK 之 HttpClient(jdk11)

本文转载自: 掘金

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

🏆【Alibaba中间件技术系列】「RocketMQ技术专题

发表于 2021-11-13

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

消息重复消费的问题

消息重复消费是各个MQ都会发生的常见问题之一,在一些比较敏感的场景下,重复消费会造成比较严重的后果,比如重复扣款等。

消息重复消费场景及解决办法

在什么情况下会发生RocketMQ的消息重复消费呢?

生产者重复发送场景

当系统的调用链路比较长的时候,比如,系统A调用系统B,系统B再把消息发送到RocketMQ中,在系统A调用系统B的时候。

如果系统B处理成功,但是迟迟没有将调用成功的结果返回给系统A的时候,系统A就会尝试重新发起请求给系统B,造成系统B重复处理,发起多条消息给RocketMQ造成重复消费。

消费者重复发送场景

在系统B发送消息给RocketMQ的时候,也有可能会发生和上面一样的问题,消息发送超时,结果系统B重试,导致RocketMQ接收到了重复的消息。

消费者重复发送场景

当RocketMQ成功接收到消息,并将消息交给消费者处理,如果消费者消费完成后还没来得及提交offset给RocketMQ,自己宕机或者重启了,那么RocketMQ没有接收到offset,就会认为消费失败了,会重发消息给消费者再次消费。

消费者没有立刻返回成功

重复消费的问题的一个可能的问题:消费者消费消息时产生了异常,并没有返回CONSUME_SUCCESS标志。

因为消息处理异常导致的消息重新消费,RocketMQ可以很好的保持消息,一定要消费成功才可以!

官方对comsumerMessage方法
1
vbnet复制代码It is not recommend to throw exception,rather than returning ConsumeConcurrentlyStatus.RECONSUME_LATER if consumption failure

无论如何,都不要抛出异常,如果需要重新消费,可以返回RECONSUME_LATER主动要求重新消费。

catch Exception根异常来捕获业务处理的异常:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码consumer.registerMessageListener(new MessageListenerConcurrently() {
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
ConsumeConcurrentlyContext context) {
logger.debug(Thread.currentThread().getName() + " Receive New Messages: " + msgs + "%n");
MessagePack msgpack = new MessagePack();
for (MessageExt msg : msgs){
byte[] data = msg.getBody();
try {
RTMsgPack rtmsg = msgpack.read(data, RTMsgPack.class);
logger.debug("Receive a message:" + rtmsg);
anlysisRTMsgPack(rtmsg, engine);
} catch (IOException e) {
logger.error("Unpack RTMsg:", e);
} catch (Exception e1){
logger.warn("Unexcepted exception.", e1);
}
}
logger.debug("RETURN CONSUME SUCCESS.");
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});

设置CONSUME_FROM_LAST_OFFSET的问题

Consumer在消费时,会设置从哪里开始消费。默认是CONSUME_FROM_LAST_OFFSET,设置的值如代码所示。

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复制代码public enum ConsumeFromWhere {
/**
* 一个新的订阅组第一次启动从队列的最后位置开始消费<br>
* 后续再启动接着上次消费的进度开始消费
*/
CONSUME_FROM_LAST_OFFSET,
@Deprecated
CONSUME_FROM_LAST_OFFSET_AND_FROM_MIN_WHEN_BOOT_FIRST,
@Deprecated
CONSUME_FROM_MIN_OFFSET,
@Deprecated
CONSUME_FROM_MAX_OFFSET,
/**
* 一个新的订阅组第一次启动从队列的最前位置开始消费<br>
* 后续再启动接着上次消费的进度开始消费
*/
CONSUME_FROM_FIRST_OFFSET,
/**
* 一个新的订阅组第一次启动从指定时间点开始消费<br>
* 后续再启动接着上次消费的进度开始消费<br>
* 时间点设置参见DefaultMQPushConsumer.consumeTimestamp参数
*/
CONSUME_FROM_TIMESTAMP,
}
  • CONSUME_FROM_LAST_OFFSET:从最后的偏移量开始消费,是从该消费者上次消费到的位置开始消费。
+ 如果是一个新的消费者,就要根据这个client所属的消费组的情况来判断。
+ 如果所属的消费者组是新上线的,订阅的消息,最早的消息都没有过,RocketMQ的设计者认为,你这是一个新上线的业务,会强制从第一条消息开始消费。
+ 如果订阅的消息,已经产生了过期消息,那么才会从我们这个client启动的时间点开始消费。

ConsumeFromWhere这个参数只对一个新的消费者第一次启动时有效

  • CONSUME_FROM_FIRST_OFFSET:从最小偏移量开始消费,
  • CONSUME_FROM_TIMESTAMP:从某个时间开始消费。
  • 而判断是不是一个新的ConsumerGroup是在broker端判断。
  • 消费到哪个offset最先是存在Consumer本地的,定时和broker同步自己的消费offset。
  • broker在判断是不是一个新的consumergroup,就是查broker端有没有这个consumergroup的offset记录。

偏移量无效化

对于一个新的queue,这个参数也是没用的,都是从0开始消费。

所以,这就有了一个问题我已经设置了CONSUME_FROM_LAST_OFFSET,为什么还是重复消费了,可能你这不是新的consumergroup,也可能是个新的Queue。

重试队列和死信队列

  • 消费端,一直不回传消费的结果。RocketMQ认为消息没收到,consumer下一次拉取,broker依然会发送该消息。
  • 任何异常都要捕获返回:ConsumeConcurrentlyStatus.RECONSUME_LATER

RocketMQ会放到重试队列,TOPIC是:%RETRY%+COnsumerGroup的名字

  • 重试的消息在延迟的某个时间点(默认是10秒,业务可设置)后,再次投递到这个ConsumerGroup。
  • 而如果一直这样重复消费都持续失败到一定次数(默认16次),就会投递到DLQ死信队列,此时需要人工干预了。
1
2
3
4
5
6
7
8
9
10
11
12
java复制代码/**
Batch consumption size
*/

private int consumeMessageBatchMaxSize = 1;

/**

Batch pull size
*/

private int pullBatchSize = 32;
  • consumeMessageBatchMaxSize 是批量消费的最大条数
  • pullBatchSize 是每次拉取的最大条数

broker端的

1
java复制代码private String messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h";

参数是设置重试的时间,即第一次1s之后,第二次5s之后

生产环境不要改

1
java复制代码messageDelayLevel = 5s 5s 5s 5s 5s 5s 5s 5s 5s 5s 5s 5s 5s 5s 5s 5s 5s 5s

16次之后,多了一个topic名为:%DLQ%+consumergroup

这个默认的16次,可以改,但是使用DefaultMQPullConsumer才可以修改。

DefaultMQPushConsumer不能修改此值。

consumeMessageBatchMaxSize 这个size是消费者注册的回调listener一次处理的消息数,默认是1,不是每次拉取的消息数(默认是32),这个不要搞混。

消息消费进度的更新

未来的文章会进行介绍相关进度更新的功能和分析

本文转载自: 掘金

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

不会吧,你还不会用RequestId看日志 ?

发表于 2021-11-13

引言

在日常的后端开发工作中,最常见的操作之一就是看日志排查问题,对于大项目一般使用类似ELK的技术栈统一搜集日志,小项目就直接把日志打印到日志文件。那不管对于大项目或者小项目,查看日志都需要通过某个关键字进行搜索,从而快速定位到异常日志的位置来进一步排查问题。

对于后端初学者来说,日志的关键字可能就是直接打印某个业务的说明加上业务标识,如果出现问题直接搜索对应的说明或者标识。例如下单场景,可能就直接打印:创建订单,订单编号:xxxx,当有问题的时候,则直接搜索订单编号或者创建订单。在这种方式下,经常会搜索出多条日志,增加问题的排查时长。

所以,今天我们就来说一说这个关键字的设计,这里我们使用RequestId进行精确定位问题日志的位置从而解决问题。

需求

目标: 帮助开发快速定位日志位置

思路:当前端进行一次请求的时候,在进行业务逻辑处理之前我们需要生成一个唯一的RequestId,在业务逻辑处理过程中涉及到日志打印我们都需要带上这个RequestId,最后响应给前端的数据结构同样需要带上RequestId。 这样,每次请求都会有一个RequestId,当某个接口异常则通过前端反馈的RequestId,后端即可快速定位异常的日志位置。

总结下我们的需求:

  • 一次请求生成一次RequestId,并且RequestId唯一
  • 一次请求响应给前端,都需要返回RequestId字段,接口正常、业务异常、系统异常,都需要返回该字段
  • 一次请求在控制台或者日志文件打印的日志,都需要显示RequestId
  • 一次请求的入参和出参都需要打印
  • 对于异步操作,需要在异步线程的日志同样显示RequestId

实现

  1. 实现生成和存储RequestId的工具类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码public class RequestIdUtils {
private static final ThreadLocal<UUID> requestIdHolder = new ThreadLocal<>();
private RequestIdUtils() {
}
public static void generateRequestId() {
requestIdHolder.set(UUID.randomUUID());
}
public static void generateRequestId(UUID uuid) {
requestIdHolder.set(uuid);
}
public static UUID getRequestId() {
return (UUID)requestIdHolder.get();
}
public static void removeRequestId() {
requestIdHolder.remove();
}
}

因为我们一次请求会生成一次RequestId,并且RequestId唯一,所以这里我们使用使用UUID来生成RequestId,并且用ThreadLocal进行存储。

  1. 实现一个AOP,拦截所有的Controller的方法,这里是主要的处理逻辑
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
java复制代码@Aspect
@Order
@Slf4j
public class ApiMessageAdvisor {


@Around("execution(public * org.anyin.gitee.shiro.controller..*Controller.*(..))")
public Object invokeAPI(ProceedingJoinPoint pjp) {
String apiName = this.getApiName(pjp);
// 生成RequestId
String requestId = this.getRequestId();
// 配置日志文件打印 REQUEST_ID
MDC.put("REQUEST_ID", requestId);
Object returnValue = null;
try{
// 打印请求参数
this.printRequestParam(apiName, pjp);
returnValue = pjp.proceed();
// 处理RequestId
this.handleRequestId(returnValue);
}catch (BusinessException ex){
// 业务异常
returnValue = this.handleBusinessException(apiName, ex);
}catch (Throwable ex){
// 系统异常
returnValue = this.handleSystemException(apiName, ex);
}finally {
// 打印响应参数
this.printResponse(apiName, returnValue);
RequestIdUtils.removeRequestId();
MDC.clear();
}
return returnValue;
}

/**
* 处理系统异常
* @param apiName 接口名称
* @param ex 系统异常
* @return 返回参数
*/
private Response handleSystemException(String apiName, Throwable ex){
log.error("@Meet unknown error when do " + apiName + ":" + ex.getMessage(), ex);
Response response = new Response(BusinessCodeEnum.UNKNOWN_ERROR.getCode(), BusinessCodeEnum.UNKNOWN_ERROR.getMsg());
response.setRequestId(RequestIdUtils.getRequestId().toString());
return response;
}

/**
* 处理业务异常
* @param apiName 接口名称
* @param ex 业务异常
* @return 返回参数
*/
private Response handleBusinessException(String apiName, BusinessException ex){
log.error("@Meet error when do " + apiName + "[" + ex.getCode() + "]:" + ex.getMsg(), ex);
Response response = new Response(ex.getCode(), ex.getMsg());
response.setRequestId(RequestIdUtils.getRequestId().toString());
return response;
}

/**
* 填充RequestId
* @param returnValue 返回参数
*/
private void handleRequestId(Object returnValue){
if(returnValue instanceof Response){
Response response = (Response)returnValue;
response.setRequestId(RequestIdUtils.getRequestId().toString());
}
}

/**
* 打印响应参数信息
* @param apiName 接口名称
* @param returnValue 返回值
*/
private void printResponse(String apiName, Object returnValue){
if (log.isInfoEnabled()) {
log.info("@@{} done, response: {}", apiName, JSONUtil.toJsonStr(returnValue));
}
}

/**
* 打印请求参数信息
* @param apiName 接口名称
* @param pjp 切点
*/
private void printRequestParam(String apiName, ProceedingJoinPoint pjp){
Object[] args = pjp.getArgs();
if(log.isInfoEnabled() && args != null&& args.length > 0){
for(Object o : args) {
if(!(o instanceof HttpServletRequest) && !(o instanceof HttpServletResponse) && !(o instanceof CommonsMultipartFile)) {
log.info("@@{} started, request: {}", apiName, JSONUtil.toJsonStr(o));
}
}
}
}

/**
* 获取RequestId
* 优先从header头获取,如果没有则自己生成
* @return RequestId
*/
private String getRequestId(){
// 因为如果有网关,则一般会从网关传递过来,所以优先从header头获取
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if(attributes != null && StringUtils.hasText(attributes.getRequest().getHeader("x-request-id"))) {
HttpServletRequest request = attributes.getRequest();
String requestId = request.getHeader("x-request-id");
UUID uuid = UUID.fromString(requestId);
RequestIdUtils.generateRequestId(uuid);
return requestId;
}
UUID existUUID = RequestIdUtils.getRequestId();
if(existUUID != null){
return existUUID.toString();
}
RequestIdUtils.generateRequestId();
return RequestIdUtils.getRequestId().toString();
}

/**
* 获取当前接口对应的类名和方法名
* @param pjp 切点
* @return apiName
*/
private String getApiName(ProceedingJoinPoint pjp){
String apiClassName = pjp.getTarget().getClass().getSimpleName();
String methodName = pjp.getSignature().getName();
return apiClassName.concat(":").concat(methodName);
}
}

简单说明:

  • 对于RequestId的获取方法 getRequestId,我们优先从header头获取,有网关的场景下一般会从网关传递过来;其次判断是否已经存在,如果存在则直接返回,这里是为了兼容有过滤器并且在过滤器生成了RequestId的场景;最后之前2中场景都未找到RequestId,则自己生成,并且返回
  • MDC.put("REQUEST_ID", requestId) 在我们生成RequestId之后,需要设置到日志系统中,这样子日志文件才能打印RequestId
  • printRequestParam 和 printResponse 是打印请求参数和响应参数,如果是高并发或者参数很多的场景下,最好不要打印
  • handleRequestId 、 handleBusinessException 、 handleSystemException 这三个方法分别是在接口正常、接口业务异常、接口系统异常的场景下设置RequestId
  1. 日志文件配置
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
xml复制代码<contextName>logback</contextName>
<springProperty scope="context" name="level" source="logging.level.root"/>
<springProperty scope="context" name="path" source="logging.file.path"/>


<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<Target>System.out</Target>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter" >
<level>DEBUG</level>
</filter>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{REQUEST_ID}] [%thread] [%-5level] [%logger{0}:%L] : %msg%n</pattern>
</encoder>
</appender>

<appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${path}</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${path}.%d{yyyy-MM-dd}.zip</fileNamePattern>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{REQUEST_ID}] [%thread] [%-5level] [%logger{0}:%L] : %msg%n</pattern>
</encoder>
</appender>

<root level="${level}">
<appender-ref ref="console"/>
<appender-ref ref="file"/>
</root>

这里是一个简单的日志格式配置文件,主要是关注[%X{REQUEST_ID}], 这里主要是把RequestId在日志文件中打印出来

  1. 解决线程异步场景下RequestId的打印问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码public class MdcExecutor implements Executor {
private Executor executor;
public MdcExecutor(Executor executor) {
this.executor = executor;
}
@Override
public void execute(Runnable command) {
final String requestId = MDC.get("REQUEST_ID");
executor.execute(() -> {
MDC.put("REQUEST_ID", requestId);
try {
command.run();
} finally {
MDC.remove("REQUEST_ID");
}
});
}
}

这里是一个简单的代理模式,代理了Executor,在真正执行的run方法之前设置RequestId到日志系统中,这样子异步线程的日志同样可以打印我们想要的RequestId

测试效果

  • 登录效果
    image.png

image.png

  • 正常的业务处理

image.png

image.png

  • 发生业务异常

image.png

image.png

  • 发生系统异常

image.png

image.png

  • 异步线程

image.png

image.png

最后

通过以上骚操作,同学,你知道怎么使用RequestId看日志了吗?

还不懂的话,请看完整源码地址:gitee.com/anyin/shiro…

有问题的话,欢迎添加个人微信好友进行讨论,个人微信:daydaycoupons

本文转载自: 掘金

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

每周一个linux命令(tar)

发表于 2021-11-13

基础环境

2ZSfGb

tar命令介绍

tar命令是linux非常使用频率非常高的一个命令,比如:离线软件包的解压缩、将一个目录打包备份、将一个压缩包解压到一个指定的目录。tar命令主要用来将一个或者多个目录以及一个或者多个文件打包到一个以后缀为tar的文件里,同时也可以将归档的文件压缩成以tar.gz结尾的文件。可以将一个tar或者tar.gz结尾的文件解压到指定的目录下。使用不带界面的linux系统时,使用频率更高,是必须掌握的一个命令。

tar命令格式

解压缩命令

1
shell复制代码tar -zxvf test.tar.gz

压缩命令

1
shell复制代码tar -zcvf test.tar.gz ./

tar命令的常用参数

1
2
3
4
5
6
shell复制代码 -c, --create               创建一个新归档
-x, --extract, --get 从归档中解出文件
-f, --file=ARCHIVE 使用归档文件
-z, --gzip, --gunzip, --ungzip 通过 gzip 过滤归档
-C, --directory=DIR 改变至目录 DIR
-v, --verbose 详细地列出处理的文件

将当前目录下的所有文件压缩

1
shell复制代码tar -zcvf test.tar.gz ./

命令说明:

​ test.tar.gz 目录被压缩后的文件名字

./ 代表被压缩的目录,压缩当前目录

-zcvf 参照上边的参数说明

FwdFyL

889E2c

wZPNA9

将压缩文件解压到当前目录下

1
shell复制代码tar -zxvf test.tar.gz

说明:

​ test.tar.gz 将要被解压的备份文件

​ -zxvf 参数参照上边的参数说明,压缩与解压的唯一区别就是解压参数为x,压缩参数为c

​ 解压后的路径,默认为当前路径

image-20211113092134883

将备份文件解压到指定路径下

1
sh复制代码 tar -zxvf test.tar.gz -C /home/

说明:

​ test.tar.gz 将要被解压的备份文件

​ -zxvf 参数参照上边的参数说明,压缩与解压的唯一区别就是解压参数为-x,压缩参数为-c

​ -C解压到指定路径/home下

image-20211113092523169

将指定目录下的文件打包,不包含目录结构

1
shell复制代码 tar -zcvf test.tar.gz -C /home/testDir/ .

说明:

​ test.tar.gz 压缩后的文件名字

​ -zxvf 参数参照上边的参数说明,压缩与解压的唯一区别就是解压参数为-x,压缩参数为-c

​ -C 压缩时,使用相对路径,不包含目录结果 /home/testDir/下

​ . 压缩相对目录后的所有文件

image-20211113095100122

本文转载自: 掘金

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

ASCII, UTF8, Unicode, and Rune

发表于 2021-11-13

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

本篇翻译自《Practical Go Lessons》 Chapter 7: Hexadecimal, octal, ASCII, UTF8, Unicode, Runes

1 你将在本章中学到什么?

  • 什么是 Unicode、ASCII 和 UTF-8?
  • 字符是如何存储的。
  • 什么是符文类型?

2 涵盖的技术概念

  • ASCII
  • UTF8
  • Hexadecimal 十六进制
  • Octal 八进制
  • Rune
  • Code Point

3 介绍

本章将讨论十六进制和八进制。我们还将讨论 ASCII 和 UTF-8。

4 Base 16:十六进制表示

要表示一个二进制数,你需要很多零和一来组合。这个表示很很长。为了表示十进制数 1324,我们需要使用 11 个二进制字符。因此我们需要更简洁的表示方法。

1
2
python复制代码In [1]: bin(1324)
Out[1]: '0b10100101100'

十六进制(Hexadecimal)也是一种位置数字系统,它使用 16 个字符来表示一个数字。

  • 前缀 Hexa 在拉丁语中的意思是 6
  • Decimal 来自拉丁词 Decem,意思是 10

十六进制的字符是数字和字母。我们使用从 0 到 9(10 个字符)的数字和从 A 到 F(6 个字符)的字母。

image
上图是 16 进制 与 10 进制之间的关系表,0 到 9 的数字对应十进制系统中的相同值,字母 A 对应于 10,字母 B 对应于 11 …等。这是十六进制数字系统的特点;我们使用字母来表示数值(节省表示的空间)。

这也许会给我们带来一些疑惑,我们必须尝试去接受它,我们需要更多的字符所以我们拿了字母…
你可以看到我们在这个符号中引入了字母。那是因为从 0 到 9,你有十个字符、十个数字,但是对于基数为 16 的编号系统,我们还需要六个字符。这就是为什么我们采用了字母表的前六个字母。这是历史的选择;其他字符可以替换字母,系统将仍然相同。

首先明确,无论是10 进制的 1324 还是 16 进制的 52C,它们表示的含义都是同一个数量。
如果我们将一个 16 进制数转换成 一个 10进制数,计算方式是:从 16 进制数的低位(右边)开始,逐步向高位(左边),取每位的字符对应 10 进制数值,与 16 的 n次幂相乘(这里的 n 不是固定的,它等于该字符所在的位序 - 1,位序指从低位到高位的排序),最后将每位的计算结果相加就是 10 进制的值了。
举个例子:10 进制的 1324 相当于 16 进制的 52C

1
2
3
4
5
6
7
8
9
shell复制代码# 十六进制 5 2 C
1. 计算 'C':
因为 'C' 对应的10进制值是12,它所在的位序是1,即它是右边第1位,所以它的值为:12*(16^(1-1)) = 12
2. 计算'2':
因为 '2' 对应的10进制值是2,它所在的位序是2,即它是右边第2位,所以它的值为:2*(16^(2-1)) = 32
3. 计算'5':
因为 '5' 对应的10进制值是5,它所在的位序是5,即它是右边第3位,所以它的值为:5*(16^(3-1)) = 1280
4. 将每位结果相加:
12 + 32 + 1280 = 1224

image

在 Go 中,如果你想打印数据的 16 进制表示,可以使用fmt函数:

1
2
3
4
5
6
7
8
go复制代码package main

import "fmt"

func main() {
n := 2548
fmt.Printf("%x", n)
}

这个程序的输出是9f4(即十进制数字 2458 对应的 16 进制表示)。”%x” 是十六进制的格式化动词,它会用小写方式展示。
如果你将格式化动词改成”%X”,就可以打印出大写的十六进制:9F4。

注意代码片段中的n是十进制表示,即代码中数值默认都是十进制表示。如果你想在代码中表示十六进制数值,需要在数值前面加上0x的标识:

1
2
3
4
5
6
7
8
9
10
go复制代码package main

import "fmt"

func main() {
n := 2548
n2 := 0x9F4
fmt.Printf("%X\n", n)
fmt.Printf("%x\n", n2)
}

输出:

1
2
shell复制代码9F4
9f4

如何你想让0x9F4以十进制的方式打印出来,你可以是格式化动词”%d”:

1
2
3
4
5
6
7
8
go复制代码package main

import "fmt"

func main() {
n2 := 0x9F4
fmt.Printf("Decimal : %d\n", n2)
}

输出:

1
shell复制代码Decimal: 2548

5 Base8:八进制表示

差点忘记介绍八进制了。它使用基数 8,这意味着八个不同的字符。选择了从 0 到 7 的数字。十进制到八进制的转换和我之前介绍的方法类似。让我们举个例子:
image
我们从最右边的字符开始,将它乘以 0 的 8 次方,即 1。然后我们取下一个字符:5 将其乘以 8 的 1 次方,即 8……
要知道,Unix 操作系统中的文件权限就是通过八进制表示的。

在 Go 中,通过增加前缀0或者0o来表示数值是八进制。和十六进制一样,fmt 包也为八进制提供两种格式化动词:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
go复制代码package main

import "fmt"

func main() {
n2 := 0x9F4
fmt.Printf("Decimal : %d\n", n2)

// n3 is represented using the octal numeral system
n3 := 02454
// alternative : n3 := 0o2454

// convert in decimal
fmt.Printf("decimal: %d\n", n3)

// n4 is represented using the decimal numeral system
n4 := 1324
// output n4 (decimal) in octal
fmt.Printf("octal: %o\n", n4)
// output n4 (decimal) in octal (with a 0o prefix)
fmt.Printf("octal with prefix : %O\n", n4)

}

输出:

1
2
3
4
shell复制代码Decimal : 2548
decimal: 1324
octal: 2454
octal with prefix : 0o2454

6 数据的表示方式:bit、nibble、bytes 和 word

bit 实际是 Binary digit 的缩写,它只有一位,用 0 和 1 来表示,可以表示2种数据,可通过组合在一起来表示更多的数据内容。比如:10100101100 由 11 位 bit 组成。这种组合方式有很多种:

  • 一个 nilbble 表示由 4 个比特构成
  • 一个 byte 由 8 个比特构成
  • 一个 word 由 16 个比特构成
  • 一个 doubleword 由 32 个比特构成
  • 一个 quadword 由 64 个比特构成(等价于 4 个 word)

使用 Go,你可以创建一个字节切片。许多常见的标准包函数和方法都将字节片作为参数。让我们看看如何创建字节切片:

1
2
3
4
5
6
7
8
9
10
go复制代码package main

import "fmt"

func main() {
b := make([]byte, 0)
b = append(b, 255)
b = append(b, 10)
fmt.Println(b)
}

在上面的代码片段中,我们创建了一个字节切片(使用内置 make),然后我们将两个数字附加到切片中。

Golang 字节类型是 uint8 的别名。 Uint8 意味着我们可以在 8 位(一个字节)的数据上存储无符号(没有任何符号,所以没有负数)整数。uint8 的最小值是 0,最大值是 255(即 8 位都是1)。

这就是为什么我们只能将 0 到 255 之间的数字追加到一个字节片中。如果你尝试追加一个大于 255 的数字,你将收到以下错误:

1
shell复制代码./prog.go:7:15: constant 256 overflows byte

如果你想以二进制来打印数值,可以用”%b”格式化动词:

1
2
3
4
5
6
7
8
9
go复制代码package main

import "fmt"

func main() {
n2 := 0x9F4
fmt.Printf("Decimal : %d\n", n2)
fmt.Printf("Binary : %b\n", n2)
}

输出:

1
2
shell复制代码Decimal : 2548
Binary : 100111110100

7 其他字符呢?

如果你想存储数字以外的东西怎么办?例如,我们如何存储 Masaoki Shiki 的这个俳句:

1
2
3
text复制代码spring rain:
browsing under an umbrella
at the picture-book store

字节类型是否合适?一个字节只不过是一个存储在 8 位上的无符号整数。这个俳句由字母和特殊字符组成。我们有一个“:”和一个“-”,我们还有换行符……我们如何存储这些字符?之前提到的十六进制、八进制或者二进制,如何来表示这个俳句?

我们必须想办法给每个字母甚至特殊字符一个唯一的代码。你可能听说过 UTF-8、ASCII、Unicode?本节将解释它们是什么以及它们是如何工作的。我开始编程时(那不是在 Go 中),字符编码是一种晦涩的东西,我觉得它并不有趣,但字符编码可能是必不可少的,因为我在工作上曾经花了几个晚上的时间来解决可以来解决关于字符的问题。

字符编码的历史非常悠久。随着电报的发展,我们需要一种可以在电线上传输的方式来编码消息。最早的尝试之一是摩尔斯电码。它由四个符号组成:短信号、长信号、短空格、长空格(来自维基百科)。字母表中的每个字母都可以用莫尔斯编码。例如,A 被编码为一个短信号,然后是一个长信号。加号“+”被编码为“short long short long short”。
image

8 名词

我们需要定义一个通用词汇来理解字符编码:

  • Character 字符 这可以由我们手写。它传达了一种意义。例如,符号“+”是一个字符。这意味着在其他东西上添加一些东西。字符可以是字母、符号或表意文字。
  • Character set 字符集:这是不同字符的集合。你经常会看到或听到缩写“charset”。
  • Code point 码点:字符集中的每个字符作为唯一标识该字符的等效数值。这个数值是一个码点。

9 字符集与编码

有一种字符集你必须得知道:Unicode。这是一个标准,列出了当今计算机上使用的生活语言中的绝大多数字符。

在它的版本 11.0 中由 137,374 个字符组成。 Unicode 就像一个巨大的表格,将一个字符映射到一个代码点。例如,字符“A”被映射到代码点“0041”。

有了 Unicode,我们有了基础的字符表,现在下一个问题是找到一种方法来对这些字符进行编码,将这些代码点放入数据字节中。这正是 ASCII 和 UTF-8 所做的事情。
image
image

10 ASCII 如何工作?

ASCII 表示美国信息交换标准代码。它是在六十年代发展起来的。目标是找到一种方法来对用于传输消息的字符进行编码。
ASCII 使用七个二进制数字上编码字符,另一个二进制数字是奇偶校验位。奇偶校验位用于检测传输错误。加在前7位之后,值为0。如果1的个数为奇数,则奇偶校验位为1;如果个数是偶数,则设置为 0。

一个字节的数据正好可以存储每个字符。使用7 个bit能表示多少个整数呢?
用一个比特,我们可以编码两个值,0 和 1,用 2 个比特,我们可以编码四个不同的值。当你添加一点时,你可以将可以编码的值的数量乘以 2。使用 7 位,你可以编码 128 个整数。一般来说,可以用 n 个二进制数字编码的无符号整数的数量是 n 次幂的 2。

1
2
3
4
5
6
7
8
text复制代码Number of bits	Number of values
1 2
2 4
3 8
4 16
5 32
6 64
7 128

所以,ASCII 允许你对 128 个不同的字符进行编码。对于每个字符,我们都有一个特定的代码点。无符号整数值表示代码点。
image
在上图 中,你可以看到 USASCII 代码图表。此表帮助你将字节转换为字符。例如,字母 B 相当于 1000010(二进制)(第 4 列,第 2 行)

11 UTF-8 如何工作?

UTF-8 表示 Universal Character Set Transformation Format1 8 比特。它是由Rob Pike 和 Ken Thompson发明的(他们两人也是 Go 的创造者!)这种编码的设计非常巧妙。我将尝试简要解释一下:

UTF-8 是一种可变宽度的编码系统。这意味着字符使用一到四个字节进行编码(一个字节代表八个二进制数字)。
image
从上图,你可以看到 UTF-8 的编码规则。一个字符可以编码为 1 到 4 个字节。

使用一个字节只可以进行编码的码点是从 U+0000 到 U+007F(包括在内)。该范围由 128 个字符组成。(从0到127,一共有128个数字。

但是需要编码更多的字符!毕竟它有 137,374 个。我们需要用两个字节来表示 U+0080 及其之后的码点,甚至更多的字节来表示更大的码点。而且,我们也需要知道当前这个码点用来几个字节来表示,这样我们在解码是就可以尽可能的节省时间了。
这就是为什么 UTF-8 的创建者添加了固定的字节来标识。第一个附加字节用 1 比特开头,值为“0”;那些是固定的。我们现在使用 2 个字节来编码我们的字符时,我们只需添加固定为“110”。它对 UTF-8 解码器说:“小心;我们是2!”。

如果我们使用 2 个字节,我们有 11 位空闲(8 * 2 - 5(固定位)=11)。我们可以对包含从 U+0080 到 U+07FF 的 Unicode 代码点的字符进行编码。那代表多少个字符

  • 十六进制 0080 = 十进制 128
  • 十六进制 07FF = 十进制 2047
  • 从 0080 到 07FF 有 2047-128+1=1920

你可能会问为什么我们要给计数加一……那是因为字符是从代码点 0 开始索引的。

如果使用 3 个字节,则第一个字节将从固定位1110开始。这将向解码器发出信号,该字符是使用 3 个字节编码的。换句话说,下一个字符将在第三个字节之后开始。附加的两个字节以10开头。使用三个编码字节,你有 16 位空闲(8 * 3 - 8(固定位)=16)。您可以将字符从 U+0800 编码到 U+FFFF。

如果你已经了解了 3 个字节是如何工作的,那么了解系统如何使用 4 个字节应该没有问题。在我们的第一个字节中,我们固定了前五个位 (11110)。然后我们有三个额外的字节。如果我们从总位数中减去固定位,我们就有 21 位可用。这意味着我们可以将代码点从 U+10000 编码到 U+10FFFF。

字节数 编码范围
1 U+0000~U+007F
2 U+0080~U+07FF
3 U+0800~U+FFFF
4 U+10000~U+10FFFF

12 字符串

字符串是“一串字符序列”。例如,“Test”是一个由 4 个不同字符组成的字符串:T、e、s 和 t。字符串很流行;我们使用它们在我们的程序中存储原始文本。它们通常是人类可读的,例如,应用程序用户的名字和姓氏是两个字符串。

字符可以来自不同的字符集。如果使用字符集 ASCII,则只能从 128 个可用字符中进行选择。

每个字符在字符集中都有一个对应的代码点。正如我们之前看到的,代码点是一个任意选择的无符号整数。字符串使用字节存储。让我们以仅由 ASCII 字符组成的字符串为例:

1
text复制代码Hello

单个字节可以存储每个字符。该字符串可以由下面的比特存储:

1
text复制代码01001000 01100101 01101100 01101100 01101111

image

顺便提一下,在 Go 中,字符串是不可变的,这意味着它们一旦创建就无法修改。

13 String literals

字符串文字有两类:

  • 原生字符串文字。它们被定义在反引号之间。
    • 禁止字符是 反引号
    • 丢弃的字符是 回车符 (\r)
  • 解释型字符串文字。它们被定义在双引号之间。
    • 禁止字符是 换行、未转义的双引号
1
2
3
4
5
6
7
8
9
10
11
12
13
14
go复制代码package main

import "fmt"

func main() {

raw := `spring rain:
browsing under an umbrella
at the picture-book store`
fmt.Println(raw)

interpreted := "i love spring"
fmt.Println(interpreted)
}

你可能注意到在这段代码中,我们没有告诉 Go 我们使用哪个字符集。这是因为字符串文字是使用 UTF-8 隐式编码的。

14 Runes

字符串是字节的集合。我们可以使用 for 循环遍历字符串的字节:

1
2
3
4
5
6
7
8
9
10
go复制代码package main

import "fmt"

func main() {
s := "我爱 Golang"
for _, v := range s {
fmt.Printf("Unicode code point: %U - character '%c' - binary %b - hex %X - Decimal %d\n", v, v, v, v, v)
}
}

输出:

1
2
3
4
5
6
7
8
9
sql复制代码Unicode code point: U+6211 - character '我' - binary 110001000010001 - hex 6211 - Decimal 25105
Unicode code point: U+7231 - character '爱' - binary 111001000110001 - hex 7231 - Decimal 29233
Unicode code point: U+0020 - character ' ' - binary 100000 - hex 20 - Decimal 32
Unicode code point: U+0047 - character 'G' - binary 1000111 - hex 47 - Decimal 71
Unicode code point: U+006F - character 'o' - binary 1101111 - hex 6F - Decimal 111
Unicode code point: U+006C - character 'l' - binary 1101100 - hex 6C - Decimal 108
Unicode code point: U+0061 - character 'a' - binary 1100001 - hex 61 - Decimal 97
Unicode code point: U+006E - character 'n' - binary 1101110 - hex 6E - Decimal 110
Unicode code point: U+0067 - character 'g' - binary 1100111 - hex 67 - Decimal 103

image

上图中的消息是“我爱 Golang”,前两个字符是中文。

程序将遍历字符串的每个字符。在 for 循环中 v 的类型是rune。rune 是一个内置类型,定义如下:

1
2
3
shell复制代码// rune is an alias for int32 and is equivalent to int32 in all ways. It is
// used, by convention, to distinguish character values from integer values.
type rune = int32

一个Rune代表一个 Unicode 码点。

  • Unicode 代码点是数值。
  • 按照惯例,它们总是用以下格式表示:“U+X”,其中 X 是代码点的十六进制表示。 X 应该有四个字符。
  • 如果 X 少于四个字符,我们添加零。
  • 例如:字符“o”的代码点等于 111(十进制)。十六进制的 111 写成 6F。十进制码点为 U+006F
    要以常规格式打印代码点,你可以使用格式动词“%U”。
    image

你也可以使用单引号来创建一个rune:

1
2
3
4
5
6
7
8
go复制代码package main

import "fmt"

func main(){
var aRune rune = 'Z'
fmt.Printf("Unicode Code point of &#39;%c&#39;: %U\n", aRune, aRune)
}

15 随堂测试

15.1 问题

  1. 判断对错:“785G”是一个十六进制数字
  2. 判断对错:“785f”和“785F”代表相同的数量
  3. 表示十六进制数(带有大写字母)的格式化动词是什么?
  4. 用十进制表示数字的格式化动词是什么?
  5. 什么是码点?
  6. 填空白。 ______ 是一个字符集,______ 是一个编码标准。
  7. 判断对错:UTF-8 允许你编码比 ASCII 更少的字符。
  8. 我可以使用多少字节来使用 UTF-8 编码系统对字符进行编码?

15.2 答案

  1. 判断对错:“785G”是一个十六进制数字

错误,因为字母 G 不属于十六进制数。字母 A 到 F 可以是十六进制数的一部分。
2. 判断对错:“785f”和“785F”代表相同的数量

正确,字母大小写的含义都相同。
3. 表示十六进制数(带有大写字母)的格式化动词是什么?

%X
4. 用十进制表示数字的格式化动词是什么?

%d
5. 什么是码点?

代码点是一个数字值,用于标识字符集中的一个字符
6. 填空白。 ______ 是一个字符集,______ 是一个编码标准。

Unicode 是一个字符集,UTF-8 是一个编码标准。
7. 判断对错:UTF-8 允许你编码比 ASCII 更少的字符

错误,UTF-8 最少用一个字节,ASCII 也是用一个字节
8. 我可以使用多少字节来使用 UTF-8 编码系统对字符进行编码?

可以使用 1~4 个字节,这取决于字符的码点

关键要点

  • 十六进制是一种类似于十进制和二进制的计数系统
  • 使用十六进制,数字用 16 个字符“0、1、2、3、4、5、6、7、8、9、A、B、C、D、E、F”表示
  • 使用 fmt 函数(fmt.Sprintf 和 fmt.Printf),你可以使用“格式化动词”来表示使用特定数字系统的数字
    • %b 二进制
    • %x或%X 十六进制
    • %d 十进制
    • %o 八进制
  • Character 是我们可以用手书写的东西,它传达了一种含义。例如:“-”、“A”、“a”
  • Character set::是不同字符的集合。你经常会看到或听到缩写“charset”。
  • Code point:字符集中的每个字符作为唯一标识该字符的等效数值。该数值是一个码点。
  • Unicode 是由 137000 + 个字符组成的字符集。
  • Unicode 中的每个字符都有一个码点。例如“A”字符相当于代码点U+0041。
  • ASCII 是一种只能编码 128 个字符的编码技术。
  • UTF-8 是一种编码技术,可以编码超过 100 万个字符。
  • 使用 UTF-8,任何字符都使用 1 到 4 个字节进行编码。
  • rune 是一种内置类型
  • rune 表示字符的 Unicode 代码点。
  • 要创建一个 rune,你可以使用单引号:
1
go复制代码var aRune rune = 'Z'
  • 当你遍历一个字符串时,您将遍历所有 rune
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
go复制代码package main

import "fmt"

func main() {
b := "hello"
for i := 0; i < len(b); i++ {
fmt.Println(b[i])
}
// will output :
// 104
// 101
// 108
// 108
// 111
// and NOT :
// h
// e
// l
// l
// o
}
  • 在 Go 中,字符串是不可变的,这意味着它们一旦创建就无法更改。

参考书目

  • Character Encodings: Essential Concepts Character Encodings: Essential Concepts.
  • Unicode V11.0

本文转载自: 掘金

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

初识 Nginx

发表于 2021-11-13

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

背景介绍

Nginx(“engine x”)一个具有高性能的【HTTP】和【反向代理】的【WEB服务器】,同时也是一个【POP3/SMTP/IMAP代理服务器】,是由伊戈尔·赛索耶夫(俄罗斯人)使用C语言编写的,Nginx的第一个版本是2004年10月4号发布的0.1.0版本。另外值得一提的是伊戈尔·赛索耶夫将Nginx的源码进行了开源,这也为Nginx的发展提供了良好的保障。

1. WEB服务器:

WEB服务器也叫网页服务器,英文名叫Web Server,主要功能是为用户提供网上信息浏览服务。

2. HTTP:

HTTP是超文本传输协议的缩写,是用于从WEB服务器传输超文本到本地浏览器的传输协议,也是互联网上应用最为广泛的一种网络协议。HTTP是一个客户端和服务器端请求和应答的标准,客户端是终端用户,服务端是网站,通过使用Web浏览器、网络爬虫或者其他工具,客户端发起一个到服务器上指定端口的HTTP请求。

3. POP3/SMTP/IMAP:

POP3(Post Offic Protocol 3)邮局协议的第三个版本,

SMTP(Simple Mail Transfer Protocol)简单邮件传输协议,

IMAP(Internet Mail Access Protocol)交互式邮件存取协议,

通过上述名词的解释,我们可以了解到Nginx也可以作为电子邮件代理服务器。

4. 反向代理

正向代理:

111.jpg

反向代理:

111.jpg

常见服务器对比

Tomcat

Tomcat是一个运行Servlet和JSP的Web应用软件,Tomcat技术先进、性能稳定而且开放源代码,因此深受Java爱好者的喜爱并得到了部分软件开发商的认可,成为目前比较流行的Web应用服务器。但是Tomcat天生是一个重量级的Web服务器,对静态文件和高并发的处理比较弱。

Apache

Apache的发展时期很长,同时也有过一段辉煌的业绩。从上图可以看出大概在2014年以前都是市场份额第一的服务器。Apache有很多优点,如稳定、开源、跨平台等。但是它出现的时间太久了,在它兴起的年代,互联网的产业规模远远不如今天,所以它被设计成一个重量级的、不支持高并发的Web服务器。在Apache服务器上,如果有数以万计的并发HTTP请求同时访问,就会导致服务器上消耗大量能存,操作系统内核对成百上千的Apache进程做进程间切换也会消耗大量的CUP资源,并导致HTTP请求的平均响应速度降低,这些都决定了Apache不可能成为高性能的Web服务器。这也促使了Lighttpd和Nginx的出现。

Nginx的优点

(1)速度更快、并发更高

单次请求或者高并发请求的环境下,Nginx都会比其他Web服务器响应的速度更快。一方面在正常情况下,单次请求会得到更快的响应,另一方面,在高峰期(如有数以万计的并发请求),Nginx比其他Web服务器更快的响应请求。Nginx之所以有这么高的并发处理能力和这么好的性能原因在于Nginx采用了多进程和I/O多路复用(epoll)的底层实现。

(2)配置简单,扩展性强

Nginx的设计极具扩展性,它本身就是由很多模块组成,这些模块的使用可以通过配置文件的配置来添加。这些模块有官方提供的也有第三方提供的模块,如果需要完全可以开发服务自己业务特性的定制模块。

(3)高可靠性

Nginx采用的是多进程模式运行,其中有一个master主进程和N多个worker进程,worker进程的数量我们可以手动设置,每个worker进程之间都是相互独立提供服务,并且master主进程可以在某一个worker进程出错时,快速去”拉起”新的worker进程提供服务。

(4)热部署

现在互联网项目都要求以7*24小时进行服务的提供,针对于这一要求,Nginx也提供了热部署功能,即可以在Nginx不停止的情况下,对Nginx进行文件升级、更新配置和更换日志文件等功能。

(5)成本低、BSD许可证

BSD是一个开源的许可证,世界上的开源许可证有很多,现在比较流行的有六种分别是GPL、BSD、MIT、Mozilla、Apache、LGPL。这六种的区别是什么,我们可以通过下面一张图来解释下:

111.jpg

Nginx本身是开源的,我们不仅可以免费的将Nginx应用在商业领域,而且还可以在项目中直接修改Nginx的源码来定制自己的特殊要求。这些点也都是Nginx为什么能吸引无数开发者继续为Nginx来贡献自己的智慧和青春。OpenRestry [Nginx+Lua] Tengine[淘宝]

Nginx的功能特性及常用功能

Nginx提供的基本功能服务从大体上归纳为”基本HTTP服务”、“高级HTTP服务”和”邮件服务”等三大类。

基本HTTP服务

Nginx可以提供基本HTTP服务,可以作为HTTP代理服务器和反向代理服务器,支持通过缓存加速访问,可以完成简单的负载均衡和容错,支持包过滤功能,支持SSL等。

  • 处理静态文件、处理索引文件以及支持自动索引;
  • 提供反向代理服务器,并可以使用缓存加上反向代理,同时完成负载均衡和容错;
  • 提供对FastCGI、memcached等服务的缓存机制,,同时完成负载均衡和容错;
  • 使用Nginx的模块化特性提供过滤器功能。Nginx基本过滤器包括gzip压缩、ranges支持、chunked响应、XSLT、SSI以及图像缩放等。其中针对包含多个SSI的页面,经由FastCGI或反向代理,SSI过滤器可以并行处理。
  • 支持HTTP下的安全套接层安全协议SSL.
  • 支持基于加权和依赖的优先权的HTTP/2

高级HTTP服务

  • 支持基于名字和IP的虚拟主机设置
  • 支持HTTP/1.0中的KEEP-Alive模式和管线(PipeLined)模型连接
  • 自定义访问日志格式、带缓存的日志写操作以及快速日志轮转。
  • 提供3xx~5xx错误代码重定向功能
  • 支持重写(Rewrite)模块扩展
  • 支持重新加载配置以及在线升级时无需中断正在处理的请求
  • 支持网络监控
  • 支持FLV和MP4流媒体传输

邮件服务

Nginx提供邮件代理服务也是其基本开发需求之一,主要包含以下特性:

  • 支持IMPA/POP3代理服务功能
  • 支持内部SMTP代理服务功能

Nginx常用的功能模块

1
2
3
4
5
6
7
8
9
10
markdown复制代码静态资源部署
Rewrite地址重写
正则表达式
反向代理
负载均衡
轮询、加权轮询、ip_hash、url_hash、fair
Web缓存
环境部署
高可用的环境
用户认证模块...

Nginx的核心组成

1
2
3
4
lua复制代码nginx二进制可执行文件
nginx.conf配置文件
error.log错误的日志记录
access.log访问日志记录

本文转载自: 掘金

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

分布式事务(1) 基础理论

发表于 2021-11-13

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

一、本地事务

事务可以看作是一次大的活动,它由不同的小活动组成,这些活动要么全部成功,要么全部失败。

而在计算机系统种,更多的是通过关系型数据库来控制事务,这是利用数据库本身的事务特性来实现的,这种基于关系型数据库的事务被称为本地事务。

为什么需要本地事务呢?

事务问题一般都会拿转账来举例子。假如 A 账户 转账 100元给 B 账户。站在用户角度来看,这是一个单一的逻辑,而在关系型数据库中,至少要分成两个步骤来完成:

  • 将 A 账户的存款减少 100元
  • 将 B 账户的存款增加 100 元

而本地事务即数据库事务的目的就是保证这两个步骤要么同时全部成功,要么全部失败。

当数据库操作失败或者系统出现崩溃的时候,系统能够以事务来进行恢复,保证数据库状态的一致性。

回顾数据库事务的四大特性 ACID :

  • 原子性(Atomicity): 事务中的所有操作作为一个整体像原子一样不可分割,要么全部成功,要么全部失败。
  • 一致性(Consistency): 事务的执行结果必须使数据库从一个一致性状态到另一个一致性状态。
+ 一致性状态是指:
    - 1.系统的状态满足数据的完整性约束(主码,参照完整性,check约束等)
    - 2.系统的状态反应数据库本应描述的现实世界的真实状态,比如转账前后两个账户的金额总和应该保持不变。
  • 隔离性(Isolation) : 并发执行的事务不会相互影响,其对数据库的影响和它们串行执行时一样。比如多个用户同时往一个账户转账,最后账户的结果应该和他们按先后次序转账的结果一样。
  • 持久性(Durability) : 事务一旦提交,其对数据库的更新就是持久的。任何事务或系统故障都不会导致数据丢失。

img

事务的并发读问题:

  • 脏读:读取到另一个事务未提交数据;
  • 不可重复读:两次读取不一致;
  • 幻读(虚读):读到另一事务已提交数据。

四大隔离级别:在相同的数据环境下,使用相同的输入,执行相同的工作根据不同的隔离级别,可以导致不同的结果。

  • SERIALIZABLE(序列化)

不会出现任何并发问题,因为它是对同一数据的访问是串行的,非并发访问的;

性能最差;最高级别

  • REPEATABLE READ(可重复读)(MySQL)

防止脏读和不可重复读,不能处理幻读问题;

性能比SERIALIZABLE好

  • READ COMMITTED(读已提交数据)(Oracle)

防止脏读,没有处理不可重复读,也没有处理幻读;

性能比REPEATABLE READ好

  • READ UNCOMMITTED(读未提交数据)

可能出现任何事务并发问题 性能最好,级别最低

二、分布式事务

2.1 为什么要用分布式事务

随着互联网的快速发展,软件系统由原来的单体应用转变为分布式应用。

分布式系统会把一个应用系统拆分为可独立部署的多个服务,因此需要服务与服务之间远程协作才能完成事务操作,这种分布式系统环境下由不同的服务之间通过网络远程协作完成的事务称为分布式事务。

例如创建订单和扣减库存,这就是分布式事务。

在分布式服务中,订单服务和库存服务不再同一个服务,在创建订单之后,再调用扣减库存服务。但是因为网络问题的存在,扣减库存成功了,但是如果远程调用超时了,那么创建订单也会回滚,因为它长时间接收不到返回的消息,但扣减库存确扣掉了,这就是本地事务回滚做不到的地方,因此,需要采用分布式事务。

2.2 应用场景

1、分布式事务典型的场景就是微服务架构。(跨 JVM)

微服务之间通过远程调用完成事务操作,这种产生的原因就是跨JVM 进程产生的分布式事务,

2、单体系统访问多个数据库实例。(跨数据库实例产生分布式事务)

当单体系统访问多个数据库实例就会产生分布式事务。

比如:用户信息和订单信息分别在两个 MYSQL 实例存储,用户管理系统删除用户信息及用户的订单信息,由于数据分布在不同的数据实例,需要通过不同的数据库链接去操作数据,此时产生分布式事务。

3、多个服务访问单个数据库实例。(跨 JVM)

会因为网络传输问题会造成数据的不一致性。

三、分布式事务基础理论

分布式事务之所以叫分布式事务,它是为了解决分布式系统中因为提供服务的节点分布在不同机器上,相互之间通过网络交互产生的事务问题,不能因为一点网络问题就导致整改系统无法提供服务,网络因素成为了分布式事务的考量标准之一。因此,分布式事务需要了解一些理论知识,从而帮助我们理解每个解决方案。

3.1 CAP 理论

CAP 理论是分布式存储的理论基石,三个字母是三个单词的缩写

  • C : Consistent ,一致性
  • A : Availablity,可用性
    • 任何事务操作都可以得到响应结果,且不会出现响应超时和响应错误
  • P : Partition tolerance ,分区容忍性。
    • 分区容错的意思是,区间通信可能失败。比如,一台服务器放在上海,另一台服务器放在北京,这就是两个区,它们之间可能因网络问题无法通信。容忍的意思是虽然两个分区的通信失败了,但仍然能对外提供服务。

分布式系统的节点往往都是分布在不同的机器进行网络隔离的,意味着随时都具备网络断开的风险,一旦断开就被称为 网络分区。

在网络分区时,两个分布式节点无法进行通信,对其中的一个节点进行的修改操作将无法同步到另外一个节点上,所以数据的一致性将无法满足。

除非牺牲可用性,在网络断开的时候不再提供修改数据的服务,直到网络恢复才恢复。

组合方式思考:

  • 怎样才能同时满足CA? 除非是单点架构
  • 何时要满足CP? 对一致性要求高的场景。例如我们的Zookeeper就是这样的,在服务节点间数据同步时,服务对外不可用。
  • 何时满足AP? 对可用性要求较高的场景。例如Eureka,必须保证注册中心随时可用,不然拉取不到服务就可能出问题。这是很多分布式系统设计的选择,通常实现 AP 都会保证最终一致性。

总结:

  • CAP 理论就是当网络分区的时候,一致性和可用性不可兼得。
  • 一般组合方式选用 AP,舍弃 C 强一致性,保证最终一致性。

3.2 BASE 理论

3.2.1 理解强一致性和最终一致性

CAP 理论告诉我们一个分布式系统最多只能满足 CAP 的某两项。其中 AP 在实际应用中较多,AP 舍弃一致性,保证可用性和分区容忍性,但是在实际生成中也有很多场景都要实现一致性,比如 主数据库向从数据库同步数据,即时不需要一致性,但是也要保证数据同步成功来保证数据最终一致性。即最终一致性它允许可以在一段时间内每个结点的数据不一致,但是经过一段时间每个结点的数据必须一致,即最终一致性。

3.2.2 Base 理论介绍

BASE 是 Basically Available(基本可用)、Soft state (软状态)和 Eventually consistent(最终一致性)三个短语的缩写。

BASE 理论是对 CAP 中 AP 的一个扩展,通过牺牲强一致性来获得可用性,当出现故障允许部分不可用但要保证核心功能可用,允许数据在一段时间内是不一致的,但最终达到抑制状态。满足 BASE 理论的事务,我们称为 “柔性事务”。

  • 基本可用:分布式系统在出现故障时,允许损失部分可用功能,保证核心功能可用。如掘金平台发布文章出现问题,但是文章依旧可用正常浏览。
  • 软状态:由于不要求强一致性,所以 BASE 允许系统中存在中间状态(软状态),这个状态不影响系统可用性。例如导出 excel 的正在导出功能。待全部导出后将最终一致性的状态改为成功状态。
  • 最终一致:最终一致是指经过一段时间后,所有节点数据都将达到一致。如订单的“支付中”状态。

小结

分布式事务是指事务的参与者,支持事务的服务器,资源服务器分别位于分布式系统的不同节点之上,通常一个分布式事物中会涉及到对多个数据源或业务系统的操作。在后面我将

本文转载自: 掘金

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

dart系列之 dart类中的泛型 简介 为什么要用泛型 怎

发表于 2021-11-13

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

简介

熟悉JAVA的朋友可能知道,JAVA在8中引入了泛型的概念。什么是泛型呢?泛型就是一种通用的类型格式,一般用在集合中,用来指定该集合中应该存储的对象格式。

有了泛型可以简化我们的编程,并且可以减少错误的产生,非常的方便。

dart语言中也有泛型。一起来看看吧。

为什么要用泛型

使用泛型的主要目的是保证类型安全,比如我们有一个List,然后只希望List中保存String类型,那么在dart中可以这样指定:

1
2
3
ini复制代码var stringList = <String>[];
stringList.addAll(['jack ma', 'tony ma']);
stringList.add(18); // 报错

那么在使用的过程中,只能向stringList中添加字符串,如果向其添加数字,则会报错,从而保证List中类型的一致性。

巧妙的使用泛型还能够减少我们的代码量,因为泛型可以代表一类通用的类型。

比如,在学校中,我们有寝室,寝室是有男女之分的,那么对应男生来说有这样的定义:

1
2
3
arduino复制代码abstract class BoyRoom {
Boy getByName(String name);
}

对于女生来说有这样的定义:

1
2
3
arduino复制代码abstract class GirlRoom{
Girl getByname(String name);
}

事实上,两者本质上没太大区别,只是参数或者返回值的类型发生了变化,那么我们可以这样写:

1
2
3
csharp复制代码abstract class Room<T>{
T getByname(String name);
}

从而简化了代码的使用。

怎么使用泛型

泛型一般使用大写的单个字符来表示,通常来说是E, T, S, K 和 V等。

泛型最常见的使用地方是集合中,比如List, set 和 map中:

1
2
3
4
5
6
javascript复制代码var listExample = <String>['jack ma', 'tony ma'];
var setExamples = <String>{'jack ma', 'tony ma'};
var mapExamples = <String, String>{
'name1': 'jack ma',
'name2': 'tony ma',
};

泛型还可以用在这些集合类的构造函数中,如下:

1
javascript复制代码var stringMap = Map<String, String>();

表示构造出来的集合中,应该包含对应的类型。

类型擦除

虽然JAVA中也有泛型,但是JAVA中的泛型有一个类型擦除的特点。什么时候类型擦除呢?类型擦除就是指泛型指定的类型,只在编译的时候生效,而在运行时是没有泛型的概念的。

对于一个List 来说,JAVA在运行时,只能判断对象是不是List,而不能判断对象是不是List。

dart就和java不一样了,dart在运行时能够携带类型信息,也就是说,在dart中可以判断一个对象是不是List。

1
2
3
dart复制代码var stringList = <String>[];
stringList.addAll(['jack ma', 'tony ma']);
print(names is List<String>); // true

泛型的继承

使用泛型的目的是限制参数的类型,所以我们通常会指定泛型的父类,以限制泛型的类型范围:

1
2
3
4
5
scala复制代码class Room<T extends Student> {

}

class Boy extends Student {...}

在使用的过程中,可以传入Student本身,也可以传入Student的子类Boy,还可以不传:

1
2
3
ini复制代码var student = Room<Student>();
var boy = Room<Boy>();
var studentDefault = Room();

泛型方法

dart中的泛型除了可以用在class中以外,还可以用在方法中:

1
2
3
4
r复制代码T doSomething<T>(List<T> list) {
T result = list[0];
return result;
}

方法中指定的泛型可以用在返回类型、参数和方法中的本地变量类型中。

总结

以上就是dart中泛型和其使用的介绍。

本文已收录于 www.flydean.com/08-dart-gen…

最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧等你来发现!

欢迎关注我的公众号:「程序那些事」,懂技术,更懂你!

本文转载自: 掘金

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

1…357358359…956

开发者博客

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