BlueXIII's Blog

热爱技术,持续学习

0%

Nginx负载均衡的几种策略

使用开源版本的Nginx可以轻松实现7层(HTTP)或4层(TCP)的负载均衡。

当做为7层负载时,最常用的有以下几种策略:

  • 轮询 round-robin — requests to the application servers are distributed in a round-robin fashion 依次把客户端的请求分发到不同的后端服务器上
  • 最少连接 least-connected — next request is assigned to the server with the least number of active connections 请求会被转发到连接数最少的服务器上
  • IP哈希 ip-hash — a hash-function is used to determine what server should be selected for the next request (based on the client’s IP address) 同一客户端(IP)的连续的请求都会被分发到固定的一台后端服务器上

前两种方案(轮询和最少连接)同一客户端的请求会被随机分配后不同的后端服务器上,如果后端是传统的有状态的WEB应用,则需要在后端WEB容器上配置Session共享,非常麻烦。

后一种方案(IP哈希)可以识别请求端的IP,固定将其分配到某一台后端服务器上,轻松解决了会话保持的问题。但IP哈希存在一个比较严重缺陷,即:客户端必须能够直连Nginx服务器,他们之间不能再插入其它层级,否则Nginx就识别不到客户端的IP了。

那除了IP HASH外,还有没有其它方式能够进行会话保持呢?

以下是官网的关于Cookie Insertion的介绍:

Cookie Insertion:NGINX Plus adds a session cookie to the first response from the upstream group to a given client, identifying which server generated the response (in an encoded fashion). Subsequent requests from the client include the cookie value and NGINX Plus uses it to route the request to the same upstream server:

Cookie植入:

  1. 客户端第一次访问时,负载均衡服务在返回请求中植入cookie(在HTTP/HTTPS响应报文中插入)
  2. 下次客户端携带此cookie访问,负载均衡服务会将请求定向转发给之前记录到的后端服务器上。

Cookie植入原理上就是劫持HTTP请求,伪造cookie,它可以在Nginx无法获取客户端IP时,也依然能够完成会话保持的功能,只要客户端支持Cookie即可。

购买并使用Nginx Plus

要使用Cookie Insertion,最简便的方式是使用商业版本的Nginx Plus,默认支持。

以下是官网关于会话保持的介绍:
https://www.nginx.com/products/session-persistence/

Cookie Insertion的配置非常简单:

1
2
3
4
5
upstream backend {
server webserver1;
server webserver2;
sticky cookie srv_id expires=1h domain=.example.com path=/;
}

于是顺道了解了一下Nginx Plus的来龙去脉:

  • 2002年,来自俄罗斯的Igor Sysoev使用C语言开发了NGINX;
  • 2004年,NGINX开放源码,基于BSD开源许可。
  • 2006年 - 2016年,NIGNX Rlease版本历经0.5, 0.6, 0.7, 0.8, 1.0, 1.2, 1.4, 1.6, 1.8, 1.9, 1.10, 1.11, 当前最新版本为1.11.3;
  • 2011年,Sysoev成立了NGINX公司,NGINX PLUS是其第一款产品,也是NGINX的商业版本;
  • 2013年 - 2016年,自NGINX Plus Initial Release (R1)版本发布以来,NGINX商业版本已更新至NGINX Plus Release 9 (R9);

NGINX Plus is the all-in-one application delivery platform for the modern web.
All-In-One的NGINX PLUS对比开源版本重点增加了若干企业特性,包括更完善的七层、四层负载均衡,会话保持,健康检查,实时监控和管理等。

可看了一下最便宜的BASIC版本也要2500刀每年,泪奔,果断PASS。
https://www.nginx.com/products/buy-nginx-plus/

使用第三方模块nginx-sticky-module

阿里云SLB的Cookie植入功能,猜测应该是自已在Nginx基础上实现的,貌似没有开源。

流传最广的是一个名为 nginx-sticky-module 的第3方的模块 ,不过已经有多年没有维护。

GoogleCode页面(需扶墙): https://code.google.com/archive/p/nginx-sticky-module/downloads

下载 nginx-sticky-module-1.1.tar.gz ,看看能否配合较新的 nginx-1.12.1 使用。

Nginx下载地址: https://nginx.org/en/download.html

选择Stable version中的nginx-1.12.1

简单编译一下试试:

1
2
./configure --prefix=/nginx/nginx-1.12.1 --add-module=/nginx/src/nginx-sticky-module-1.1  
make && make install

果然报错:

1
2
3
4
5
/nginx/src/nginx-sticky-module-1.1/ngx_http_sticky_module.c: In function ‘ngx_http_get_sticky_peer’:
/nginx/src/nginx-sticky-module-1.1/ngx_http_sticky_module.c:333: error: assignment makes pointer from integer without a cast
make[1]: *** [objs/addon/nginx-sticky-module-1.1/ngx_http_sticky_module.o] Error 1
make[1]: Leaving directory `/nginx/src/nginx-1.12.1'
make: *** [build] Error 2

代码太老旧,没有细查的必要了,弃用。

使用第三方模块nginx-sticky-module-ng

另外还有一个比较新的模块 nginx-sticky-module-ng ,源码托管在BitBucket上:
https://bitbucket.org/nginx-goodies/nginx-sticky-module-ng/

在Downloads/Tags下,找到1.2.6的源码并下载,或者直接下载Master分支上最新的代码。

在编译之前,首先来动手修复一个BUG。。。
找到ngx_http_sticky_misc.c,添加以下两个include:

1
2
#include <openssl/sha.h>
#include <openssl/md5.h>

第三方的东西凑和着用吧。。。

编译一下试试:

1
2
./configure --prefix=/nginx/nginx-1.12.1 --add-module=/nginx/src/nginx-sticky-module-ng
make && make install

编译很顺利,配置一下试试吧:
配置上非常简单,只需要在upstream{}中加入sticky即可。

1
2
3
upstream {
sticky;
}

Sticky的详细配置说明:
sticky [name=route] [domain=.foo.bar] [path=/] [expires=1h]
[hash=index|md5|sha1] [no_fallback] [secure] [httponly];

  • name: the name of the cookies used to track the persistant upstream srv; default: route
  • domain: the domain in which the cookie will be valid default: nothing. Let the browser handle this.
  • path: the path in which the cookie will be valid default: /
  • expires: the validity duration of the cookie default: nothing. It’s a session cookie. restriction: must be a duration greater than one second
  • hash: the hash mechanism to encode upstream server. It cant’ be used with hmac. default: md5
  • md5|sha1: well known hash
  • index: it’s not hashed, an in-memory index is used instead, it’s quicker and the overhead is shorter Warning: the matching against upstream servers list is inconsistent. So, at reload, if upstreams servers has changed, index values are not guaranted to correspond to the same server as before! USE IT WITH CAUTION and only if you need to!
  • hmac: the HMAC hash mechanism to encode upstream server It’s like the hash mechanism but it uses hmac_key to secure the hashing. It can’t be used with hash. md5|sha1: well known hash default: none. see hash.
  • hmac_key: the key to use with hmac. It’s mandatory when hmac is set default: nothing.
  • no_fallback: when this flag is set, nginx will return a 502 (Bad Gateway or Proxy Error) if a request comes with a cookie and the corresponding backend is unavailable.
  • secure enable secure cookies; transferred only via https
  • httponly enable cookies not to be leaked via js

下面给出一个完整的配置:

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
worker_processes  4;

events {
worker_connections 1024;
}

http {
upstream proxy_admin {
#ip_hash;
sticky;
server 192.168.1.61:19022 weight=10 max_fails=3 fail_timeout=20s;
server 192.168.1.62:19022 weight=10 max_fails=3 fail_timeout=20s;
}

server {
listen 29022;
server_name proxy_admin;
location / {
root html;
proxy_pass http://proxy_admin;
proxy_next_upstream error timeout invalid_header http_500 http_503 http_404;
proxy_redirect off;
proxy_ignore_client_abort on;
proxy_set_header Host $host:$server_port;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_connect_timeout 60;
proxy_send_timeout 60;
proxy_read_timeout 60;
proxy_buffer_size 4k;
proxy_buffers 4 32k;
proxy_busy_buffers_size 64k;
proxy_temp_file_write_size 64k;
client_body_buffer_size 128k;
}
}
}

测试,用浏览器访问一下试试:
http://IP:29022/mobagent-admin/
如果在控制台中的Cookie中看到一个名为route的键,就说明基本上成功了。
然后多请求几次,看看后端服务的日志,最终确认一下,是不是只指向了同一台服务器。

注意事项

最后,请注意,因为nginx-sticky-module-ng的可靠性未经长时间验证,请勿直接用于生产系统。

DEMO GitLab

更多源码请移步GitHub
https://github.com/xiiiblue/demo-oss

OSS SDK下载

https://promotion.aliyun.com/ntms/act/ossdoclist.html

OSS SDK源码

https://github.com/aliyun/aliyun-oss-java-sdk

OSS Maven依赖

1
2
3
4
5
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>2.8.1</version>
</dependency>

OSS官方文档

https://help.aliyun.com/document_detail/32009.html

申请及配置OSS流程

  1. 登录aliyun后,进入OSS控制台:
    https://oss.console.aliyun.com/overview

  2. 右侧点击”购买资源包”,选择合适的档位,付费购买。

  3. 右侧点击”新建Bucket”,输入一个全局唯一的bucket名,并选择所属地域,新建Bucket。

  4. 左侧列表中,进入新建的Bucket,可以查询到”Endpoint”

  5. 获取AK,可以直接访问链接:
    https://ak-console.aliyun.com/#/accesskey

常用操作示例

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
private Logger logger = LoggerFactory.getLogger(OssComponent.class);
@Value("${aliyun.oss.endpoint}")
private String endpoint;
@Autowired
private OSSClient ossClient;


/**
* 通过bucket与fileKey获取URL
*
* @param bucket
* @param fileKey
* @return
*/
public String urlFromFileKey(String bucket, String fileKey) {
return "http://" + bucket + "." + this.endpoint + "/" + fileKey;
}

/**
* 上传本地文件
*
* @param bucket Bucket
* @param filePath 本地文件路径
* @param fileKey 指定OSS文件名,传空时自动生成UUID
* @return
* @throws FileNotFoundException
*/
public String putObject(String bucket, String filePath, String fileKey) throws FileNotFoundException {
if (fileKey == null) {
fileKey = UUID.randomUUID().toString().replaceAll("-", "") + filePath.substring(filePath.lastIndexOf("."));
}

ossClient.putObject(bucket, fileKey, new File(filePath));

logger.info("OSS putObject success! fileKey={}", fileKey);
return fileKey;
}

/**
* 下载文件到本地目录
*
* @param bucket Bucket
* @param fileKey OSS文件名
* @param localPath 本地文件目录
* @return
*/
public String getObject(String bucket, String fileKey, String localPath) {
String localFileKey = localPath + fileKey;
ossClient.getObject(new GetObjectRequest(bucket, fileKey), new File(localFileKey));

logger.info("OSS getObject success! localFileKey={}", localFileKey);
return localFileKey;
}


/**
* 检查文件是否存在
*
* @param bucket Bucket
* @param fileKey OSS文件名
* @return
*/
public boolean checkObject(String bucket, String fileKey) {
return ossClient.doesObjectExist(bucket, fileKey);
}


/**
* 列出所有文件
*
* @param bucket Bucket
* @param keyPrifix 文件名前缀
*/
public List<String> listObjects(String bucket, String keyPrifix) {
List<String> fileList = new ArrayList<>();

ObjectListing objectListing = ossClient.listObjects(bucket, keyPrifix);
List<OSSObjectSummary> sums = objectListing.getObjectSummaries();
for (OSSObjectSummary s : sums) {
fileList.add(bucket);
}

return fileList;
}

/**
* 删除文件
*
* @param bucket
* @param fileKey
*/
public void deleteObject(String bucket, String fileKey) {
ossClient.deleteObject(bucket, fileKey);
}
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
private Logger logger = LoggerFactory.getLogger(OssComponentTest.class);
private String localPath;
@Autowired
OssComponent ossComponent;

public OssComponentTest() {
localPath = System.getProperty("user.dir") + "/" + "upload/";
}


@Test
public void urlFromFileKey() throws Exception {
String bucket = "foobar";
String fileKey = "0c2eb1357a454c71b71aa20462951ac4.jpg";
String url = ossComponent.urlFromFileKey(bucket, fileKey);

logger.debug(url);
assertNotNull(url);
}

@Test
public void putObject() throws Exception {
String bucket = "foobar";
String filePath = localPath + "sonic.jpg";

String fileKey = ossComponent.putObject(bucket, filePath, null);
logger.debug(fileKey);
assertNotNull(fileKey);

fileKey = ossComponent.putObject(bucket, filePath, "sonic.jpg");
logger.debug(fileKey);
assertNotNull(fileKey);
}

@Test
public void getObject() throws Exception {
String bucket = "foobar";
String fileKey = "0c2eb1357a454c71b71aa20462951ac4.jpg";
String localPath = this.localPath;

String localFileKey = ossComponent.getObject(bucket, fileKey, localPath);

logger.debug(localFileKey);
assertNotNull(localFileKey);
}

@Test
public void checkObject() throws Exception {
String bucket = "foobar";
String fileKey = "0c2eb1357a454c71b71aa20462951ac4.jpg";

boolean flag = ossComponent.checkObject(bucket, fileKey);

assertTrue(flag);
}

@Test
public void listObjects() throws Exception {
String bucket = "foobar";
String keyPrifix = "";

List<String> list = ossComponent.listObjects(bucket, keyPrifix);

logger.debug(list.toString());
assertTrue(list.size() > 0);
}

@Test
public void deleteObject() throws Exception {
String bucket = "foobar";
String fileKey = "sonic.jpg";

ossComponent.deleteObject(bucket, fileKey);
}

以下大部分内容非原创,整理自阿里云官方文档

单库单表

建一张单库单表,不做任何拆分。

1
2
3
4
5
6
CREATE TABLE single_tbl(
id int,
name varchar(30),
primary key(id)
);
show topology from single_tbl;
1
2
3
4
5
CREATE TABLE normal_table(
id int,
name varchar(30),
primary key(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

分库不分表

假设已经建好的分库数为 8,建一张表,只分库不分表,分库方式为根据 id 列哈希。

1
2
3
4
5
6
7
CREATE TABLE multi_db_single_tbl(
id int,
name varchar(30),
primary key(id)
) dbpartition by hash(id);

show topology from multi_db_single_tbl;

分库分表

使用哈希函数做拆分

1
2
3
4
5
6
7
8
CREATE TABLE multi_db_multi_tbl(
id int auto_increment,
bid int,
name varchar(30),
primary key(id)
) dbpartition by hash(id) tbpartition by hash(bid) tbpartitions 3;

show topology from multi_db_multi_tbl;

使用双字段哈希函数做拆分

1
2
3
4
5
6
7
8
9
create table test_order_tb (  
id int,
seller_id varchar(30) DEFAULT NULL,
buyer_id varchar(30) DEFAULT NULL,
create_time datetime DEFAULT NULL,
primary key(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 dbpartition by RANGE_HASH(seller_id,buyer_id, 10) tbpartition by RANGE_HASH(seller_id,buyer_id, 10) tbpartitions 3;

show topology from multi_db_multi_tbl;

使用日期做拆分

可以使用日期函数 MM/DD/WEEK/MMDD 来作为分表的拆分算法

1
2
3
4
5
6
7
8
CREATE TABLE user_log(
userId int,
name varchar(30),
operation varchar(30),
actionDate DATE
) dbpartition by hash(userId) tbpartition by WEEK(actionDate) tbpartitions 7;

show topology from user_log;
1
2
3
4
5
6
7
8
9
CREATE TABLE user_log2(
userId int,
name varchar(30),
operation varchar(30),
actionDate DATE
) dbpartition by hash(userId) tbpartition by MM(actionDate) tbpartitions 12;

show topology from user_log2;

默认使用主键作为拆分字段

当拆分算法不指定任何拆分字段时,系统默认使用主键作为拆分字段。

1
2
3
4
5
CREATE TABLE prmkey_tbl(
id int,
name varchar(30),
primary key(id)
) dbpartition by hash();
1
2
3
4
5
CREATE TABLE prmkey_multi_tbl(
id int,
name varchar(30),
primary key(id)
) dbpartition by hash() tbpartition by hash() tbpartitions 3;

广播表

子句BROADCAST用来指定创建广播表。广播表是指将这个表复制到每个分库上,在分库上通过同步机制实现数据一致,有秒级延迟。
这样做的好处是可以将 JOIN 操作下推到底层的 RDS(MySQL),来避免跨库 JOIN。

1
2
3
4
5
CREATE TABLE brd_tbl(
id int,
name varchar(30),
primary key(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 BROADCAST;

直接在客户端使用DDL建表好像并不成功,尽量使用WEB控制台建表

其它操作

查看物理表的拓扑结构

1
SHOW TOPOLOGY FROM multi_db_multi_tbl;

查看逻辑表是否创建成功

1
CHECK TABLE multi_db_multi_tbl;

以下大部分内容非原创,整理自阿里云官方文档

SQL大类限制

  • 暂不支持用户自定义数据类型、自定义函数。
  • 暂不支持视图、存储过程、触发器、游标。
  • 暂不支持 BEGIN…END、LOOP…END LOOP、REPEAT…UNTIL…END REPEAT、WHILE…DO…END WHILE 等复合语句。
  • 暂不支类似 IF ,WHILE 等流程控制类语句。

DDL限制

  • CREATE TABLE tbl_name LIKE old_tbl_name 不支持拆分表。
  • CREATE TABLE tbl_name SELECT statement 不支持拆分表。

数据库管理限制

  • SHOW WARNINGS Syntax 不支持 LIMIT/COUNT 的组合。
  • SHOW ERRORS Syntax 不支持 LIMIT/COUNT 的组合。

DML限制

  • 暂不支持 SELECT INTO OUTFILE/INTO DUMPFILE/INTO var_name。
  • 暂不支持 INSERT DELAYED Syntax。
  • 暂不支持非 WHERE 条件的 Correlate Subquery。
  • 暂不支持 SQL 中带聚合条件的 Correlate Subquery。
  • 暂不支持 SQL 中对于变量的引用和操作,比如 SET @c=1, @d=@c+1; SELECT @c, @d。

关于 Correlated Subquery 的解释:
Correlated Subquery is a sub-query that uses values from the outer query. In this case the inner query has to be executed for every row of outer query.
See example here http://en.wikipedia.org/wiki/Correlated_subquery
Simple subquery doesn’t use values from the outer query and is being calculated only once:

1
2
3
4
5
SELECT id, first_name
FROM student_details
WHERE id IN (SELECT student_id
FROM student_subjects
WHERE subject= 'Science');

分析

DDL限制、数据库管理限制,由于与应用关系不大,编码时无须特别关心。
SQL大类限制,如视图、存储过程、触发器、游标等,通常是不被提倡的坏味道,如果有的话,直接改方式实现即可。
DML限制,需要特别注意。如果使用ORM,应该不会有太大问题,但用jdbcTemplate写原生SQL的话(如实时报表类的功能点)有可能会踩坑。
另外默认不支持跨实例事务,私有云貌似也没有GTS选项,需要在代码层面进行优化。

DML错误示例

1
select * from (select id,name from normal_tables) t

[b8040ac9f401000][192.168.2.47:3306][dev_sc_dmanage]ERR-CODE: [TDDL-4007][ERR_CANNOT_FETCH_TABLE_META] Table ‘normal_tables’ metadata cannot be fetched because Table ‘dev_sc_dmanage_ssiu_0000.normal_tables’ doesn’t exist. More: [http://middleware.alibaba-inc.com/faq/faqByFaqCode.html?faqCode=TDDL-4007]

1
2
select a,*,b.* from shard_table a
left join (select id,name from normal_table) b on b.id=a.id;

[b80405010801000][192.168.2.47:3306][dev_sc_dmanage]column: a is not existed in either null or select clause

1
2
select a,*,b.* from normal_tables a
left join (select id,name from shard_table) b on b.id=a.id;

[b8040692c001000][192.168.2.47:3306][dev_sc_dmanage]ERR-CODE: [TDDL-4007][ERR_CANNOT_FETCH_TABLE_META] Table ‘normal_tables’ metadata cannot be fetched because Table ‘dev_sc_dmanage_ssiu_0000.normal_tables’ doesn’t exist. More: [http://middleware.alibaba-inc.com/faq/faqByFaqCode.html?faqCode=TDDL-4007]

1
2
insert into group_seq_tbl (name) values ('hello');
insert into group_seq_tbl (name) values ('hello');

[b807dfe12c01000-24][192.168.2.47:3306][dev_sc_dmanage]ERR-CODE: [TDDL-4603][ERR_ACCROSS_DB_TRANSACTION] Transaction accross db is not supported in current transaction policy, transaction node is: DEV_SC_DMANAGE_1501040729564NYQSDEV_SC_DMANAGE_SSIU_0003_RDS, but this sql execute on: DEV_SC_DMANAGE_1501040729564NYQSDEV_SC_DMANAGE_SSIU_0006_RDS. More: [http://middleware.alibaba-inc.com/faq/faqByFaqCode.html?faqCode=TDDL-4603]

以下大部分内容非原创,整理自阿里云官方文档

DRDS Sequence简介

DRDS 全局唯一数字序列(64 位数字,对应 MySQL 中 Signed BIGINT 类型)的主要目标是为了生成全局唯一和有序递增的数字序列,常用于主键列、唯一索引列等值的生成

显式与隐式

  • 显式 Sequence,通过 Sequence DDL 语法创建和维护,可以独立使用;通过select seq.nextval;获取序列值,seq 是具体 Sequence 的名字;
  • 隐式 Sequence,在为主键定义 AUTO_INCREMENT 后,用于自动填充主键,由 DRDS 自动维护。

注意:仅拆分表和广播表指定了 AUTO_INCREMENT 后,DRDS 才会创建隐式的 Sequence。
非拆分表并不会,非拆分表的 AUTO_INCREMENT 的值由底层 RDS(MySQL)自己生成。

三种类型

Group Sequence(GROUP)

全局唯一的 Sequence,产生的值是自然数序列,但是 不保证连续和单调递增。如果未指定 Sequence 类型,DRDS 默认使用 Group Sequence。

Time-based Sequence(TIME)

基于时间戳 + 节点编号 + 序列号组合而成的一种 Sequence,保证全局唯一和 宏观自增(产生的序列不连续)。

Simple Sequence(SIMPLE)

支持自定义步长、最大值和循环/非循环利用。但每产生一个值都要进行一次持久化操作,性能不好。

大部分场景下建议选用Group Sequence
如果业务强依赖连续的Sequence,此时只能使用 Simple Sequence(注意性能问题)
对于性能要求比较高时优先考虑使用 Time-based Sequence

创建及删除显式Sequence

默认创建GROUP类型

1
CREATE SEQUENCE seq1;

指定GROUP类型

1
CREATE GROUP SEQUENCE seq2;

指定TIME类型

1
CREATE TIME SEQUENCE seq3;

指定SIMPLE类型(起始值是 1000,步长为 2,最大值为 99999999999,增长到最大值后不继续循环)

1
CREATE SIMPLE SEQUENCE seq4 START WITH 1000 INCREMENT BY 2 MAXVALUE 99999999999 NOCYCLE;

删除Sequence

1
DROP SEQUENCE seq1;

查询Sequence

1
SHOW SEQUENCES

取Sequence的值

1
2
select seq1.NEXTVAL;
select seq1.NEXTVAL from dual;

隐式用法

语法

1
2
3
4
5
CREATE TABLE <name> (
<column> ... AUTO_INCREMENT [ BY GROUP | SIMPLE | TIME ],
<column definition>,
...
) ... AUTO_INCREMENT=<start value>

示例

1
2
3
4
5
6
7
8
CREATE TABLE `group_seq_tbl` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(30) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2000 DEFAULT CHARSET=utf8 dbpartition by hash(`id`);

insert into group_seq_tbl (name) values ('foobar');
select * from group_seq_tbl;

查看建表语句

1
SHOW CREATE TABLE group_seq_tbl;

修改起始值

1
2
3
SHOW SEQUENCES;
ALTER SEQUENCE AUTO_SEQ_group_seq_tbl START WITH 600000;
select AUTO_SEQ_group_seq_tbl.nextval;

将GRUOP类型一个Sequence的START由200000设成300000后,测试nextval时会从400000开始。
这不是BUG,是由GROUP特性决定的。使用SIMPLE,才可以保证连续、单调递增。

简介

本文主要介绍如何使用spring-cloud-feign,在项目中使用Feign进行REST调用。

通常我们进行微服务间的REST调用时,一般会使用restTemplate,写起来也比较方便,例如:

1
2
3
ResponseEntity<UserDTO> result = restTemplate.getForEntity(baseurl + "/users?serialNumber=18612341234", UserDTO.class);

ResponseEntity<String> result = restTemplate.exchange(baseurl + "/users/1715043034165359", HttpMethod.PUT, new HttpEntity(user), String.class);

但RestTemplate这种方式的缺点是代码量略大,且不太直观。

微服务强调跨语言解耦,不提倡以前那种将API部分打包并分发的方式。不同微服务间的业务代码的冗余不可避免。使用Feign,就可以简化Rest客户端这一部分的代码。

引入pom.xml依赖

Spring Boot工程中,直接引入spring-cloud-starter-feign依赖即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Dalston.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-feign</artifactId>
</dependency>
</dependencies>

application.yml配置

新版本的Feign默认是禁用Hystrix的,需要手工配置打开。

1
feign.hystrix.enabled: true

Feign本身可以与Eureka/Ribbon比较好的配合使用,不需要其它配置,直接使用”应用名”进行调用。

当微服务没有使用Eureka做服务发现时,就需要手工配置Ribbon。例如,当使用marathon-lb时,可以这样配置:

1
marathon-lb-internal.ribbon.listOfServers: marathon-lb-internal.marathon.mesos

这里指定了一个DNS,当然也可以写死一个或多个IP。

启用EnableFeignClients注解

最简单的,可以在Application.java上,加上这个注解:

1
@EnableFeignClients

或者,新建一个配置类,指定profile:

1
2
3
4
@Profile("enable-feign")
@Configuration
@EnableFeignClients(basePackages = {"com.sitech.sdtools"})
public class FeignConfiguration {}

使用配置类+profile的好处是,可以根据不同的环境,非常方便的启用/禁用Feign。
特别是在单元测试时,由于mockito无法对Feign生成的Bean进行Mock,这时就可以在profile中禁用Feign,直接执行Fallback。

另外,需要注意一下,如果配置类的路径不是在根路径,而是在com.foo.bar.config这样的包下,需要加上”basePackages”参数。

解决Bean冲突

我目前的Spring Clound的版本是Dalston.SR2,如果集成了Hystrix,编写Fallback类后会有Bean冲突的问题,貌似是自动生成的@Primary注解无效,具体原因没有深究,可以通过配置解决,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Configuration
@ConditionalOnClass({Feign.class})
public class FeignConfiguration {
@Bean
public WebMvcRegistrations feignWebRegistrations() {
return new WebMvcRegistrationsAdapter() {
@Override
public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
return new FeignFilterRequestMappingHandlerMapping();
}
};
}

private static class FeignFilterRequestMappingHandlerMapping extends RequestMappingHandlerMapping {
@Override
protected boolean isHandler(Class<?> beanType) {
return super.isHandler(beanType) && (AnnotationUtils.findAnnotation(beanType, FeignClient.class) == null);
}
}
}

开启日志

Feign的日志是DEBUG级别,在LogBack中有时需要特别配置一下:

1
2
3
<logger name="com.foo.bar.client" additivity="false" level="debug">
<appender-ref ref="stdout"/>
</logger>

定义client接口

下面开始,正式进入正题,定义一个接口,并加上@FeignClient注解。

1
2
3
4
5
6
@FeignClient(name = "marathon-lb-internal", fallback = StaticInfoClientFallback.class)
@RequestMapping(value = "/sd/staticinfo")
public interface StaticInfoClient {
@RequestMapping(value = "/products/{productId}", method = RequestMethod.GET)
public ProductDTO getProductInfo(@PathVariable("productId") Long productId);
}

可以直接在服务端的Controller层中拷贝代码,稍做修改即可,非常方便。

但请注意:

  1. @PathVariable("productId"),需要显示的指定对应的参数名,不能像SpringMVC一样自动对应。
  2. 目前只能使用@RequestMapping注解,而不能使用@GetMapping

编写fallback类

当调用失败时,会执行fallback类中的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Component
class StaticInfoClientFallback implements StaticInfoClient {
private static final Logger logger = LoggerFactory.getLogger(TradeInfoClientFallback.class);

@Override
public ProductDTO getProductInfo(Long productId) {
logger.error("StaticInfoClient.getProductInfo 执行失败");

ProductDTO dto = new ProductDTO();
dto.setProductId(productId);
dto.setProductName("产品名称暂时无法获取");
return dto;
}
}

注意不要忘记加上@Component

发起REST调用

REST调用也非常简单,一行代码搞定:

1
ProductDTO productInfo = staticInfoClient.getProductInfo(entity.getProductId());

END

Feign的配置略微复杂,坑也比较多,有一定的学习成本的。但带来的好处理,在使用上即优雅又方便。
与RestTemplate相比,只能说是各有利弊吧,可以酌情选择。

概述

使用spotify的dockerfile maven插件,可以帮助我们将maven与docker进行集成,更快速的在maven构建过程中进行docker镜像的打包操作。
配置仍然是基于Dockerfile的,插件只是辅助。

请注意,插件的名称是 spotifydockerfile-maven-plugin ,而非 docker-maven-plugin

详情可访问 github页面

准备工作

首先准备一个spring-boot工程,假设工程名为playground。另外本机需要安装docker,过程都不再赘述。
本文平台为macOS12,docker在不同平台下可能有所差异。

由于dockerhub访问较慢,可以用$$先自行搭建一个socks5代理来加速访问。代理不是必须的。

1
export ALL_PROXY=socks5://xx.xx.xx.xx:xx

建立Dockerfile

在工程目录下新建一个Dockerfile,基于openjdk的镜像:

1
2
3
4
5
FROM openjdk:8-jdk-alpine
VOLUME /tmp
ADD target/playground-1.1.0.jar app.jar
ENV JAVA_OPTS=""
ENTRYPOINT [ "sh", "-c", "java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /app.jar" ]

使用docker命令构建镜像

在使用Maven插件之前,可以试着先用docker命令来进行打包:

1
docker build -t="yourname/playground:latest" .

查看镜像是否生成:

1
docker images

然后可以使用docker push推送镜像:

1
docker push yourname/playground

编辑pom.xml

测试通过后,就可以在pom.xml中添加spotify插件了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<properties>
<docker.image.prefix>yourname</docker.image.prefix>
</properties>
<build>
<plugins>
<plugin>
<groupId>com.spotify</groupId>
<artifactId>dockerfile-maven-plugin</artifactId>
<version>1.3.4</version>
<configuration>
<repository>${docker.image.prefix}/${project.artifactId}</repository>
</configuration>
</plugin>
</plugins>
</build>

另外还可以添加一个execution,在maven的install生命周期自动进行build和push。

1
2
3
4
5
6
7
8
9
10
<executions>
<execution>
<id>default</id>
<phase>install</phase>
<goals>
<goal>build</goal>
<goal>push</goal>
</goals>
</execution>
</executions>

使用插件构建镜像

1
mvn install dockerfile:build

使用插件推送镜像

在使用dockerfile-maven-plugin推送前,需要先编辑一下~/.docker/config.json,添加用户名及密码,否则会报认证失败。

1
2
3
4
5
6
7
8
9
{
"auths": {
"https://index.docker.io/v1/": {
"Username": "xxxxxx",
"Secret": "xxxxxx"
}
},
"credsStore": "osxkeychain"
}

然后直接运行即可:

1
mvn dockerfile:push

运行镜像

最后,可以使用docker run命令可以将镜像启动,在浏览器中查看一下运行情况

1
docker run -p 8080:8080 -t yourname/playground

使用docker logs简单查看日志

1
2
docker ps
docker logs f4d7e737f47f

概述

ZEN-SC-SAMPLE:SpringCloud常用组件示例。

源码

更多源码,请移步GitHub:
https://github.com/xiiiblue/zen-sc-sample

启动方式

Docker方式启动

1
2
mvn clean package
docker-compose up

手工方式启动

添加DNS

1
2
3
4
vi /etc/hosts
127.0.0.1 zen-eureka
127.0.0.1 zen-zipkin
127.0.0.1 zen-config

在每个模块下分别执行

1
mvn spring-boot:run

组件清单

  • hystrix - 服务降级与融断
  • feign - 声明式REST调用
  • eureka - 服务注册与发现
  • ribbon - 客户端负载均衡
  • config-server - 配置中心
  • zuul - API网关
  • turbine - 监控聚合
  • hystrix-dashboard - Hystrix面板
  • sleuth - 服务跟踪
  • zipkin - 调用链跟踪

未包含组件

  • ELK

URLs

其它说明

刷新配置

1
curl -X POST http://localhost:8080/refresh

地址

https://12factor.net/zh_cn

简介

12-Factor 为构建如下的 SaaS 应用提供了方法论:
使用标准化流程自动配置,从而使新的开发者花费最少的学习成本加入这个项目。
和操作系统之间尽可能的划清界限,在各个系统中提供最大的可移植性。
适合部署在现代的云计算平台,从而在服务器和系统管理方面节省资源。
将开发环境和生产环境的差异降至最低,并使用持续交付实施敏捷开发。
可以在工具、架构和开发流程不发生明显变化的前提下实现扩展。

目录

I. 基准代码
一份基准代码,多份部署
II. 依赖
显式声明依赖关系
III. 配置
在环境中存储配置
IV. 后端服务
把后端服务当作附加资源
V. 构建,发布,运行
严格分离构建和运行
VI. 进程
以一个或多个无状态进程运行应用
VII. 端口绑定
通过端口绑定提供服务
VIII. 并发
通过进程模型进行扩展
IX. 易处理
快速启动和优雅终止可最大化健壮性
X. 开发环境与线上环境等价
尽可能的保持开发,预发布,线上环境相同
XI. 日志
把日志当作事件流
XII. 管理进程
后台管理任务当作一次性进程运行

12-factor应用本身从不考虑存储自己的输出流。 不应该试图去写或者管理日志文件。相反,每一个运行的进程都会直接的标准输出(stdout)事件流。开发环境中,开发人员可以通过这些数据流,实时在终端看到应用的活动。

解决Ubuntu无法识别Android手机的问题

  1. 安装mtp相关的包:
    1
    sudo apt-get install libmtp-common mtp-tools libmtp-dev libmtp-runtime libmtp9
  2. 查看手机的USB信息
    执行:
    1
    lsusb
    记录下新增的一条USB设备的信息,以坚果Pro1为例:
    1
    Bus 002 Device 011: ID 29a9:7020  
  3. 编辑69-libmtp.rules
    1
    sudo vi /lib/udev/rules.d/69-libmtp.rules
    新增一行:
    1
    ATTR{idVendor}=="29a9", ATTR{idProduct}=="7020", SYMLINK+="libmtp-%k", ENV{ID_MTP_DEVICE}="1", ENV{ID_MEDIA_PLAYER}="1"
    注意替换掉其中的idVendor和idProduct