0%

SQL执行过程

  • 客户端 -> 连接器 -> 分析器 -> 优化器 -> 执行器
  • 连接器
  • 管理连接,权限验证,维持管理连接
  • 分析器
  • 词法分析,语法分析
  • 优化器
  • 执行计划生成,索引选择
  • 执行器
  • 操作引擎,返回结果
  • 存储引擎
  • 存储数据,提供读写接口
  • 连接器会优先查询缓存,如果命中则直接返回结果

连接器

  • 连接器负责跟客户端建立连接、获取权限、维持和管理连接。

查询缓存

  • 大多数情况下不要使用查询缓存
  • 查询缓存的失效非常频繁,只要有对一个表的更新,这个表上所有的查询缓存都会被清空。

分析器

  • 分析器先会做“词法分析”。

优化器

  • 优化器是在表里面有多个索引的时候,决定使用哪个索引
  • 在一个语句有多表关联(join)的时候,决定各个表的连接顺序。

执行器

  • 执行之前会进行权限校验
  • 根据表的引擎定义,使用引擎提供的接口
  • 获取记录集作为结果返回给客户端。

MySQL日志模块

redo log (重做日志)

  • WAL (Write-Ahead Logging)
  • 核心先写日志,在写磁盘
  • 保证及时数据库发生异常重启,之前提交的记录不会丢失(crash-safe)
  • InnoDB特有

binlog (归档日志)

对比

  • redo log 是 InnoDB 引擎特有的;binlog 是 MySQL 的 Server 层实现的,所有引擎都可以使用。
  • redo log 是物理日志,记录的是“在某个数据页上做了什么修改”;binlog 是逻辑日志,记录的是这个语句的原始逻辑,比如“给 ID=2 这一行的 c 字段加 1 ”。
  • redo log 是循环写的,空间固定会用完;binlog 是可以追加写入的。“追加写”是指 binlog 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。

两阶段提交

  • 引擎讲更新操作记录到redo log中,此时redo log处于prepare状态,同时告知执行器执行完成,可以提交事务。
  • 执行器生成该操作的binlog并写入磁盘。
  • 执行器调用引擎提交事务接口,引擎讲刚刚写入的redo log改为提交commit状态。
  • redo log的写入拆成了2个步骤prepare和commit,即两阶段提交。

先写rodo log 后写binlog问题

  • 写完rodo log 后,binlog未写完,重启后,主库可以通过redo log恢复,但是通过binlog恢复临时库会丢失该次更新。

先写binlog 后写redo log问题

  • 写完binlog后未写redo log,重启后由于redo log没写,即该次事务无效,而binlog中已经包含,则用binlog恢复会多出来一个事务。

  • 源:<极客时间> MySQL实战45讲教程

背景

  • 前两天的某个上午,正在开会ing。。突然收到了报警Es服务不可用,同时几个其他业务部门的人也都过来反馈说ES挂了。
  • 二话不说先启动ES恢复业务再说。然后就开始分析日志找问题了。

排查过程

  • 先是怀疑系统资源被用满了,看了一下zabbix,系统负载不太高,8C 16G机器load维持在5-10左右波动,应该不是这个问题。
  • 看了一下网络读取带宽,也没有达到什么高峰(时间是上午10点半左右),感觉系统方面应该不至于出问题。
  • 又怀疑是出现OOM内存不足,可是也没发现dump文件,然后就开始找ES日志看问题了
  • 看ES日志在down机之前有一个java.lang.StackOverflowError,应该就是这个原因了,之前还真没碰到过这个问题,将错误信息在google上面一查,有一些说是使用前缀或者正则查询导致的,感觉应该是这个问题,便开始抓取down机前1分钟的日志。
  • 运气不坏,很快就找到了一个高度怀疑的参数,是在搜索建议词中出现的,搜索建议词使用的es原生suggest + prefix,传过来的关键词是一个json数组去除了双引号和冒号(应该是api做的过滤),但是里面还有1600+字符,同时包含了“{}[]”符号,在测试机上面进行一下测试,把这个Query放进去,ES果然直接Down掉了,问题排查就算是完成了。

修复过程

  • 紧急发布了Hotfix(就是加入参数长度限制,特殊字符限制)
  • 后续准备考虑使用Ngram来解决这个问题
  • 吸取教训:
    • 能不用就尽可能不用通配符查询,无论是前缀还是模糊
    • 大不了空间换时间,暴力使用ngram解决问题
    • 参数一定要加入校验机制

原因分析(转)

  • 问题出现时,ES服务端日志有如下报错:

    1
    2
    3
    4
    5
    java.lang.StackOverflowError: null
    at org.apache.lucene.util.automaton.Operations.isFinite(Operations.java:1053) ~[lucene-core-6.2.1.jar:6.2.1 43ab70147eb494324a1410f7a9f16a896a59bc6f - shalin - 2016-09-15 05:15:20]
    at org.apache.lucene.util.automaton.Operations.isFinite(Operations.java:1053) ~[lucene-core-6.2.1.jar:6.2.1 43ab70147eb494324a1410f7a9f16a896a59bc6f - shalin - 2016-09-15 05:15:20]
    at org.apache.lucene.util.automaton.Operations.isFinite(Operations.java:1053) ~[lucene-core-6.2.1.jar:6.2.1 43ab70147eb494324a1410f7a9f16a896a59bc6f - shalin - 2016-09-15 05:15:20]
    at org.apache.lucene.util.automaton.Operations.isFinite(Operations.java:1053) ~[lucene-core-6.2.1.jar:6.2.1 43ab70147eb494324a1410f7a9f16a896a59bc6f - shalin - 2016-09-15 05:15:20]
  • Prefix/Regex/Fuzzy一类的Query,是直接构造的deterministic automaton,如果查询字符串过长,或者pattern本身过于复杂,构造出来的状态过多,之后一个isFinite的Lucene方法调用可能产生堆栈溢出。

  • PrefixQuery继承自Lucene的AutomatonQuery,在实例化的时候,maxDeterminizedStates传的是Integer.MAX_VALUE, 并且生成automaton之前,prefix的长度也没有做限制。

  • 附参考链接:

背景

  • 13号有一个大上线,其中一个功能就是排序功能,说复杂也不复杂,说难吧也不难,但是麻烦的就是加上Sort之后速度奇慢无比。
  • 排序实现也不太复杂,基本都是基于function score + sort 实现,对应不同的商品品类,有不同的排序规则(Hmm,目前还没有上个性化排序。。),实现方式就是定位品类之后,用function score进行提升部分商品的score,在这之后用sort进行【score + field1 +field2】的排序,看着也不复杂对吧。
  • ES机器配置不太高(qiong…..)4C 8G * 3,索引中在售商品也不太多几,万的样子,分布到200多个城市站,总共合起来大概千万出头的数据(有部分非在售商品)。

问题

  • 每次定位到品类之后,使用了品类排序【score + field1 +field2】,速度奇慢无比,平时几十毫秒的查询能变成5-6秒甚至更多,有点让人无法忍受

