iyihua

  • 首页

  • 标签

  • 分类

  • 归档

记一次线上卡顿问题的处理-Mysql的Lock wait timeout

发表于 2019-11-18 | 分类于 exception

记一次线上卡顿问题的处理-Mysql的Lock wait timeout

问题表现:

  • 1.程序jdbc连接Mysql报错’Lock wait timeout exceeded;try restarting transaction’

  • 2.页面某2个操作提交卡住,一直转

排查:

使用show processlist和select * from information_schema.innodb_trx会看到有一些等待中的mysql线程

可以用kill命令把这些mysql线程kill掉,但这只是暂时紧急应对,没有定位到真正的问题所在。

出现锁等待超时的情况,一般可能会是因为一个sql执行完,但未commit,接下来的sql要执行就会被锁,直到完成或者超时结束。

继续对问题进行分析。

出现等待的两个sql分别是insert ignore into biz_customer_contact...以及update customer ...,而这2个表只是百万级的表,正常的插入和更新表操作一般不会造成等待和锁定,因此考虑不是sql本身的问题,sql本身不用进行优化。

同时,出现锁定超时的问题仅仅是这2个地方这2个sql,而其它地方没有出现问题,因此也排除是整个数据库出现性能问题。

再深入问题,找到这2个sql执行所在的程序代码,review代码后,发现两者有一个共性特征,就是两者都需要调用一个其它系统的接口来完成业务操作。考虑到网络调用或者I/O操作是很可能出现阻塞等待的情况的,因此怀疑是接口调用等待的时间太长,导致数据库事务一直在等待,最后导致超时,出现上面的线程等待问题和异常报错。

针对这个问题,专门测试了依赖的接口和咨询对应系统的运维后,确认,依赖的系统确实当时正出现问题。因此定位到问题所在。

定位到问题后,就要做适当的处理,处理方案为:

  • 1.恢复出现问题的服务
  • 2.将接口调用的超时时间缩短,原来是没有设置这个时间的。缩短接口超时时间的目的是假如服务不可用,则快速失败抛出错误,避免系统一直等待卡顿。
  • 3.适当设置mysql数据库的锁等待超时时间,例如set GLOBAL innodb_lock_wait_timeout = 5000,缩短等待超时时间,目的也是避免高并发时太多线程在等待拖垮系统。

Mysql的PARTITION表分区要点

发表于 2019-11-18 | 分类于 mysql

Mysql自带的Partition分区方案好处:

优势1,数据库操作变更简单;
优势2,分区后程序不需要修改,数据库分区的表使用和分区前一致;
优势3,分区后,查询效率没有降低。

1. 建立分区步骤

现在假设biz_process和biz_follow是2张大表,现在要对它们进行mysql数据库表分区

有2种推荐的方式进行表分区:

  • 一是,先进行备份,再对需要分区的表直接使用ALTER TABLE进行分区设置

  • 二是,先创建使用别名的分区表,再复制数据进分区表,最后变更表名字原来的表和分区表切换。