尝试过程

  • 最开始打算用缓存解决问题,但是更新频率实在有点高的不行。。这个有点扛不住。
  • 后来打算按照城市站分索引,这样一个索引数据量就很小了,排序应该就不是什么大问题了,但是查询有点麻烦。
  • 先reindex一个小的索引,试了试排序速度,果然恢复到了几十毫秒的数量级了。
    突然想到了也许多加一些分片也可以解决这个问题,毕竟我们是有城市站routing的啊。
  • 不过城市站反正是查询的必须条件已经加上了routing,吧shard从默认的5个先加到了50个,试了一下果然速度提升到100ms左右,再往上加shard的时候发现了另外一个问题,大批量的出现ESReject问题,可能是一次bulk或者index数据的量略大,ES又需要进行routing的原因吧,最终上线的时候,保守一点使用了40 Shard,目前速度尚在可接受范围(线上最起码是8c 16G的。。),如果想在提升速度,估计要加机器了(还是那句话。。。qiong。。。)

加速原理

  • 没看源码,纯属自己猜测:
    • 感觉shard和分索引应该差不多,Es Query需要找到对应的shard,拽出来符合条件的数据进行排序,然后输出出去。拽出来数据应该是在shard内部进行过排序,如果shard内部文档过多,速度应该会比较慢。
    • 我们这边业务场景是城市站必须有,而且文档按照城市站分开后相对来说很平均了(当然还是有部分shard很小。。),将shard数量提高以后,shard内部排序的文档数量少了很多,速度自然就上去了。
    • shard过多,在ES进行bulk插入数据时候,routing到对应分片,这步骤应该会花较多时间,如果机器配置不太给力的话(囧。。比如说我们),shard数量还是不要太多,否则机器负载会很高很高(前两天看到负载破20了 - -!还好只是瞬时)

  • Hmm..就是在leetcode上面做题碰到了一个题目,让用SQL实现根据分值排序,原题描述如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    编写一个 SQL 查询来实现分数排名。如果两个分数相同,则两个分数排名(Rank)相同。请注意,平分后的下一个名次应该是下一个连续的整数值。换句话说,名次之间不应该有“间隔”。

    +----+-------+
    | Id | Score |
    +----+-------+
    | 1 | 3.50 |
    | 2 | 3.65 |
    | 3 | 4.00 |
    | 4 | 3.85 |
    | 5 | 4.00 |
    | 6 | 3.65 |
    +----+-------+
    例如,根据上述给定的 Scores 表,你的查询应该返回(按分数从高到低排列):

    +-------+------+
    | Score | Rank |
    +-------+------+
    | 4.00 | 1 |
    | 4.00 | 1 |
    | 3.85 | 2 |
    | 3.65 | 3 |
    | 3.65 | 3 |
    | 3.50 | 4 |
    +-------+------+
  • 生产中应该不会遇到这种问题吧。。有的话应该也是新出表之类的解决,不过想了想,这个题还真么啥解决思路(自己引用自己还是内外层的。。在这之前真不会TAT)

  • 结合别人的答案,总算是弄明白了这个SQL是怎么写了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
SELECT
Score,
(
SELECT
count( DISTINCT score )
FROM
Scores
WHERE
score >= s.score
) AS Rank
FROM
Scores s
ORDER BY
Score DESC;
  • 外层其实很好理解,查询score表中Score和Rank,倒序一下。

  • 内层就是把外层每条的Score拿到(语句中的s.score),然后统计一下不小于s.score的值(去重),这个结果也就是对应的RanK了。

  • 换个条件。。如果两个分数相同,则两个分数排名(Rank)相同,名次之间“间隔”,也可以用这个思路来解决。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
SELECT
Score,
(
SELECT
count( score ) + 1
FROM
Scores
WHERE
score > s.score
) AS Rank
FROM
Scores s
ORDER BY
Score DESC;


  • 今天将公司之前的shell脚本处理一下,放到Jenkins中执行,方便大家部署,结果出现了一个奇怪的问题,通过ssh 远程执行命令的时候发现找不到java命令。
  • 直接用ssh切换到那台机器是没有问题的,java命令存在。远程执行 ssh -t user@host ‘java -version’ 提示java命令找不到。
  • 查了一堆资料,定位了问题:
    • 使用这种方式执行命令,不会执行/etc/profile文件,而我的java_home,java path都是在/etc/profile文件中配置的
  • 解决也不太复杂:
    • 方法1:ssh -t user@host ‘source /etc/profile && java -version’ 久违的jdk1.8终于出来了(嗯。。我就用这个解决的)
    • 方法2: 修改 ~/.bashrc 加入java_home,java path (这个没敢动。。因为机器是公用的。。。)
  • 补充知识点:
  1. 通过SSH登录后再执行命令和脚本
    这种方式会使用Bash的interactive + login shell模式,这里面有两个概念需要解释:interactive和login。

    • login故名思义,即登陆,login shell是指用户以非图形化界面或者以ssh登陆到机器上时获得的第一个shell,简单些说就是需要输入用户名和密码的shell。因此通常不管以何种方式登陆机器后用户获得的第一个shell就是login shell。
    • interactive意为交互式,这也很好理解,interactive shell会有一个输入提示符,并且它的标准输入、输出和错误输出都会显示在控制台上。所以一般来说只要是需要用户交互的,即一个命令一个命令的输入的shell都是interactive shell。而如果无需用户交互,它便是non-interactive shell。通常来说如bash script.sh此类执行脚本的命令就会启动一个non-interactive shell,它不需要与用户进行交互,执行完后它便会退出创建的Shell。
    • 在interactive + login shell模式中,Shell首先会加载/etc/profile文件,然后再尝试依次去加载下列三个配置文件之一,一旦找到其中一个便不再接着寻找:
    1
    2
    3
    ~/.bash_profile
    ~/.bash_login
    ~/.profile
  2. 通过SSH直接执行远程命令和脚本
    这种方式会使用Bash的non-interactive + non-login shell模式,它会创建一个shell,执行完脚本之后便退出,不再需要与用户交互。

    • no-login shell,顾名思义就是不是在登录Linux系统时启动的(比如你在命令行提示符上输入bash启动)。它不会去执行/etc/profile文件,而会去用户的HOME目录检查.bashrc并加载。
    • 系统执行Shell脚本的时候,就是属于这种non-interactive shell。Bash通过BASH_ENV环境变量来记录要加载的文件,默认情况下这个环境变量并没有设置。如果有指定文件,那么Shell会先去加载这个文件里面的内容,然后再开始执行Shell脚本。

引用: https://www.cnblogs.com/zhenyuyaodidiao/p/9287497.html


  • 上周五用python2实现了一个简易的canal监控报警脚本(主要就是检测时间戳,超时就进行邮件通知),脚本不太复杂,上传到线上服务器之后,直接运行一切正常,模拟了一下错误数据,也正常发出了邮件通知。然后就配置了一下crontab定时任务,每5分钟执行一次检测,本来以为万事大吉,谁知道部署之后,日志什么的都正常更新了,唯独就是邮件没有发送出去。
  • 问题排查:
    • 本地运行正常,服务器直接通过python monitor.py 执行,也正常。唯独就是通过crontab执行不正常,只是记录了日志,没有进行邮件通知。
    • 开始怀疑是程序中引用路径有问题,crontab执行命令不是在monitor脚本目录执行,获取sys.args[0]路径可能有问题,全都替换成绝对路径,问题依旧。
    • 无解,谷歌百度一番之后,有人说执行shell脚本,要使用/bin/bash /path/shell.sh,这样才能正常运行,那我这个估计也是这个原因。修改crontab命令使用: /usr/bin/python /data/monitor.py 重于收到了久违的邮件,至此问题解决。
  • 原因:
    • 初步怀疑直接运行python /data/monitor.py 可能会使用其他版本的python(python3),我的脚本使用python2写的(已知账户均有python2,没有python3环境,懒得找运维-0-),其中用到了print xxx的语法,由于我没有root权限,这个暂时不进行验证了。
  • 总结:
    • 使用crontab所有命令,执行器都要使用绝对路径,免得引起不必要的麻烦-0-!

Swagger使用简介

Swagger简介

没有API文档工具之前,大家都是手写API文档的,在什么地方书写的都有,有在confluence上写的,有在对应的项目目录下readme.md上写的。这种方式不是说不好,大家都有一个通病,就是懒得更新文档,隔了一段时间,接口变动了什么没人清楚了。另外还有就是开始写文档的时候特别痛苦,每个字段,一行行注释解释。
其实大多数开发都还是写注释的(为了防止自己看不懂吧。。),如果稍加修改,可以从注释中自动生成文档就好了。Hmm..Swagger可以完成这个功能,尤其是针对Java,C#这样的项目,而且是前后端分离的,更加合适了。
Swagger能做什么呢?可以自动根据Controller自动生成对应的文档,并且提供测试接口。如果你能容忍一定程度的代码侵入(也不是太多。。就是在需要暴露的Model和Controller上加点注解,一个配置文件类),Swagger还是很方便的。

Swagger 和 Spring 项目整合

其实很简单,加个依赖,加个配置类,嗯。。这个应该是最低工作保证了。。

先说依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- swagger2 生成对应的Json文档,这个应该可以说是核心依赖了 -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.6.1</version>
</dependency>
<!-- swagger-ui 为项目提供api展示及测试的界面 -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.6.1</version>
</dependency>
<!-- 集成 swagger 的时候,缺少这个 jar包是不OK的-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.8.6</version>
</dependency>

说真的,依赖包不是太多springfox-swagger2是生成接口访问JSON和核心,同时依赖jackson。界面展示依赖的是swagger-ui。嗯。。引入的时候注意下Jar包冲突。。尤其是Spring的版本不同exclusion一下。。

依赖包这样基本就已经满足了,剩下就是加一个配置类就好了。如下:

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


import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.ParameterBuilder;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.schema.ModelRef;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Parameter;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import java.util.ArrayList;
import java.util.List;


@Configuration
@EnableSwagger2
public class ApiConfig {
@Bean(name = "docket")
public Docket api() {
ParameterBuilder ticketPar = new ParameterBuilder();
List<Parameter> pars = new ArrayList<>();
ticketPar.name("login-token").description("登录Token")
.modelRef(new ModelRef("string")).parameterType("header")
//header中的ticket参数非必填,传空也可以
.required(false).build();
//根据每个方法名也知道当前方法在设置什么参数
pars.add(ticketPar.build());
Docket docket = new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.any())
.build().globalOperationParameters(pars);
docket.apiInfo(apiInfo());
return docket;
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("后台接口")
.description("后台接口")
.version("1.0.0")
.build();
}
}

如果项目使用Spring是XML方式,就在配置文件加上如下几行:

1
2
3
<mvc:default-servlet-handler/>
<mvc:resources mapping="/webjars/**" location="classpath:/META-INF/resources/webjars/" />
<mvc:resources mapping="swagger-ui.html" location="classpath:/META-INF/resources/" />

如果是SpringBoot的就更简单了。。直接拷贝配置类,加上依赖包就可以运行了。
Hmm。。简单来说这样就足够了。。。
访问地址是默认地址+/swagger-ui.html,我这里是http://localhost:8080/swagger-ui.html
效果如图所示:

稍微复杂一些

上面的只是提供了基础功能,Hmm…还是没有注释说明什么的,只是有一些基础的序列化参数(Json字段还是可以自动识别的,而且能提供默认值)
如果要加入一些注释,就需要对原有项目加入一些侵入代码了(一些swagger特有注解)

首先是Model实体上面的

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
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
@ApiModel
public class AddKeywordRequestVo extends BaseVo {
/**
* 关键词名称
*/
@ApiModelProperty(value = "关键词名称",example = "汽车")
private String keywordName;
/**
*关键词品类,多个用逗号分隔,0全部,1灯泡,2火花塞,3轮胎,4蓄电池,5油品,6雨刮,7全车件。默认为0
*/
@ApiModelProperty(value = "关键词品类,多个用逗号分隔,0全部,1灯泡,2火花塞,3轮胎,4蓄电池,5油品,6雨刮,7全车件。默认为0",example = "1")
private String keywordCategory;
/**
* 关键词拼音
*/
@ApiModelProperty(value = "关键词拼音",example = "qiche")
private String keywordPinyin;
/**
* 关键词同义词
*/
@ApiModelProperty(value = "关键词同义词",example = "卡车")
private String keywordSynonym;
/**
* 词频
*/
@ApiModelProperty(value = "词频",example = "1")
private Integer wordFrequency;
/**
* 词性,0通用,1专属
*/
@ApiModelProperty(value = "词性,0通用,1专属",example = "1")
private Integer wordType;
/**
* 备注
*/
@ApiModelProperty("备注")
private String remark;
public String getKeywordName() {
return keywordName;
}

public void setKeywordName(String keywordName) {
this.keywordName = keywordName;
}

public String getKeywordCategory() {
return keywordCategory;
}

public void setKeywordCategory(String keywordCategory) {
this.keywordCategory = keywordCategory;
}

public String getKeywordPinyin() {
return keywordPinyin;
}

public void setKeywordPinyin(String keywordPinyin) {
this.keywordPinyin = keywordPinyin;
}

public String getKeywordSynonym() {
return keywordSynonym;
}

public void setKeywordSynonym(String keywordSynonym) {
this.keywordSynonym = keywordSynonym;
}

public Integer getWordFrequency() {
return wordFrequency;
}

public void setWordFrequency(Integer wordFrequency) {
this.wordFrequency = wordFrequency;
}

public Integer getWordType() {
return wordType;
}

public void setWordType(Integer wordType) {
this.wordType = wordType;
}

public String getRemark() {
return remark;
}

public void setRemark(String remark) {
this.remark = remark;
}

public AddKeywordRequestVo() {}

public AddKeywordRequestVo(Integer wordFrequency, Integer wordType, String keywordName, String keywordCategory) {
this.wordFrequency = wordFrequency;
this.wordType = wordType;
this.keywordName = keywordName;
this.keywordCategory = keywordCategory;
}
}

其实也不是太多,一个ApiModelProperty,一个ApiModel就够用了,加上上面的注解有什么效果呢?看图吧。。

model的:

参数示例的:

其实Response也是可以的,但是需要为泛型或者具体的,Object类型的就。。。。参考我这边的。。。
如果是泛型的话,参考下图:
model的:

参数示例的:

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