1.1 创建需要分区的表的备份

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
CREATE TABLE `biz_process_bak` (
`process_id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '流程编号',
`customer_id` bigint(20) NOT NULL COMMENT '客户编号',
`dept_id` bigint(20) NOT NULL COMMENT '业务部门编号',
`user_id` bigint(20) DEFAULT NULL COMMENT '业务人员编号',
--···建表语句
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='商机流程表';

CREATE TABLE `biz_follow_bak` (
`follow_id` bigint(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '跟进编号',
`customer_id` bigint(11) NOT NULL COMMENT '客户编号',
`process_id` bigint(11) NOT NULL COMMENT '流程编号',
--···建表语句
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='客户跟进表';

insert into biz_process_bak select * from biz_process;
insert into biz_follow_bak select * from biz_follow;

1.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
CREATE TABLE `biz_process_range` (
`process_id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '流程编号',
`customer_id` bigint(20) NOT NULL COMMENT '客户编号',
`dept_id` bigint(20) NOT NULL COMMENT '业务部门编号',
`user_id` bigint(20) DEFAULT NULL COMMENT '业务人员编号',
--···建表语句
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='商机流程表' PARTITION BY RANGE(process_id) (
PARTITION p1 VALUES LESS THAN (2000000),
PARTITION p2 VALUES LESS THAN (4000000),
PARTITION p3 VALUES LESS THAN (6000000),
PARTITION p4 VALUES LESS THAN (8000000),
PARTITION p5 VALUES LESS THAN (10000000),
PARTITION p6 VALUES LESS THAN (12000000),
PARTITION p7 VALUES LESS THAN (14000000),
PARTITION p8 VALUES LESS THAN (16000000),
PARTITION p9 VALUES LESS THAN (18000000),
PARTITION p10 VALUES LESS THAN (20000000),
PARTITION p11 VALUES LESS THAN (MAXVALUE)
);

CREATE TABLE `biz_follow_range` (
`follow_id` bigint(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '跟进编号',
`customer_id` bigint(11) NOT NULL COMMENT '客户编号',
`process_id` bigint(11) NOT NULL COMMENT '流程编号',
--···建表语句
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='客户跟进表' PARTITION BY RANGE(follow_id) (
PARTITION p1 VALUES LESS THAN (2000000),
PARTITION p2 VALUES LESS THAN (4000000),
PARTITION p3 VALUES LESS THAN (6000000),
PARTITION p4 VALUES LESS THAN (8000000),
PARTITION p5 VALUES LESS THAN (10000000),
PARTITION p6 VALUES LESS THAN (12000000),
PARTITION p7 VALUES LESS THAN (14000000),
PARTITION p8 VALUES LESS THAN (16000000),
PARTITION p9 VALUES LESS THAN (18000000),
PARTITION p10 VALUES LESS THAN (20000000),
PARTITION p11 VALUES LESS THAN (MAXVALUE)
);

insert into biz_process_range select * from biz_process;
insert into biz_follow_range select * from biz_follow;

1.3 使用重命名快速把原表切换到分区表

1
2
RENAME TABLE biz_process TO biz_process_tmp, biz_process_range TO biz_process;
RENAME TABLE biz_follow TO biz_follow_tmp, biz_follow_range TO biz_follow;

ps. 也可以直接使用ALTER TABLE的方式,直接为原来的表添加分区

2. 关于分区新增和变更删除等操作

2.1 为当前表创建分区

因为是对已有表进行改造,所以只能用 alter 的方式:

1
2
3
4
5
6
ALTER TABLE stat
PARTITION BY RANGE(TO_DAYS(dt)) (
PARTITION p0 VALUES LESS THAN(0),
PARTITION p190214 VALUES LESS THAN(TO_DAYS('2019-02-14')),
PARTITION pm VALUES LESS THAN(MAXVALUE)
);

复制代码这里有2点要注意:

  • 一是 p0 分区,这是因为 MySQL(我是5.7版) 有个 bug,就是不管你查的数据在哪个区,它都会扫一下第一个区,我们每个区的数据都有几十万条,扫一下很是肉疼啊,所以为了避免不必要的扫描,直接弄个0数据分区就行了。
  • 二是 pm 分区,这个是最大分区。假如不要 pm,那你存 2019-02-15 的数据就会报错。所以 pm 实际上是给未来的数据一个预留的分区。

2.2 定期扩展分区

由于 MySQL 的分区并不能自己动态扩容,所以我们要写个代码为它动态的增加分区。

增加分区需要用到 REORGANIZE 命令,它的作用是对某个分区重新分配。

比如明天是 15 号,那我们要给 15 号也增加个分区,实际上就是把 pm 分区拆分成2个分区:

1
2
3
4
5
ALTER TABLE stat
REORGANIZE PARTITION pm INTO (
PARTITION p190215 VALUES LESS THAN(TO_DAYS('2019-02-15')),
PARTITION pm VALUES LESS THAN(MAXVALUE)
);

复制代码这里就涉及到一个问题,即如何获得当前表的所有分区?网上有挺多方法,但我试了下感觉还是先 show create table stat然后用正则匹配出所有分区更方便一点。

2.3 定期删除分区

随着数据库越来越大,我们肯定是要清除旧的数据,同时也要清除旧的分区。
这个也比较简单:

1
ALTER TABLE stat DROP PARTITION p190214, p190215

3. 附录:Mysql的Partition分区使用说明

3.1 基于时间进行range分区例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
CREATE TABLE my_range_datetime(
id INT,
hiredate DATETIME
) ENGINE=INNODB PARTITION BY RANGE (YEAR(hiredate) ) (
PARTITION p1 VALUES LESS THAN (2011),
PARTITION p2 VALUES LESS THAN (2012),
PARTITION p3 VALUES LESS THAN (2013),
PARTITION p4 VALUES LESS THAN (2014),
PARTITION p5 VALUES LESS THAN (2015),
PARTITION p6 VALUES LESS THAN (2016),
PARTITION p7 VALUES LESS THAN (2017),
PARTITION p8 VALUES LESS THAN (2018),
PARTITION p9 VALUES LESS THAN (2019),
PARTITION p10 VALUES LESS THAN (2020),
PARTITION p11 VALUES LESS THAN (MAXVALUE)
);

3.2 基于主键进行range分区例子:由于表如果设置了主键的话,分区的字段一定要属于主键,所以一般要么把分区字段添加为主键,要么就只能针对主键进行分区

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
CREATE TABLE `range_process` (
`process_id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '流程编号',
`customer_id` bigint(20) NOT NULL COMMENT '客户编号',
`dept_id` bigint(20) NOT NULL COMMENT '业务部门编号',
`memo` varchar(100) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '备注',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`process_id`),
KEY `IX_DEPT_ID` (`dept_id`),
KEY `IX_CREATE_TIME` (`create_time`),
KEY `IX_CUSTOMER_ID` (`customer_id`) USING BTREE,
KEY `IX_MEMO` (`memo`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='商机流程表' PARTITION BY RANGE(process_id) (
PARTITION p1 VALUES LESS THAN (1000),
PARTITION p2 VALUES LESS THAN (2000),
PARTITION p3 VALUES LESS THAN (3000),
PARTITION p4 VALUES LESS THAN (MAXVALUE)
);

3.3 分区后,如果在查询时想要避免搜索所有分区,则最好在查询的where条件中添加分区字段作为过滤条件。哪怕是冗余的没有必要的过滤条件,对于定位分区还是有用的。

例如,普通查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mysql> EXPLAIN PARTITIONS select * from range_process \G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: range_process
partitions: p1,p2,p3,p4
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 20
filtered: 100.00
Extra: NULL
1 row in set, 2 warnings (0.00 sec)

添加主键作为where条件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
EXPLAIN PARTITIONS select * from range_process where process_id > 3000 \G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: range_process
partitions: p4
type: range
possible_keys: PRIMARY
key: PRIMARY
key_len: 8
ref: NULL
rows: 10
filtered: 100.00
Extra: Using where
1 row in set, 2 warnings (0.00 sec)

  • 在应用层,对于做了分区的大表,进行查询时,可以通过动态sql的方式,在需要优化的sql查询处加入分区字段作为where的过滤条件。

3.4 分区表使用的限制

  • 一个表最多只能有1024个分区
  • 主键和唯一索引的列,必须作为分区的字段

3.5 分区后,一般情况下,各种查询使用和join联表操作均正常使用,和分区前一致,但要注意几个问题

  • 关于优化,应该尽量使用where条件,将检索的分区限制再少数分区中。
  • (mysql5.5以上不需要考虑这个问题)NULL值会使分区过滤无效,并在分区中创建一个默认的第一个分区,从而导致所有查询都会默认去检索这个分区,所以,应该创建一个“无用”的第一个分区,导致数据无法落入这个分区,从而避免mysql查询每次检索第一个分区。mysql5.5以上不需要考虑这个问题,可以直接使用列本身进行分区,就不会有这个问题。
  • 对于大多数系统,100个左右的分区是可以接受的,不会有太大问题。

配置Maven项目打包部署到Nexus私服

发表于 2019-11-18 | 分类于 java

配置Maven项目打包部署到Nexus私服

要点总结:

  • 对于公共二方库的开发人员,需要配置maven/conf下setting.xml配置,增加节点。项目中已添加私服仓库地址和部署插件,开发人员不需要再配置。
  • 对于使用公共二方库的项目,需要在maven配置文件中的mirrors添加镜像。也可以在项目的pom文件中添加仓库,然后在pom文件中添加需要使用的库的即可。

1. maven/conf下setting.xml配置,增加节点

增加节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<servers>
<server>
<id>maven-public</id>
<username>dev</username>
<password>devmvn.db</password>
</server>
<server>
<id>maven-snapshots</id>
<username>dev</username>
<password>devmvn.db</password>
</server>
<server>
<id>maven-releases</id>
<username>dev</username>
<password>devmvn.db</password>
</server>
</servers>

2. 二方库项目中pom.xml配置,增加distributionManagement节点,注意与properties节点平级。

1
2
3
4
5
6
7
8
9
10
11
12
13
<distributionManagement>
<repository>
<id>maven-releases</id>
<name>maven-releases</name>
<url>http://ip:port/repository/maven-releases/</url>
<layout>default</layout>
</repository>
<snapshotRepository>
<id>maven-snapshots</id>
<name>maven-snapshots</name>
<url>http://ip:port/repository/maven-snapshots/</url>
</snapshotRepository>
</distributionManagement>
  • 需要打包部署到私服的工程,则执行maven命令mvn clean deploy即可

3. 需要使用依赖的二方库的工程,则需要在maven的conf中配置mirrors节点,或者在工程的pom文件中配置好私服仓库地址

  • 方式1:mirrors:
1
2
3
4
5
<mirror>     
<id>bolingcavalry-nexus-releases</id>
<mirrorOf>*</mirrorOf>
<url>http://ip:port/repository/maven-public/</url>
</mirror>
  • 方式2:在工程的pom文件中配置私服仓库地址
1
2
3
4
5
6
7
<repositories>
<repository>
<id>maven-public</id>
<name>maven-public</name>
<url>http://ip:port/repository/maven-public/</url>
</repository>
</repositories>

4. 依赖准备

  1. 在Nexus服务器上创建一个能deploy的Nexus账号用户名和密码
  2. 设置账号为允许上传release的jar包
  3. 设置账号为允许上传snapshots的jar包

5. 备选配置记录

部署到nexus,除了在pom中添加部署插件,也可以以设置maven config文件的形式设置profile,配置方法记录如下:

5.1. 方式1

5.1.1 设置私服地址,在节点之下增加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<profile>
    <id>test</id>
<repositories>
<repository>
  <id>test</id>
  <name>test</name>
  <!-- 该 url 没有意义,可以随便写,但必须有。 -->  
<url>http://*</url>
  <releases><enabled>true</enabled></releases>  
  <snapshots><enabled>true</enabled></snapshots>  
</repository>
    </repositories>
<pluginRepositories>
<pluginRepository>
<id>test</id>
<name>local private nexus</name>
<url>http://ip:port/repository/maven-public/</url>
            <releases><enabled>true</enabled></releases>  
            <snapshots><enabled>true</enabled></snapshots>  
         </pluginRepository>  
    </pluginRepositories>  
</profile>

5.1.2 激活配置

1
2
3
<activeProfiles>
<activeProfile>test</activeProfile>
</activeProfiles>

5.2. 方式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
<repository>
<id>maven-releases</id>
<url>http://ip:port/repository/maven-releases/</url>
<releases><enabled>true</enabled></releases>
<snapshots><enabled>true</enabled></snapshots>
</repository>
<repository>
<id>maven-snapshots</id>
<url>http://ip:port/repository/maven-snapshots/</url>
<releases><enabled>true</enabled></releases>
<snapshots><enabled>true</enabled></snapshots>
</repository>


<pluginRepository>
<id>maven-releases</id>
<url>http://ip:port/repository/maven-releases/</url>
<releases><enabled>true</enabled></releases>
<snapshots><enabled>true</enabled></snapshots>
</pluginRepository>
<pluginRepository>
<id>maven-snapshots</id>
<url>http://ip:port/repository/maven-snapshots/</url>
<releases><enabled>true</enabled></releases>
<snapshots><enabled>true</enabled></snapshots>
</pluginRepository>

RESTful接口设计规范

发表于 2019-11-04 | 分类于 design

RESTful API 设计规范

RESTful API是目前比较成熟的一套互联网应用程序的API设计理论,是目前最流行的API设计规范,用于Web数据接口的设计。

什么是RESTful架构:

  • (1)每一个URI代表一种资源;
  • (2)客户端和服务器之间,传递这种资源的某种表现层;
  • (3)客户端通过四个HTTP动词,对服务器端资源进行操作,实现”表现层状态转化”。

1. 协议

API与用户的通信协议,总是使用HTTPs协议。

2. 域名

应该尽量将API部署在专用域名之下。

1
https://api.example.com

如果确定API很简单,不会有进一步扩展,可以考虑放在主域名下。

1
https://example.org/api/

3. 版本(Versioning)

应该将API的版本号放入URL。

1
https://api.example.com/v1/

另一种做法是,将版本号放在HTTP头信息中,但不如放入URL方便和直观。Github采用这种做法。

4. 路径(Endpoint)

路径又称”终点”(endpoint),表示API的具体网址。

在RESTful架构中,每个网址代表一种资源(resource),所以网址中不能有动词,只能有名词,而且所用的名词往往与数据库的表格名对应。一般来说,数据库中的表都是同种记录的”集合”(collection),所以API中的名词也应该使用复数。

举例来说,有一个API提供动物园(zoo)的信息,还包括各种动物和雇员的信息,则它的路径应该设计成下面这样。

1
2
3
https://api.example.com/v1/zoos
https://api.example.com/v1/animals
https://api.example.com/v1/employees

而下面的 URL 不是名词,所以都是错误的。

1
2
3
/getAllCars
/createNewCar
/deleteAllRedCars

5. HTTP动词

对于资源的具体操作类型,由HTTP动词表示。

常用的HTTP动词有下面五个(括号里是对应的SQL命令)。

1
2
3
4
5
GET(SELECT):从服务器取出资源(一项或多项)。
POST(CREATE):在服务器新建一个资源。
PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整资源)。
PATCH(UPDATE):在服务器更新资源(客户端提供改变的属性)。
DELETE(DELETE):从服务器删除资源。

还有两个不常用的HTTP动词。

1
2
HEAD:获取资源的元数据。
OPTIONS:获取信息,关于资源的哪些属性是客户端可以改变的。

下面是一些例子。

1
2
3
4
5
6
7
8
GET /zoos:列出所有动物园
POST /zoos:新建一个动物园
GET /zoos/ID:获取某个指定动物园的信息
PUT /zoos/ID:更新某个指定动物园的信息(提供该动物园的全部信息)
PATCH /zoos/ID:更新某个指定动物园的信息(提供该动物园的部分信息)
DELETE /zoos/ID:删除某个动物园
GET /zoos/ID/animals:列出某个指定动物园的所有动物
DELETE /zoos/ID/animals/ID:删除某个指定动物园的指定动物

6. 过滤信息(Filtering)

如果记录数量很多,服务器不可能都将它们返回给用户。API应该提供参数,过滤返回结果。

下面是一些常见的参数。

1
2
3
4
5
?limit=10:指定返回记录的数量
?offset=10:指定返回记录的开始位置。
?page=2&per_page=100:指定第几页,以及每页的记录数。
?sortby=name&order=asc:指定返回结果按照哪个属性排序,以及排序顺序。
?animal_type_id=1:指定筛选条件

参数的设计允许存在冗余,即允许API路径和URL参数偶尔有重复。比如,GET /zoo/ID/animals 与 GET /animals?zoo_id=ID 的含义是相同的。

7. 状态码(Status Codes)

服务器向用户返回的状态码和提示信息,常见的有以下一些(方括号中是该状态码对应的HTTP动词)。

1
2
3
4
5
6
7
8
9
10
11
12
13
200 OK - [GET]:服务器成功返回用户请求的数据,该操作是幂等的(Idempotent)。
201 CREATED - [POST/PUT/PATCH]:用户新建或修改数据成功。
202 Accepted - [*]:表示一个请求已经进入后台排队(异步任务)
204 NO CONTENT - [DELETE]:用户删除数据成功。
400 INVALID REQUEST - [POST/PUT/PATCH]:用户发出的请求有错误,服务器没有进行新建或修改数据的操作,该操作是幂等的。
401 Unauthorized - [*]:表示用户没有权限(令牌、用户名、密码错误)。
403 Forbidden - [*] 表示用户得到授权(与401错误相对),但是访问是被禁止的。
404 NOT FOUND - [*]:用户发出的请求针对的是不存在的记录,服务器没有进行操作,该操作是幂等的。
406 Not Acceptable - [GET]:用户请求的格式不可得(比如用户请求JSON格式,但是只有XML格式)。
410 Gone -[GET]:用户请求的资源被永久删除,且不会再得到的。
422 Unprocesable entity - [POST/PUT/PATCH] 当创建一个对象时,发生一个验证错误。
500 INTERNAL SERVER ERROR - [*]:服务器发生错误,用户将无法判断发出的请求是否成功。
状态码的完全列表参见这里。

8. 错误处理(Error handling)

如果状态码是4xx,就应该向用户返回出错信息。一般来说,返回的信息中将error作为键名,出错信息作为键值即可。

1
2
3
{
error: "Invalid API key"
}

9. 返回结果

针对不同操作,服务器向用户返回的结果应该符合以下规范。

1
2
3
4
5
6
GET /collection:返回资源对象的列表(数组)
GET /collection/resource:返回单个资源对象
POST /collection:返回新生成的资源对象
PUT /collection/resource:返回完整的资源对象
PATCH /collection/resource:返回完整的资源对象
DELETE /collection/resource:返回一个空文档

10. Hypermedia API

RESTful API最好做到Hypermedia,即返回结果中提供链接,连向其他API方法,使得用户不查文档,也知道下一步应该做什么。

比如,当用户向api.example.com的根目录发出请求,会得到这样一个文档。

1
2
3
4
5
6
{"link": {
"rel": "collection https://www.example.com/zoos",
"href": "https://api.example.com/zoos",
"title": "List of zoos",
"type": "application/vnd.yourformat+json"
}}

上面代码表示,文档中有一个link属性,用户读取这个属性就知道下一步该调用什么API了。rel表示这个API与当前网址的关系(collection关系,并给出该collection的网址),href表示API的路径,title表示API的标题,type表示返回类型。

Hypermedia API的设计被称为HATEOAS。Github的API就是这种设计,访问api.github.com会得到一个所有可用API的网址列表。

1
2
3
4
5
{
"current_user_url": "https://api.github.com/user",
"authorizations_url": "https://api.github.com/authorizations",
// ...
}

从上面可以看到,如果想获取当前用户的信息,应该去访问api.github.com/user,然后就得到了下面结果。

1
2
3
4
{
"message": "Requires authentication",
"documentation_url": "https://developer.github.com/v3"
}

上面代码表示,服务器给出了提示信息,以及文档的网址。

11. RESTful API 最佳实践

11.1. URL 设计,动词的覆盖

有些客户端只能使用GET和POST这两种方法。服务器必须接受POST模拟其他三个方法(PUT、PATCH、DELETE)。

这时,客户端发出的 HTTP 请求,要加上X-HTTP-Method-Override属性,告诉服务器应该使用哪一个动词,覆盖POST方法。

1
2
POST /api/Person/4 HTTP/1.1  
X-HTTP-Method-Override: PUT

上面代码中,X-HTTP-Method-Override指定本次请求的方法是PUT,而不是POST。

11.2. 避免多级 URL

常见的情况是,资源需要多级分类,因此很容易写出多级的 URL,比如获取某个作者的某一类文章。

1
GET /authors/12/categories/2

这种 URL 不利于扩展,语义也不明确,往往要想一会,才能明白含义。

更好的做法是,除了第一级,其他级别都用查询字符串表达。

1
GET /authors/12?categories=2

下面是另一个例子,查询已发布的文章。你可能会设计成下面的 URL。

1
GET /articles/published

查询字符串的写法明显更好。

1
GET /articles?published=true

11.3. 状态码

11.3.1 状态码必须精确

客户端的每一次请求,服务器都必须给出回应。回应包括 HTTP 状态码和数据两部分。

HTTP 状态码就是一个三位数,分成五个类别。

1
2
3
4
5
1xx:相关信息
2xx:操作成功
3xx:重定向
4xx:客户端错误
5xx:服务器错误

这五大类总共包含100多种状态码,覆盖了绝大部分可能遇到的情况。每一种状态码都有标准的(或者约定的)解释,客户端只需查看状态码,就可以判断出发生了什么情况,所以服务器应该返回尽可能精确的状态码。

API 不需要1xx状态码,下面介绍其他四类状态码的精确含义。

11.3.2 2xx 状态码

200状态码表示操作成功,但是不同的方法可以返回更精确的状态码。

1
2
3
4
5
GET: 200 OK
POST: 201 Created
PUT: 200 OK
PATCH: 200 OK
DELETE: 204 No Content

上面代码中,POST返回201状态码,表示生成了新的资源;DELETE返回204状态码,表示资源已经不存在。

此外,202 Accepted状态码表示服务器已经收到请求,但还未进行处理,会在未来再处理,通常用于异步操作。下面是一个例子。

1
2
3
4
5
6
7
8
HTTP/1.1 202 Accepted

{
"task": {
"href": "/api/company/job-management/jobs/2130040",
"id": "2130040"
}
}

11.3.3 3xx 状态码

API 用不到301状态码(永久重定向)和302状态码(暂时重定向,307也是这个含义),因为它们可以由应用级别返回,浏览器会直接跳转,API 级别可以不考虑这两种情况。

API 用到的3xx状态码,主要是303 See Other,表示参考另一个 URL。它与302和307的含义一样,也是”暂时重定向”,区别在于302和307用于GET请求,而303用于POST、PUT和DELETE请求。收到303以后,浏览器不会自动跳转,而会让用户自己决定下一步怎么办。下面是一个例子。

1
2
HTTP/1.1 303 See Other
Location: /api/orders/12345

11.3.4 4xx 状态码

4xx状态码表示客户端错误,主要有下面几种。

  • 400 Bad Request:服务器不理解客户端的请求,未做任何处理。
  • 401 Unauthorized:用户未提供身份验证凭据,或者没有通过身份验证。
  • 403 Forbidden:用户通过了身份验证,但是不具有访问资源所需的权限。
  • 404 Not Found:所请求的资源不存在,或不可用。
  • 405 Method Not Allowed:用户已经通过身份验证,但是所用的 HTTP 方法不在他的权限之内。
  • 410 Gone:所请求的资源已从这个地址转移,不再可用。
  • 415 Unsupported Media Type:客户端要求的返回格式不支持。比如,API 只能返回 JSON 格式,但是客户端要求返回 XML 格式。
  • 422 Unprocessable Entity :客户端上传的附件无法处理,导致请求失败。
  • 429 Too Many Requests:客户端的请求次数超过限额。

11.3.5 5xx 状态码

5xx状态码表示服务端错误。一般来说,API不会向用户透露服务器的详细信息,所以只要两个状态码就够了。

  • 500 Internal Server Error:客户端请求有效,服务器处理时发生了意外。
  • 503 Service Unavailable:服务器无法处理请求,一般用于网站维护状态。

11.4. 服务器回应

11.4.1 不要返回纯本文

API 返回的数据格式,不应该是纯文本,而应该是一个 JSON 对象,因为这样才能返回标准的结构化数据。所以,服务器回应的 HTTP 头的Content-Type属性要设为application/json。

客户端请求时,也要明确告诉服务器,可以接受 JSON 格式,即请求的 HTTP 头的ACCEPT属性也要设成application/json。下面是一个例子。

1
2
GET /orders/2 HTTP/1.1 
Accept: application/json

11.4.2 发生错误时,不要返回 200 状态码

有一种不恰当的做法是,即使发生错误,也返回200状态码,把错误信息放在数据体里面,就像下面这样。

1
2
3
4
5
6
7
8
9
HTTP/1.1 200 OK
Content-Type: application/json

{
"status": "failure",
"data": {
"error": "Expected at least two items in list."
}
}

上面代码中,解析数据体以后,才能得知操作失败。

这张做法实际上取消了状态码,这是完全不可取的。正确的做法是,状态码反映发生的错误,具体的错误信息放在数据体里面返回。下面是一个例子。

1
2
3
4
5
6
7
8
9
HTTP/1.1 400 Bad Request
Content-Type: application/json

{
"error": "Invalid payoad.",
"detail": {
"surname": "This field is required."
}
}

参考资料:

  • RESTful API 设计指南:http://www.ruanyifeng.com/blog/2014/05/restful_api.html
  • RESTful API 最佳实践:http://www.ruanyifeng.com/blog/2018/10/restful-api-best-practices.html
  • Learn REST: A RESTful Tutorial:https://www.restapitutorial.com/

java后端项目开发规范

发表于 2019-11-04 | 分类于 manager

一. mysql数据库

(一)建表约定

1.【强制】表名、字段名必须使用小写字母或数字,禁止出现数字开头,禁止两个下划线中间只出现数字。数据库字段名的修改代价很大,因为无法进行预发布,所以字段名称需要慎重考虑。

说明:MySQL 在 Windows 下不区分大小写,但在 Linux下默认是区分大小写。因此,数据库名、表名、字段名,都不允许出现任何大写字母,避免节外生枝。

正例:aliyun_admin,rdc_config,level3_name

反例:AliyunAdmin,rdcConfig,level_3_name

2.【强制】小数类型为 decimal,在需要精确经度的时候禁止使用 float和double。

说明:float 和 double在存储的时候,存在精度损失的问题,很可能在值的比较时,得到不正确的结果。如果存储的数据范围超过decimal的范围,建议将数据拆成整数和小数分开存储。

3.【强制】表示状态类型等的数字可枚举类的字段,数据类型应该使用tinyint。如果字段为非负数,数据类型是unsigned tinyint.

说明:任何字段如果为非负数,必须是 unsigned。

4.【强制】表名不使用复数名词

5.【强制】主键索引名为 pk_字段名;唯一索引名为 uk_字段名;普通索引名则为 idx_字段名。

说明:pk_ 即 primary key;uk_ 即 unique key;idx_ 即 index 的简称。

6.【强制】如果存储的字符串长度几乎相等,使用 char定长字符串类型。

7.【强制】varchar是可变长字符串,不预先分配存储空间,长度不要超过 5000,如果存储长度大于此值,定义字段类型为 text,独立出来一张表,用主键来对应,避免影响其它字段索引效率。

8.【推荐】表的命名最好是加上“业务名称_表的作用”。

9.【推荐】字段允许适当冗余,以提高查询性能,但必须考虑数据一致。

冗余字段应遵循:

 1)不是频繁修改的字段。

 2)不是 varchar 超长字段,更不能是 text 字段。

正例:商品类目名称使用频率高,字段长度短,名称基本一成不变,可在相关联的表中冗余存储类目名称,避免关联查询。

(二)索引约定

1.【强制】超过三个表禁止 join。需要join的字段,数据类型必须绝对一致;多表关联查询时,保证被关联的字段需要有索引。

说明:即使双表 join 也要注意表索引、SQL 性能。

2.【强制】在varchar字段上建立索引时,必须指定索引长度,没必要对全字段建立索引,根据实际文本区分度决定索引长度即可。

说明:索引的长度与区分度是一对矛盾体,一般对字符串类型数据,长度为 20 的索引,区分度会高达 90%以上,可以使用 count(distinct left(列名, 索引长度))/count(*)的区分度
来确定。

3.【推荐】页面搜索严禁左模糊或者全模糊,如果需要请走搜索引擎来解决。

说明:索引文件具有B-Tree的最左前缀匹配特性,如果左边的值未确定,那么无法使用此索
引。

4.【推荐】如果有 order by的场景,请注意利用索引的有序性。order by最后的字段是组合索引的一部分,并且放在索引组合顺序的最后,避免出现 file_sort 的情况,影响查询性能。

正例:where a=? and b=? order by c; 索引:a_b_c

反例:索引中有范围查找,那么索引有序性无法利用,如:WHERE a>10 ORDER BY b; 索引
a_b 无法排序。

5.【推荐】利用延迟关联或者子查询优化超多分页场景。

说明:MySQL 并不是跳过 offset 行,而是取 offset+N 行,然后返回放弃前 offset 行,返回N行,那当offset特别大的时候,效率就非常的低下,要么控制返回的总页数,要么对超过特定阈值的页数进行 SQL 改写。

正例:先快速定位需要获取的 id 段,然后再关联:

1
SELECT a.* FROM 表 1 a, (select id from 表 1 where 条件 LIMIT 100000,20 ) b where a.id=b.id

6.【推荐】建组合索引的时候,区分度最高的在最左边。

正例:如果

1
where a=? and b=?

,a列的几乎接近于唯一值,那么只需要单建idx_a索引即可。

说明:存在非等号和等号混合判断条件时,在建索引时,请把等号条件的列前置。如:

1
where a>?and b=?

那么即使 a 的区分度更高,也必须把 b 放在索引的最前列。

(三)SQL语句约定

1.【强制】不得使用外键与级联,一切外键概念必须在应用层解决。

说明:以学生和成绩的关系为例,学生表中的student_id是主键,那么成绩表中的student_id则为外键。如果更新学生表中的 student_id,同时触发成绩表中的 student_id 更新,即为级联更新。外键与级联更新适用于单机低并发,不适合分布式、高并发集群;级联更新是强阻塞,存在数据库更新风暴的风险;外键影响数据库的插入速度。

2.【强制】不要使用 count(列名)或 count(常量)来替代 count(),count()是 SQL92 定义的标准统计行数的语法,跟数据库无关,跟 NULL 和非 NULL 无关。

说明:count(*)会统计值为 NULL 的行,而 count(列名)不会统计此列为 NULL 值的行。

3.【强制】当某一列的值全是 NULL 时,count(col)的返回结果为 0,但 sum(col)的返回结果为NULL,因此使用 sum()时需注意 NPE 问题。

正例:可以使用如下方式来避免 sum 的 NPE 问题:

1
SELECT IF(ISNULL(SUM(g)),0,SUM(g)) FROM table;

4.【强制】在代码中写分页查询逻辑时,若 count 为 0 应直接返回,避免执行后面的分页语句。

5.【强制】禁止使用存储过程,存储过程难以调试和扩展,更没有移植性。

6.【强制】数据订正时,删除和修改记录时,要先 select,避免出现误删除,确认无误才能执

行更新语句。

7.【推荐】in 操作能避免则避免,若实在避免不了,需要仔细评估 in 后边的集合元素数量,控

制在 1000 个之内。

8.【参考】如果有全球化需要,所有的字符存储与表示,均以 utf-8 编码,注意字符统计函数的区别。

说明:

  • SELECT LENGTH(“轻松工作”); 返回为 12
  • SELECT CHARACTER_LENGTH(“轻松工作”); 返回为 4

如果需要存储表情,那么选择 utfmb4 来进行存储,注意它与 utf-8 编码的区别。

(四)ORM 映射

1.【强制】在表查询中,一律不要使用 *作为查询的字段列表,需要哪些字段必须明确写明。

说明:

1)增加查询分析器解析成本。
2)增减字段容易与 resultMap 配置不一致。

2.【强制】不要用 resultClass 当返回参数,即使所有类属性名与数据库字段一一对应,也需要定义;反过来,每一个表也必然有一个与之对应。

说明:配置映射关系,使字段与 DO 类解耦,方便维护。

3.【强制】sql.xml 配置参数使用:#{},#param# 不要使用${} 此种方式容易出现 SQL 注入。

4.【推荐】不要直接拿 HashMap 与 Hashtable 作为查询结果集的输出。

说明:resultClass=”Hashtable”,会置入字段名和属性值,但是值的类型不可控。

5.【推荐】不要写一个大而全的数据更新接口。传入为 POJO类,不管是不是自己的目标更新字段,都进行 update table set c1=value1,c2=value2,c3=value3; 这是不对的。执行 SQL时,不要更新无改动的字段,一是易出错;二是效率低;三是增加binlog存储.

2. 工程结构

(一)应用分层

1. 【推荐】图中默认上层依赖于下层,箭头关系表示可直接依赖,如:开放接口层可以依赖于Web 层,也可以直接依赖于 Service 层,依此类推:

  • 开放接口层:可直接封装 Service 方法暴露成 RPC 接口;通过 Web 封装成 http 接口;进行网关安全控制、流量控制等。
  • 终端显示层:各个端的模板渲染并执行显示的层。当前主要是 velocity 渲染,JS 渲染,JSP 渲染,移动端展示等。
  • Web层:主要是对访问控制进行转发,各类基本参数校验,或者不复用的业务简单处理等。
  • Service 层:相对具体的业务逻辑服务层。
  • Manager 层:通用业务处理层,它有如下特征:

     1) 对第三方平台封装的层,预处理返回结果及转化异常信息;

     2) 对 Service 层通用能力的下沉,如缓存方案、中间件通用处理;

     3) 与 DAO 层交互,对多个 DAO 的组合复用。

  • DAO 层:数据访问层,与底层 MySQL、Oracle、Hbase 等进行数据交互。
     外部接口或第三方平台:包括其它部门 RPC 开放接口,基础平台,其它公司的 HTTP 接口。

(二)二方库依赖

1. 【强制】定义 GAV 遵从以下规则:

 1) GroupID 格式:com.{公司/BU }.业务线.[子业务线],最多 4 级。
说明:{公司/BU} 例如:alibaba/taobao/tmall/aliexpress 等 BU 一级;子业务线可选。
正例:com.taobao.jstorm 或 com.alibaba.dubbo.register

 2) ArtifactID 格式:产品线名-模块名。语义不重复不遗漏,先到中央仓库去查证一下。
正例:dubbo-client / fastjson-api / jstorm-tool

 3) Version:详细规定参考下方。

2. 【强制】二方库版本号命名方式:主版本号.次版本号.修订号

 1) 主版本号:产品方向改变,或者大规模 API 不兼容,或者架构不兼容升级。

 2) 次版本号:保持相对兼容性,增加主要功能特性,影响范围极小的 API 不兼容修改。

 3) 修订号:保持完全兼容性,修复 BUG、新增次要功能特性等。

说明:注意起始版本号必须为:1.0.0,而不是 0.0.1正式发布的类库必须先去中央仓库进
行查证,使版本号有延续性,正式版本号不允许覆盖升级。如当前版本:1.3.3,那么下一个合理的版本号:1.3.4 或 1.4.0 或 2.0.0

3. 【强制】线上应用不要依赖 SNAPSHOT 版本(安全包除外)。

说明:不依赖SNAPSHOT版本是保证应用发布的幂等性。另外,也可以加快编译时的打包构建。

3. 编程约定

springboot-不同环境的编译打包运行

发表于 2019-11-04 | 分类于 java-spring

不同环境的编译打包运行

项目一共4套环境

  • local:本地开发的环境
  • dev:开发环境
  • test:测试环境
  • prod:正式环境

不同环境的打包

使用mvn clean package -P${profile.active}命令对不同环境进行打包

profile.active变量取值:

  • local
  • dev
  • test
  • prod

例如:要对测试环境打包,则使用命令mvn clean package -Ptest

这样application.yml文件中的spring.profiles.active取值就是:test,从而使用application-test.yml文件

运行

本地IDE直接运行BacApplication.java

开发和测试环境打包后,使用java -jar运行即可,其他参数可自行定制,无需再指定环境

  • 对于正式环境,在jar包上传到线上服务器后,可以通过运行参数选择存放于服务器上的特定配置文件
script
1
java -jar target/bac.jar --spring.config.location=file://${file.path}

变量file.path为配置文件路径

例如:

  • linux
script
1
java -jar target/bac.jar --spring.config.location=file:///data/bac/config/application-prod.yml
  • windows
script
1
java -jar target/bac.jar --spring.config.location=file:///D:/bac/application-local.yml
1
2
3
spring:
profiles:
active: @profile.active@
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
<profiles>
<profile>
<id>local</id>
<properties>
<profile.active>local</profile.active>
</properties>
</profile>
<profile>
<id>dev</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<profile.active>dev</profile.active>
</properties>
</profile>
<profile>
<id>test</id>
<properties>
<profile.active>test</profile.active>
</properties>
</profile>
<profile>
<id>prod</id>
<properties>
<profile.active>prod</profile.active>
</properties>
</profile>
</profiles>

Hello World

发表于 2019-05-22

Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub.

Quick Start

Create a new post

1
$ hexo new "My New Post"

More info: Writing

Run server

1
$ hexo server

More info: Server

Generate static files

1
$ hexo generate

More info: Generating

Deploy to remote sites

1
$ hexo deploy

More info: Deployment

[Go]写扩展性好的代码:函数

发表于 2018-12-14 | 更新于 2019-05-22 | 分类于 Go

如何写出扩展性好的代码?这是我工作最近半年来一直在考虑的问题。不管自己做一套系统还是接手别人的项目,只要你的项目需要和别人交互,这个问题都是需要考虑的。我们今天只说说如何写出扩展性好的函数代码。代码都以golang示例。

函数声明
函数声明首先是函数名字要具有自解释性,这个要说到代码注释了,这里就不赘述了。除了函数声明外,还有函数的形参定义。这里以一个例子来说一下扩展性好的函数的参数应该如何定义。

  1. 普通函数
    假设我们需要一个简单的server,我们可以像下面这样定义,addr表示server启动在哪个端口上。
1
func NewServer(addr string)

第一期的需求很简单,就上面这些足够满足了。项目上线跑了一段时间发现,由于连接没有设置超时,很多连接一直得不到释放(异常情况),严重影响服务器性能。好,那第二期我们加个timeout。

1
func NewServer(addr string, timeout time.Duration)

这个时候尴尬的情况出现了,调用你代码的所有人都需要改动代码。而且这只是一个改动,之后如果要支持tls,那么又得改动一次。

  1. 不定参数
    解决上面的窘境的一种方法是使用不定参数。下面先简单介绍一下不定参数。第一次接触不定参数是学习C语言中的Hello World的代码中printf,声明如下
1
static int printf(const char *fmt, ...)

C的函数调用可以简单看成call/ret,call的时候会把当前的IP保存起来,然后将函数地址以及函数参数入栈。printf的fmt中保存了参数的类型(%d表示int,%s表示string)并能计算出个数,这样就能找到每个具体的参数是什么了。golang也是支持不定参数的,比如我要实现一个整数加法。

1
2
3
4
5
6
7
8
9
10
11
func Add(list ...int) int {
sum := 0
for _, x := range list {
sum += x
}
return sum
}

func main() {
fmt.Println(Add(1,2,3)) //6
}

上面是所有的变参都是同一种类型,如果是不同的类型可以使用interface,使用反射来判断其类型。

1
2
3
4
5
6
7
func Varargs(list ...interface{}) {
for _, x := range list {
if reflect.ValueOf(x).Kind() == int {
//
}
}
}

但是如果是我们自己定义的函数的话,类型通常是知道的,也就不需要上面那么麻烦地再去判断一次,可以直接进行类型转换。

1
2
3
4
5
func Varargs(list ...interface{}) {
//通过interface.(type)将interface类型转换成type类型
fmt.Println(list[0].(int))
fmt.Println(list[1].(string))
}

但是这么做比较危险,使用的时候必须严格按照说明进行传参,任何一种类型不正确,程序将panic。还有一个问题就是不定参数不能为空,或者说传入的实参必须是形参的一个严格前缀。

  1. 封装成 struct
    相比于上面两种方法更好一点的是把所有参数封装成struct,这样函数声明看起来很简单。
1
2
3
4
5
6
7
type Param struct {
x int
y string
...
}

func Varargs(p *Param) {}

封装成struct的方式应该是一种对参数比较好的组织形式,之后函数不管怎么扩张,只需要增加struct成员就好,而不需要改变函数声明了。而struct的坏处在什么地方呢?比如上面的Param.x是int型,如果我们不设置x,也就是下面这样传参。

1
2
3
4
5
p := &Param{
y: "hello",
}

Varargs(p)

这个时候Varargs看到的Param.x的0。你让Varargs怎么想?用户没有设置x(忘记设置?想使用默认值?)?用户把x设置成0?这真的有点尴尬。但是这个问题还是有解决方案的?1.避开默认值,int型不使用0,string类型不使用””。2.使用指针,用户没有设置的时候x==nil,设置的时候对x解引用(*x)取得值。这两种方式不管怎么来看,都是十分的反人类,一点也不simple。

  1. option
    option的方式的最早是由 Rob Pike 提出,Rob Pike就不做介绍了,感兴趣的可以看他的wiki连接。我们把option参数封装成一个函数传给我们的目标函数,所有相关的工作由函数来做。举个栗子,我们现在要写个Server,timeout和tls都是可选项,那么可以像下面这么来写(所有error handle都省去)。
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
func NewServer(addr string, options ...func(*Server)) (*Server, error) {
srv := &Server{
Addr: addr,
}

for _, option := range options {
option(srv)
}

return srv
}

func timeout(d time.Duration) func(*Server) {
return func(srv *Server) {
srv.timeout = d
}
}

func tls(c *config) func(*Server) {
return func(srv *Server) {
Tls := loadConfig(c)
srv.tls = Tls
}
}

//使用
src, err = NewServer("localhost:8080", timeout(1), tls(path/to/cert))

这么写的好处一目了然,横向扩展起来特别方便,而且解决上面的提到的基本所有的问题。

函数实现
正常单一功能的函数实现没有什么好说的。如果需要根据不同的条件来执行不同的行为的话,这个应该怎么做的?举个例子,我现在在公司做一个优惠券的项目,用户领券和使用券的时候有一些规则,比如每人每日限领3张等。这些规则肯定不会一成不变,也许第一期是2个规则,第二期就变成4个规则了。正常可能会像下面这么写。

1
2
3
4
5
6
7
8
9
func ruleVerify() {
//process
if cond1 {
//
} else if cond2 {
//
}
...
}

或者用switch-case。虽然很多人说switch-case写起来要比if-else更好看或者高端一点,其实我并不这么觉得。if-else和switch-case本质上并没有什么区别,扩展的时候如果需要多加一个条件分支,这两种方法改动起来都比较丑。下面说说我的解决方案。

  1. 类工厂模式
    熟悉设计模式的肯定对工厂模式肯定不会陌生。工厂模式的意思是通过参数来决定生成什么样的对象实例。我这里并不是说直接使用工厂模式而是使用工厂模式这种思想来编程。举个典型的例子,webserver的router实现方式:根据不同的路由(/foo,/bar)对应到不同的handler。光这么说,可能很多人还是不明白这种方式的扩展性好在什么地方。下面从0到1来感受一下。
    首先根据不同的条件对应不同的handler,这个最简单的是使用Map来实现,没有问题,但是map里面存什么呢?如果我要增加一个条件以及对应的处理函数的时候怎么做呢?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//存放 <cond, handler> 对应关系
var mux map[string]func(option *Option) error

//注册handler
func register(key string, f func(option *Option) error) {
if mux == nil {
mux = make(map[string]func(option *Option) error)
}
if _, exist := mux[key]; exist {
return errors.New("handler exist")
}
mux[key] = f
}

//factory
func factory(option *Option) error {
return mux[option.Key](option)
}

代码主要分三个部分:1.mux用来存放cond和handler的对应关系;2.register用来注册新的handler; 3.提供给外部的代码入口。下面到了最核心的问题了,如果某一天PM和你说:大神,我们现在要新加一个用户用券规则。这个时候你就可以和她说:没问题。代码上的改动只需要实现一个新增规则的实现函数,同时调用一下register即可。

[elasticsearch]windows下安装elasticsearch5-6

发表于 2018-12-13 | 更新于 2019-05-22 | 分类于 elasticsearch

elasticsearch>5后,elasticsearch-head

不能放在elasticsearch的 plugins、modules 目录下

不能使用 elasticsearch-plugin install

直接启动elasticsearch即可

安装 elasticsearch-head

修改 elasticsearch/config/elasticsearch.yml

添加

1
2
http.cors.enabled: true
http.cors.allow-origin: "*"

下载 elasticsearch-head 或者 git clone 到随便一个文件夹

安装nodejs

1
2
3
4
5
6
7
cd /path/to/elasticsearch-head

npm install -g grunt-cli

npm install

grunt server

http://localhost:9100/

Enjoy it.

[design]多认证方式的情况下如何设计帐号模型表

发表于 2018-12-13 | 更新于 2019-05-22 | 分类于 design

用户帐号登录表设计

当存在密码登录、第三方授权登录等登录方式的时候,系统的用户帐号模型应该如何设计?

1. 首先,这是一种思考方案

用户profile表一张,相当于用户总表

帐号密码登录,作为一种登录方式,搞一张表

OAuth作为一种登录方式,也建一张表;如果还有别的登录方式,也可以创建对应的认证表;

有些网站需要API访问,API可以使用api_key和api_secret来认证,可是怎么把一个API访问关联到一个用户?方法还是增加一种API Auth的表:

1
id | user_id | api_key  | api_secret

每一种X-Auth表都存储了用户的登录认证信息,并通过user_id关联到Users表。这样一来,不但登录过程简化了,而且一个用户可以使用多种方式登录。只要登录成功,拿到了user_id,最后读取Users表是为了获得用户的Profile

2. 另一种思路:

用户总表,含平台唯一uid;

每一个app,都注册登录过,会有对应的appuserid;

不同app通过一个和总表的关联表关联起来

1
2
3
4
5
本地用户唯一ID  社交网络类型  社交网络唯一ID
1112 weibo 20
5111 qq 20
6000 weixin 10
6000 qq 300

3. 目前第三方登录通常有两种设计思路

1、将第三方登录看成一种登录方式,使用第三方登录的前提是,你先要有一个账户。

缺点:当用户没有账户,直接使用第三方登录时,你只能提示他先去注册账户。但在用户看来,就是欺骗,容易引起用户的反感。

优点:当用户账户绑定了第三方账号后,用户就在也不用记住密码了,完全可以通过各个熟悉的第三方账号登录系统。

2、将第三方登录,看成一个独立的用户

优点:没有注册流程,容易吸引用户,由于是第三方直接登录,也不需要考虑用户会忘记密码

缺点:一个人容易产生多个账户,用户的行为容易分散到多个账户中,增加了系统的复杂性,某些情况下还会影响用户的产品体验。

通篇看下来,第三方登录并没有想象中的那么美好,第一种方式也就仅仅解决用户忘记密码的问题,注册时还会引起用户反感。第二种方式虽然能起到吸引用户的作用,但却增加了系统的复杂性,某些情况下还会影响用户的产品体验。

所以,大家可以根据自己的实际情况,考虑是否要添加第三方登录的支持,如果要,应该使用哪种第三方登录的模式。

4. user_auths表例子

1
2
3
4
5
6
7
8
9
user_auths


id # 自增id
user_id # users表对应的id
identity_type # 身份类型(站内username 邮箱email 手机mobile 或者第三方的qq weibo weixin等等)
identifier # 身份唯一标识(存储唯一标识,比如账号、邮箱、手机号、第三方获取的唯一标识等)
credential # 授权凭证(比如密码 第三方登录的token等)
verified # 是否已经验证(存储 1、0 来区分是否已经验证通过)
12…9
Wanglv Yihua

Wanglv Yihua

82 日志
20 分类
206 标签
RSS
© 2019 Wanglv Yihua
由 Hexo 强力驱动 v3.9.0
|
主题 – NexT.Muse v6.4.1