/**
* 关键词Controller
*/
@RestController
@Api("关键词Controller")
public class KeywordsController {
@Autowired
private AddKeyWordService addKeyWordService;
@Autowired
private DelKeyWordService delKeyWordService;
@Autowired
private BatchDelKeyWordService batchDelKeyWordService;

private static Logger logger = LoggerFactory.getLogger(KeyWordController.class);

/**
* 添加关键词
* @param vo 添加关键词请求参数
* @return 添加关键词结果
*/
@RequestMapping(value = "/add/keyword",method = RequestMethod.POST)
@ResponseBody
@ApiOperation("添加关键词")
public Result addKeyword(@RequestBody AddKeywordRequestVo vo){
LogModel lm = LogModel.newLogModel("addKeyword");
logger.info(lm.addMetaData(vo).toJson());
Result res = new Result();
if (!addKeyWordService.checkParam(vo,res)){
return res;
}
try{
addKeyWordService.addKeyword(vo,res);
}catch(Exception e){
res.setStatus(ReturnStatusEnum.SERVICE_ERROR.getValue());
res.setMessage(ReturnStatusEnum.SERVICE_ERROR.getDesc());
}
logger.info(lm.getMeta("status", res.getStatus()).getMeta("meg", res.getMessage()).toJson());
return res;
}

/**
* 删除关键词
* @return 删除关键词结果
*/
@RequestMapping(value = "/del/keyword",method = RequestMethod.DELETE)
@ResponseBody
@ApiOperation("删除关键词")
public Result delKeyword(@RequestBody DelKeywordRequestVo vo){
LogModel lm = LogModel.newLogModel("delKeyword");
logger.info(lm.addMetaData(vo).toJson());
Result res = new Result();
if (!delKeyWordService.checkParam(vo,res)){
return res;
}
try {
delKeyWordService.delKeyword(vo,res);
} catch (Exception e) {
res.setStatus(ReturnStatusEnum.SERVICE_ERROR.getValue());
res.setMessage(ReturnStatusEnum.SERVICE_ERROR.getDesc());
}
logger.info(lm.getMeta("status", res.getStatus()).getMeta("meg", res.getMessage()).toJson());
return res;
}

/**
* 批量删除关键词接口
* @param vo 批量删除关键词请求
* @return 批量删除结果
*/
@ApiOperation("批量删除关键词接口")
@RequestMapping(value = "/batchdel/keyword",method = RequestMethod.DELETE)
@ResponseBody
public Result delKeywords(@RequestBody DelKeywordsRequestVo vo){
LogModel lm = LogModel.newLogModel("delKeywords");
logger.info(lm.addMetaData(vo).toJson());
Result res = new Result();
if (!batchDelKeyWordService.checkParam(vo,res)){
return res;
}
try {
batchDelKeyWordService.delKeywords(vo,res);
} catch (Exception e) {
res.setStatus(ReturnStatusEnum.SERVICE_ERROR.getValue());
res.setMessage(ReturnStatusEnum.SERVICE_ERROR.getDesc());
}
logger.info(lm.getMeta("status", res.getStatus()).getMeta("meg", res.getMessage()).toJson());
return res;
}
}

效果如图所示:

还有更强大的

你以为这就是全部? nonono swagger还可以直接访问接口,Hmm。。这应该是最方便的吧

直接输入对应的参数,点击Try it out! 如图所示:

是不是有了这个连Postman都不用了=-=~

总结

好处

  • 文档跟随项目接口实时改变,不用担心文档和接口不同步
  • 一大批懒开发(比如我)还是写一些注释的,加入注释同时加个注解,也不算太麻烦。
  • 对接流畅多了,前端调用也方便多了。
  • 自己测试也方便

坏处

  • 有一定代码侵入(加入注解,依赖,配置文件等)
  • 放到线上一定一定要屏蔽掉/swagger相关路径(通过Nginx屏蔽掉)
  • 变动更频繁了。。
  • 人更懒了。。

Redis 学习汇总

  • 近期看书看博客看视频,相对较为系统的学习了一下Redis,不过版本还是比较老,主要还是3.X系列的。虽然目前最新的已经是4.X,不过老版本的基本都还兼容。

Redis 编译安装

  • Redis安装有很多种方式,Centos可以快捷的使用yum安装,Ubuntu也可以找到apt源进行安装,我这为了尝鲜,用的的是编译安装,其实蛮简单的,我这里直接在mac下进行编译安装,步骤如下:
  • 首先下载redis源码包 :wget http://download.redis.io/releases/redis-4.0.11.tar.gz
  • 解压 tar -zxvf redis-4.0.11.tar.gz
  • 进入redis-4.0.11目录 cd redis-4.0.11
  • 执行make命令编译(好像需要command tools,之前安装过,没太注意)
  • 然后执行make install 进行安装
  • 然后就可以运行了(mac下直接发送到bin目录下面了,不需要额外配置= =!),执行redis-server 就可以看到熟悉的姐妹了

Redis 简易配置

  • 开始还是单机运行吧,创建一个conf文件,我这里叫做 redis-6379.conf ,内容如下:
1
2
3
4
5
6
7
8
bind 127.0.0.1 #绑定Ip
port 6379 #暴露端口
daemonize yes #后台启动
pidfile /Users/eviltuzki/Public/redis/redis_6379.pid
logfile "/Users/eviltuzki/Public/redis/6379.log"
databases 16
dbfilename dump.rdb
dir /Users/eviltuzki/Public/redis #工作目录
  • 配置都比较简单,就不过多解释了,主要就是一些工作目录之类的内容
  • 配置完成后通过 redis-server redis-6379.conf 启动redis,可以通过redis-cli检查是否启动成功

Redis 基本数据类型

  • 新版本增加了若干新的数据类型,我暂时没有使用需求,没做过多研究,主要还是针对常用的5中数据结构

Strings

  • 这应该是Redis中使用最多最多的数据结构了,使用起来也很简单,直接set key value 进行赋值,get key进行取值
  • 常用的应用场景(好吧,我只是说一下我经常用的场景吧,在使用Token进行登录验证的时候,token存储于Redis中,使用的就是这种结构,设置好过期时间,定期刷新,可以理解为模拟Session吧)
  • 列举一些常用API
API 解释 使用示例
set 设置指定 key 的值 set key value
mset 同时设置一个或多个 key-value 对 mset key value [key1 value1 …]
get 获取指定 key 的值 get key
mget 获取所有(一个或多个)给定 key 的值 mget key1 [key2 …]
strlen 返回 key 所储存的字符串值的长度 strlen key
incr 将 key 中储存的数字值增一。 incr key
incrby 将 key 所储存的值加上给定的增量值(increment) incrby key increment
decr 将 key 中储存的数字值减一 decr key
decrby key 所储存的值减去给定的减量值(decrement) decrby key increment
append 如果 key 已经存在并且是一个字符串, APPEND 命令将指定的 value 追加到该 key 原来值(value)的末尾 append key value
getset 将给定 key 的值设为 value ,并返回 key 的旧值(old value) getset key value
expire 设置指定key的过期时间(time) expire key time

Hash

  • Hash结构可以认为是一个微型Redis(如果把Redis简单认为是Strings类型),换成Java语言来说,Hash结构就是 Map<String,Map<String,String>>
  • 应用场景。。。。Hmm。。项目中没有用到,不过觉得如果加入用户角色权限等信息。。。。是不是session可以用这个来处理呢?或者是application。。。。Hmm。。。暂时没有想法
  • 还是列举一些常用API
API 解释 使用示例
hget 获取存储在哈希表中指定字段的值。 HGET key field
hset 将哈希表 key 中的字段 field 的值设为 value 。 HSET key field value
hmget 获取所有给定字段的值 HMGET key field1 [field2]
hmset 同时将多个 field-value (域-值)对设置到哈希表 key 中。 HMSET key field1 value1 [field2 value2 ]
hgetall 获取在哈希表中指定 key 的所有字段和值 HGETALL key
hscan 迭代哈希表中的键值对。 HSCAN key cursor [MATCH pattern] [COUNT count] ``
hexist 查看哈希表 key 中,指定的字段是否存在。 HEXISTS key field
hdel 删除一个或多个哈希表字段 HDEL key field1 [field2]
hincrby 为哈希表 key 中的指定字段的整数值加上增量 increment 。 HINCRBY key field increment
hkeys 获取所有哈希表中的字段 HKEYS key
hlen 获取哈希表中字段的数量 HLEN key
hvals 获取哈希表中所有值 HVALS key

List

  • 列表虽然最近项目中没有使用,不过之前的项目中大规模使用,场景是。。。把list当做消息队列了。。
  • 应用场景。。。除了消息队列。。Hmm。。我也想不到什么了。。。如果有其他场景,烦请告诉我,谢谢。。
  • 老规矩,列举一些常用API
API 解释 使用示例
blpop 移出并获取列表的第一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。 BLPOP key1 [key2] timeout
brpop 移出并获取列表的最后一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。 BRPOP key1 [key2] timeout
brpoplpush 从列表中弹出一个值,将弹出的元素插入到另外一个列表中并返回它; 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。 BRPOPLPUSH source destination timeout
lindex 通过索引获取列表中的元素 LINDEX key index
linsert 在列表的元素前或者后插入元素 LINSERT key BEFORE\AFTER pivot value
llen 获取列表长度 LLEN key
lpop 移出并获取列表的第一个元素 LPOP key
lpush 将一个或多个值插入到列表头部 LPUSH key value1 [value2]
lpushx 将一个值插入到已存在的列表头部 LPUSHX key value
lrange 获取列表指定范围内的元素 LRANGE key start stop
lrem 移除列表元素 LREM key count value
lset 通过索引设置列表元素的值 LSET key index value
ltrim 对一个列表进行修剪(trim),就是说,让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除。 LTRIM key start stop
rpop 移除并获取列表最后一个元素 RPOP key
rpoplpush 移除列表的最后一个元素,并将该元素添加到另一个列表并返回 RPOPLPUSH source destination
rpush 在列表中添加一个或多个值 RPUSH key value1 [value2]
rpushx 为已存在的列表添加值 RPUSHX key value

Set

  • Set集合,就知道这个是一个集合,无序,不可以重复(Hmm和java中的Set很相似)
  • 应用场景。。。没想到。。。Hmm。。待补充
  • 常用API
API 解释 使用示例
sadd 向集合添加一个或多个成员 SADD key member1 [member2]
scard 获取集合的成员数 SCARD key
sdiff 返回给定所有集合的差集 SDIFF key1 [key2]
sdiffstore 返回给定所有集合的差集并存储在 destination 中 SDIFFSTORE destination key1 [key2]
sinter 返回给定所有集合的交集 SINTER key1 [key2]
sinterstore 返回给定所有集合的交集并存储在 destination 中 SINTERSTORE destination key1 [key2]
sismember 判断 member 元素是否是集合 key 的成员 SISMEMBER key member
smembers 返回集合中的所有成员 SMEMBERS key
smove 将 member 元素从 source 集合移动到 destination 集合 SMOVE source destination member
spop 移除并返回集合中的一个随机元素 SPOP key
srandmember 返回集合中一个或多个随机数 SRANDMEMBER key [count]
srem 移除集合中一个或多个成员 SREM key member1 [member2]
sunion 返回所有给定集合的并集 SUNION key1 [key2]
sunionstore 所有给定集合的并集存储在 destination 集合中 SUNIONSTORE destination key1 [key2]
sscan 迭代集合中的元素 SSCAN key cursor [MATCH pattern] [COUNT count]

Zset

  • Hmm Zset 也叫做Sorted Set 就是一个排序的集合,简单的说就是Set的有序版本(不过这个和Java的SortedSet不太一样。。),区别是什么呢,区别就是每个元素都有一个Score,排序的依据呢就是这个Score了。。
  • 应用场景,项目中倒是用到了,不过感觉用到并不是太对。。。所以不说了。老项目使用这个实现了一个排行榜,Hmm还是可以的,定期刷入到MySQL中持久化,也不怕数据丢失什么的。。。挺好
  • 说一下常用API吧
API 解释 使用示例
zadd 向有序集合添加一个或多个成员,或者更新已存在成员的分数 ZADD key score1 member1 [score2 member2]
zcard 获取有序集合的成员数 ZCARD key
zcount 计算在有序集合中指定区间分数的成员数 ZCOUNT key min max
zincrby 有序集合中对指定成员的分数加上增量 increment ZINCRBY key increment member
zinterstore 计算给定的一个或多个有序集的交集并将结果集存储在新的有序集合 key 中 ZINTERSTORE destination numkeys key [key …]
zlexcount 在有序集合中计算指定字典区间内成员数量 ZLEXCOUNT key min max
zrange 通过索引区间返回有序集合成指定区间内的成员 ZRANGE key start stop [WITHSCORES]
zrangebylex 通过字典区间返回有序集合的成员 ZRANGEBYLEX key min max [LIMIT offset count]
zrangebyscore 通过分数返回有序集合指定区间内的成员 ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT]
zrank 返回有序集合中指定成员的索引 ZRANK key member
zrem 移除有序集合中的一个或多个成员 ZREM key member [member …]
zremrangebylex 移除有序集合中给定的字典区间的所有成员 ZREMRANGEBYLEX key min max
zremrangebyrank 移除有序集合中给定的排名区间的所有成员 ZREMRANGEBYRANK key start stop
zremrangebyscore 移除有序集合中给定的分数区间的所有成员 ZREMRANGEBYSCORE key min max
zrevrange 返回有序集中指定区间内的成员,通过索引,分数从高到底 ZREVRANGE key start stop [WITHSCORES]
zrevrangebyscore 返回有序集中指定分数区间内的成员,分数从高到低排序 ZREVRANGEBYSCORE key max min [WITHSCORES]
zrevrank 返回有序集合中指定成员的排名,有序集成员按分数值递减(从大到小)排序 ZREVRANK key member
zscore 返回有序集中,成员的分数值 ZSCORE key member
zunionstore 计算给定的一个或多个有序集的并集,并存储在新的 key 中 ZUNIONSTORE destination numkeys key [key …]
zscan 迭代有序集合中的元素(包括元素成员和元素分值) ZSCAN key cursor [MATCH pattern] [COUNT count]

Redis 读写分离结构

  • 读写分离其实不是太复杂,简单来说就是多个Redis组成小”集群”,注意我这里的集群是带有引号的哈,这并不是一个真正意义上的集群,而是主从结构(Master&Slave),可以是一主多从,也可以是一主一从,甚至可以使只有一个Master(这个就退化成了。。。单机模式了。。)
  • 说一下怎么配置吧
    • 先参考 Redis 简易配置,并启动6379节点

    • 拷贝一份配置文件,将所有的6379替换为6380 (Hmm,我比较懒。。就单机先这么搞了。。)

    • 启动6380这个实例 redis-server redis-6380.conf

    • 查看是否都启动成功了,执行 ps -ef |grep redis-server|grep -v ‘grep’ 我这里显示如下,表示两个实例已经启动成功

      1
      2
      501 42485     1   0 12:29下午 ??         0:07.18 redis-server 127.0.0.1:6379
      501 43688 1 0 10:20下午 ?? 0:00.33 redis-server 127.0.0.1:6380
    • 执行 redis-cli -p 6380 info replication 看到6380实例目前是以Master角色运行

1
2
3
4
5
6
7
8
9
10
11
# Replication
role:master
connected_slaves:0
master_replid:13f6fcea2807ec7a78d52ecf76c382c0ba7d9c55
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0
- 给6380分配角色slave,跟从Master 执行 redis-cli -p 6380  slaveof 127.0.0.1 6379
- 执行 redis-cli -p 6380 info replication 看到6380实例目前是以Slave角色运行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Replication
role:slave
master_host:127.0.0.1
master_port:6379
master_link_status:up
master_last_io_seconds_ago:8
master_sync_in_progress:0
slave_repl_offset:154
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:7a2c11e996810f76bdc72765130dcf47b5af4ab8
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:154
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:71
repl_backlog_histlen:84
- 这种是通过Redis Client来分配角色,还可以在配置文件中进行配置,修改redis-6380.conf如下,并重启:
1
2
3
4
5
6
7
8
9
bind 127.0.0.1
port 6380
daemonize yes
pidfile /Users/eviltuzki/Public/redis/redis_6380.pid
logfile "/Users/eviltuzki/Public/redis/6380.log"
databases 16
dbfilename dump.rdb
dir /Users/eviltuzki/Public/redis
slaveof 127.0.0.1 6379
  • 实际生产环境通常使用配置文件的方式配置主从结构
  • 完成了配置简单尝试一下:
    • 在Master set value -> redis-cli -p 6379 set hello world -> OK
    • 在Slave get value -> redis-cli -p 6380 get hello -> “world”
    • 在Slave set value -> redis-cli -p 6380 set test test -> (error) READONLY You can’t write against a read only slave.
  • 基本测试完成,主节点可读可写,从节点同步主节点,只能读取,不能写入,Hmm。。。如果要多加入几个Slave节点。。Copy一下配置文件就好了。。。

Redis HA 之 sentinel

说一下背景

  • Hmm 上文说道了集群,其实主从结构并不是一个可靠的集群,比如某天一大波僵尸来袭。。。跑题了,一大波流量来袭。。。Master挂了。。。然后。。。Hmm。。。没有节点可以写入了,咋办呢?
  • 解决方法也不是太复杂,举个场景:3台机器,1Master 2Slave,然后某天。。。Master突然挂了。。剩下2个Slave,这个时候咋办?切换一下,让其中一个Slave变成Master,另外一个跟随这个新的Master,这样就1主1从1挂机(鄙视挂机党。。。)。好赖可以正常提供服务了,等挂机服务器启动起来了,将它设置为新Master的Slave节点,这样就完成了Master Slave的转换了,然后就可以正常提供服务了。
  • 听起来上面的方案还不错,其实服务端执行起来也不太复杂。如下:
    • Master挂了,剩下2个Slave,记为Slave1 Slave2
    • 对Slave 1 执行 slaveof no one,将Slave 1 升级为新Master
    • 对Slave 2 执行 slaveof Slave1,将Slave 2设置跟从新的Master
    • 等原Master启动,执行slaveof Slave1,将原Master设置为Slave并且跟从新的Master
  • 为啥说服务端简单呢?Client连接服务器也得跟着切换啊。。Client写入只能写入到Master节点,服务端经过这么一折腾,Client也要跟着切换IP,才能正常访问。
  • 所以呢,官方提供了sentinel 一种HA方案,服务端的切换可以自动执行,sentinel 节点负责监控Master Slave状态,如果切换了,同时通知Client进行切换,达到可服务状态。

怎么配置?

  • 首先要额外准备机器作为sentinel节点,我这里偷懒,继续单机运行。。。(Hmm,穷人。。没有太多机器。。也不想搞虚拟机)

  • 先启动一个Maste 6379,2个Slave 6380 8381,执行ps -ef |grep redis|grep -v grep 如下:

    1
    2
    3
    501 42485     1   0 12:29下午 ??         0:10.87 redis-server 127.0.0.1:6379
    501 43910 1 0 10:47下午 ?? 0:02.45 redis-server 127.0.0.1:6380
    501 44117 1 0 11:17下午 ?? 0:00.53 redis-server 127.0.0.1:6381
  • 查看Master状态 redis-cli -p 6379 info replication

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    # Replication
    role:master
    connected_slaves:2
    slave0:ip=127.0.0.1,port=6380,state=online,offset=3796,lag=0
    slave1:ip=127.0.0.1,port=6381,state=online,offset=3796,lag=0
    master_replid:7a2c11e996810f76bdc72765130dcf47b5af4ab8
    master_replid2:0000000000000000000000000000000000000000
    master_repl_offset:3796
    second_repl_offset:-1
    repl_backlog_active:1
    repl_backlog_size:1048576
    repl_backlog_first_byte_offset:1
    repl_backlog_histlen:3796
  • 全部启动成功,然后开始配置sentinel节点

  • 从Redis解压文件中可以看到一个sentinel.conf文件,Hmm。。。懒人。。直接Copy这个开始改造,sentinel-26379.conf 如下:

    1
    2
    3
    4
    5
    6
    7
    8
    port 26379
    dir /Users/eviltuzki/Public/redis/
    sentinel monitor mymaster 127.0.0.1 6379 2 # 监控mymaster集群,Master地址为127.0.0.1 6379,当2个sentinel认为Master有问题,则进行Master转换
    sentinel down-after-milliseconds mymaster 30000 #下线Master时间30s
    sentinel parallel-syncs mymaster 1
    sentinel failover-timeout mymaster 180000
    sentinel deny-scripts-reconfig yes
    daemonize yes #守护进程方式启动
  • 生成对应的3份,分别是sentinel-26379.conf、sentinel-26380.conf、sentinel-26381.conf,然后通过redis-sentinel sentinel-263xx.conf启动sentinel节点

  • 查看进程,是否启动成功:ps -ef |grep sent|grep -v grep

    1
    2
    3
    501 44288     1   0 11:36下午 ??         0:00.64 redis-sentinel *:26379 [sentinel]
    501 44291 1 0 11:36下午 ?? 0:00.61 redis-sentinel *:26380 [sentinel]
    501 44293 1 0 11:36下午 ?? 0:00.63 redis-sentinel *:26381 [sentinel]
  • 查看redis-sentinel状态:redis-cli -p 26379 info sentinel

    1
    2
    3
    4
    5
    6
    7
    # Sentinel
    sentinel_masters:1
    sentinel_tilt:0
    sentinel_running_scripts:0
    sentinel_scripts_queue_length:0
    sentinel_simulate_failure_flags:0
    master0:name=mymaster,status=ok,address=127.0.0.1:6379,slaves=2,sentinels=3
  • 可以看到master0状态OK,2个Slave,3个sentinels,一切正常。接下来就可以使用对应的Client进行连接了。

  • Hmm 这块内容有点太多了。。。后面单开Java Client连接。。

  • 接下来模拟一下事故吧:

    • 查看一下刚刚配置的sentinel-26379.conf文件,多了一些内容:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      port 26379
      dir "/Users/zhaojian/Public/redis"
      sentinel myid bca8111060bbda4a42ec3744391d1f40ca1fda00
      sentinel deny-scripts-reconfig yes
      sentinel monitor mymaster 127.0.0.1 6379 2
      sentinel config-epoch mymaster 0
      sentinel leader-epoch mymaster 0
      # Generated by CONFIG REWRITE
      sentinel known-slave mymaster 127.0.0.1 6381
      sentinel known-slave mymaster 127.0.0.1 6380
      sentinel known-sentinel mymaster 127.0.0.1 26380 ba913a9c10e46fd4df76bf0289721136031926ef
      daemonize yes
      sentinel known-sentinel mymaster 127.0.0.1 26381 7c387d9b2e95ac2338655fb8f5011f277635dade
      sentinel current-epoch 0
    • 主节点和从节点信息都能看到,现在我准备下线Master节点,看看会有什么反应(Hmm怎么下线呢?直接kill掉吧)

    • 等待一小会儿,Hmm,大概30S左右吧,再次查看sentinel-26379.conf文件,发现有些变化了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
port 26379
dir "/Users/zhaojian/Public/redis"
sentinel myid bca8111060bbda4a42ec3744391d1f40ca1fda00
sentinel deny-scripts-reconfig yes
sentinel monitor mymaster 127.0.0.1 6380 2
sentinel config-epoch mymaster 1
sentinel leader-epoch mymaster 1
# Generated by CONFIG REWRITE
sentinel known-slave mymaster 127.0.0.1 6379
sentinel known-slave mymaster 127.0.0.1 6381
sentinel known-sentinel mymaster 127.0.0.1 26380 ba913a9c10e46fd4df76bf0289721136031926ef
daemonize yes
sentinel known-sentinel mymaster 127.0.0.1 26381 7c387d9b2e95ac2338655fb8f5011f277635dade
sentinel current-epoch 1
- Hmm,首先Master已经切换了,不再是6379了,而是6380,而6379变成了known-slave,也就是Slave节点,无妨。。。反正现在也不工作。。。
- 那我现在恢复一下6379节点(重新执行redis-server redis-6379.conf),注意,这里的配置6379可是Master哦~
- 执行 redis-cli -p 6379 info replication ,信息如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Replication
role:slave
master_host:127.0.0.1
master_port:6380
master_link_status:up
master_last_io_seconds_ago:0
master_sync_in_progress:0
slave_repl_offset:162525
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:a6939796e0faba3555a74719ec18498e7b247756
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:162525
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:155146
repl_backlog_histlen:7380
- Hmm,说明sentinel还是蛮智能的,尽管6379之前是Master,但是选举出新的Master之后,旧的Master会被 降级到Slave节点,避免出现多个Master。
- 附带看一下6381和6380的日志:
- 首先是6380的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
43910:S 06 Oct 23:45:51.903 * MASTER <-> SLAVE sync started
43910:S 06 Oct 23:45:51.904 # Error condition on socket for SYNC: Connection refused
43910:S 06 Oct 23:45:52.914 * Connecting to MASTER 127.0.0.1:6379

...
43910:S 06 Oct 23:46:21.205 * MASTER <-> SLAVE sync started
43910:S 06 Oct 23:46:21.206 # Error condition on socket for SYNC: Connection refused
43910:M 06 Oct 23:46:21.960 # Setting secondary replication ID to 7a2c11e996810f76bdc72765130dcf47b5af4ab8, valid up to offset: 111989. New replication ID is a6939796e0faba3555a74719ec18498e7b247756
43910:M 06 Oct 23:46:21.960 * Discarding previously cached master state.
43910:M 06 Oct 23:46:21.962 * MASTER MODE enabled (user request from 'id=13 addr=127.0.0.1:51348 fd=12 name=sentinel-7c387d9b-cmd age=568 idle=0 flags=x db=0 sub=0 psub=0 multi=3 qbuf=0 qbuf-free=32768 obl=36 oll=0 omem=0 events=r cmd=exec')
43910:M 06 Oct 23:46:21.963 # CONFIG REWRITE executed with success.
43910:M 06 Oct 23:46:23.354 * Slave 127.0.0.1:6381 asks for synchronization
43910:M 06 Oct 23:46:23.354 * Partial resynchronization request from 127.0.0.1:6381 accepted. Sending 422 bytes of backlog starting from offset 111989.
43910:M 06 Oct 23:49:59.772 * Slave 127.0.0.1:6379 asks for synchronization
43910:M 06 Oct 23:49:59.772 * Partial resynchronization not accepted: Replication ID mismatch (Slave asked for '4a931cc983685e6f1b029225185120eee25f03b4', my replication IDs are 'a6939796e0faba3555a74719ec18498e7b247756' and '7a2c11e996810f76bdc72765130dcf47b5af4ab8')
43910:M 06 Oct 23:49:59.773 * Starting BGSAVE for SYNC with target: disk
43910:M 06 Oct 23:49:59.773 * Background saving started by pid 44404
44404:C 06 Oct 23:49:59.775 * DB saved on disk
43910:M 06 Oct 23:49:59.855 * Background saving terminated with success
43910:M 06 Oct 23:49:59.856 * Synchronization with slave 127.0.0.1:6379 succeeded
- 然后看一下6381的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
...
44117:S 06 Oct 23:46:22.338 * Connecting to MASTER 127.0.0.1:6379
44117:S 06 Oct 23:46:22.339 * MASTER <-> SLAVE sync started
44117:S 06 Oct 23:46:22.340 # Error condition on socket for SYNC: Connection refused
44117:S 06 Oct 23:46:22.866 * SLAVE OF 127.0.0.1:6380 enabled (user request from 'id=11 addr=127.0.0.1:51346 fd=12 name=sentinel-7c387d9b-cmd age=569 idle=0 flags=x db=0 sub=0 psub=0 multi=3 qbuf=133 qbuf-free=32635 obl=36 oll=0 omem=0 events=r cmd=exec')
44117:S 06 Oct 23:46:22.867 # CONFIG REWRITE executed with success.
44117:S 06 Oct 23:46:23.351 * Connecting to MASTER 127.0.0.1:6380
44117:S 06 Oct 23:46:23.351 * MASTER <-> SLAVE sync started
44117:S 06 Oct 23:46:23.352 * Non blocking connect for SYNC fired the event.
44117:S 06 Oct 23:46:23.353 * Master replied to PING, replication can continue...
44117:S 06 Oct 23:46:23.353 * Trying a partial resynchronization (request 7a2c11e996810f76bdc72765130dcf47b5af4ab8:111989).
44117:S 06 Oct 23:46:23.354 * Successful partial resynchronization with master.
44117:S 06 Oct 23:46:23.355 # Master replication ID changed to a6939796e0faba3555a74719ec18498e7b247756
44117:S 06 Oct 23:46:23.355 * MASTER <-> SLAVE sync: Master accepted a Partial Resynchronization.
- 额,刚想起来6379重启后的日志也看一下:
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
42485:M 06 Oct 23:45:51.599 # User requested shutdown...
42485:M 06 Oct 23:45:51.600 * Saving the final RDB snapshot before exiting.
42485:M 06 Oct 23:45:51.601 * DB saved on disk
42485:M 06 Oct 23:45:51.601 * Removing the pid file.
42485:M 06 Oct 23:45:51.602 # Redis is now ready to exit, bye bye...
44398:C 06 Oct 23:49:48.652 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
44398:C 06 Oct 23:49:48.653 # Redis version=4.0.11, bits=64, commit=00000000, modified=0, pid=44398, just started
44398:C 06 Oct 23:49:48.654 # Configuration loaded
44399:M 06 Oct 23:49:48.656 * Increased maximum number of open files to 10032 (it was originally set to 4864).
44399:M 06 Oct 23:49:48.657 * Running mode=standalone, port=6379.
44399:M 06 Oct 23:49:48.657 # Server initialized
44399:M 06 Oct 23:49:48.657 * DB loaded from disk: 0.000 seconds
44399:M 06 Oct 23:49:48.658 * Ready to accept connections
44399:S 06 Oct 23:49:58.958 * Before turning into a slave, using my master parameters to synthesize a cached master: I may be able to synchronize with the new master with just a partial transfer.
44399:S 06 Oct 23:49:58.958 * SLAVE OF 127.0.0.1:6380 enabled (user request from 'id=3 addr=127.0.0.1:52967 fd=7 name=sentinel-ba913a9c-cmd age=10 idle=0 flags=x db=0 sub=0 psub=0 multi=3 qbuf=0 qbuf-free=32768 obl=36 oll=0 omem=0 events=r cmd=exec')
44399:S 06 Oct 23:49:58.960 # CONFIG REWRITE executed with success.
44399:S 06 Oct 23:49:59.769 * Connecting to MASTER 127.0.0.1:6380
44399:S 06 Oct 23:49:59.769 * MASTER <-> SLAVE sync started
44399:S 06 Oct 23:49:59.770 * Non blocking connect for SYNC fired the event.
44399:S 06 Oct 23:49:59.771 * Master replied to PING, replication can continue...
44399:S 06 Oct 23:49:59.771 * Trying a partial resynchronization (request 4a931cc983685e6f1b029225185120eee25f03b4:1).
44399:S 06 Oct 23:49:59.774 * Full resync from master: a6939796e0faba3555a74719ec18498e7b247756:155145
44399:S 06 Oct 23:49:59.774 * Discarding previously cached master state.
44399:S 06 Oct 23:49:59.856 * MASTER <-> SLAVE sync: receiving 202 bytes from master
44399:S 06 Oct 23:49:59.857 * MASTER <-> SLAVE sync: Flushing old data
44399:S 06 Oct 23:49:59.857 * MASTER <-> SLAVE sync: Loading DB in memory
44399:S 06 Oct 23:49:59.858 * MASTER <-> SLAVE sync: Finished with success
- 从日志中可以看到,6379下线以后,6380和6381经历了一段时间(约30s)的找不到Master,之后6380收到了sentinel-7c387d9b-cmd发送的请求,转换角色为Master。6381收到sentinel-7c387d9b-cmd发送的请求,变更为跟随6380而不是6379。等6379重新启动后收到了sentinel-ba913a9c-cmd的请求,降级为Slave并且跟随6380。
  • 补充说明一下sentinel一定要集群部署,不能单点!否则网络等因素会导致集群来回切换角色,另外sentinel本身单点也有风险!

Redis HA 之 Clusterv

背景及问题

  • 说一下背景

    • Doc数量约20W+
    • 机器配置 4C 8G * 3
    • ElasticSearch 5.6.3
    • 每个Doc中的Nested字段(价格列表,不同城市价格不同)中包含约100-200个嵌套文档
  • 查询需求:

    • 每个查询均需要从嵌套文档中进行查询(根据城市Id)
    • 最终只需要返回一个嵌套文档(城市Id符合需求的)
  • 开始没有想太复杂,直接使用nested Query + inner_hits取出来文档,觉得很方便,但是后来进行测试,发现只要条件中加入了城市Id,查询就很慢(平均120+ms),而不带城市id基本在40ms+,于是开始了查询问题定位。

问题定位

  • 根据条件定位问题肯定是出在了城市Id查询,观察了一下mapping,估计问题是出在了nested上,对Query DSL语句进行注意尝试,发现如果不带inner_hits,只是进行城市Id查询,速度可以稳定在45ms左右,一旦加上innder_hits速度就在120ms左右,看来问题是在这里了。
  • 查了官方文档,说inner_hits是父子文档的优化版本,附带了一句nested对检索性能有较大影响,也没有其他相关资料了。
  • 论坛,Google 一下也没有这方面资料,有点没有头绪。

问题解决

  • 本地使用Query DSL 尝试了一下将整个整个价格列表返回,不再使用inner_hits,发现速度竟然提升了(数据返回话费了4s+,但是EsQuery仅用了50ms)那说明可以删掉inner_hits,直接返回价格列表,然后在内存中进行筛选
  • 按照这个思路改了一下项目代码,发现速度确实有提升,之前平均响应时间在130+ ms(Query+处理),现在基本不过70ms。

继续优化

  • 官方的那句nested对检索性能有较大影响还是得重视起来,后来进行了一个尝试,将价格列表和Doc组合起来,不再使用嵌套文档,直接进行平铺,这样就相当于取消了nested查询,发现速度再一次提高了,查询的平均检索时间降低到40ms+,针对城市Id又加上了_routing,速度最终稳定在20ms+。好了,在测试环境的优化最终就这样了,等节后花时间测试一下,放到线上估计速度能提高不少~

总结

  • 其实还是最初的索引结构设计不是很合理,尤其是城市价格这块,最开始没有加入_routing,这算是一个败笔了,每次查询都需要查询城市id,还没加上,这个确实是失误了。。。
  • nested+inner_hits对检索性能确实有很大影响(最起码我这种场景影响很大了)
  • 官方提示了,nested一定程度影响性能,如果可以的话,还是要注意一下,避免这种结构,现在将nested字段平铺开后虽然doc数量激增,但是检索速度大幅度提升,这样代价也值了~

背景

  • 前两天做项目需要讲数据从MySQL 同步到ES中,由于索引结构经常发生变化,需要时不时就跑一个全量的索引。
  • 数据库MySQL5.7
  • 开始方案没有选好,图省事,用的select * from table limit x,10000进行全量读取(现在已经改成用id扫描方式了。。),开始速度还可以,越到后面越慢。

自己测试

  • 在自己的电脑上面做了一个一个测试,主要是优化一下limit查询,有一定提升,但是这个尽量还是别用的好,全量还是用id扫描最快。

  • 配置如下:

    • 16款MacPro,16G内存 4Core i7 2.2G,SSD硬盘
    • MySQL 5.7.22
    • 表很简单一列主键id,一列guid,guid没有加索引,InnoDB引擎
    • 数据量24589792(自己select insert 进去的)
    • 没有做任何特殊优化,只是标准安装(- -自己水平有限)
  • 测试结果如下:

    • 语句1:SELECT * FROM token LIMIT 24589790,1
    • 结果:
      • OK, Time: 5.920000s
      • OK, Time: 5.872000s
      • OK, Time: 5.821000s
      • OK, Time: 5.819000s
      • OK, Time: 5.880000s
    • 语句2:SELECT * FROM token INNER JOIN (SELECT id FROM token LIMIT 24589790,1) t USING (id)
    • 结果:
      • OK, Time: 4.897000s
      • OK, Time: 4.962000s
      • OK, Time: 4.897000s
      • OK, Time: 4.889000s
      • OK, Time: 4.910000s
  • 其实还是挺明显的,接近1秒的差别

原理

  • 从《高性能MySQL》中看到的,摘抄一下
  • LIMIT和OFFSET,尤其是OFFSET 会导致MySQL扫描大量的不需要的行,然后抛弃掉,这个也是查询慢的根本原因。
  • 语句2提升查询效率是因为他可以让MySQL扫描尽可能少的页面,获取到需要访问的记录后再根据关联列去原表找到所需要的列。

高效的最终方案

  • 就是上面提到的通过Id扫描的方式进行查询,速度杠杠的
  • SELECT * FROM token WHERE id>4589790 LIMIT 1
  • 花费时间(hmmm……显示是OK, Time: 0.000000s)