不为有趣之事,何遣有涯之生
不失其所者久,死而不亡者寿

Redis之路系列原理篇(5)

hzqiuxm阅读(1504)

5 原理篇—问渠哪得清如许

单线程模型

Redis的线程模型:基于NI/O、单线程、异步的线程模型
Redis基于Reactor模式开发了网络事件处理器,这个处理器叫做文件事件处理器(file event handler)。

这个文件事件处理器,是单线程的,采用I/O多路复用机制同时监听多个socket,它根据socket上的事件,来选择对应的事件处理器来处理这个事件。

下方是Redis单线程模型示意图:

文件事件处理器的结构包含4个部分:多个socket,I/O多路复用程序,文件事件分发器,事件处理器
客戶端与Redis通信的流程大致如下:

  • 1:客户端发出连接Redis的请求,产生 AE_READABLE事件,最后会关联到连接应答处理器,由它来负责真正处理跟客户端的连接
  • 2:客户端向Redis发出命令请求,产生 AE_READABLE事件,最后会关联到命令请求处理器,由它来从socket中获取相关数据,然后进行真正的执行处理
  • 3 :Redis处理完成请求过后,会准备好返回给客户端的数据,产生 AE_WRITABLE事件,最后会关联到命令回复处理器,由它来把数据写入到 socket,返回到客户端
  • 4 :命令回复处理器完成后,就会删除这个socket的 AE_WRITABLE事件和命令回复处理器之间的关联关系

事务

基本特点

Redis中的事务本质就是一组命令的集合,被依次顺序的执行,当然可以放弃事务的执行,那么所有事务里面的命令都不会执行。
Redis中的事务特点:

  • 单线程架构保证了执行完事务内所有指令前是不可能再去同时执行其他客户端的请求的
  • Redis的事务没有隔离级别的概念,不存在”事务内的查询要看到事务里的更新,在事务外查询不能看 到”这种问题了
  • Redis的事务不保证原子性,只有决定是否开始执行全部指令的能力,没有执行到一半进行回滚的能力

基本过程

  • 1 发送事务开始指令:multi
  • 2 依次发送执行命令
  • 3 依次执行命令:exec

注意:如果某命令有语法错误,那么所有命令都不会执行;如果某命令只是执行错误,其它命令会正常执行,之后返回错误信息,无法回滚

持久化

Redis持久化分成三种方式:RDB(Redis DataBase)、AOF (Append Only File)和AOF+RDB混合持久化

  • RDB:在不同的时间点,将Redis某一时刻的数据生成快照并存储到磁盘上(缺省打开)
  • AOF :只允许追加不允许改写的文件,是将Redis执行过的所有写指令记录下来,在下次Redis重新启动时,只要把这些写指令从前到后再重复执行一遍,就可以实现数据恢复了(缺省关闭)
  • 混合方式:混合方式是先使用RDB进行快照存储,然后使用AOF持久化记录所有的写操作(redis6后推荐使用,缺省开启依赖RDB和AOF)。

RDB

默认情况下就是开启的

RDB方式,Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了, 再用这个临时文件替换上次持久化好的文件。

  • 主进程不进行任何的IO操作,确保了极高的性能

  • RDB是快照读取,大规模数据效率比AOF好,但可能会丢失最后一次持久化数据

  • 适合对数据完整性不是很敏感的场景,比如冷备份,可以高性能快速恢复数据

相关配置:

  • save * :保存快照的频率,第一个表示多长时间,单位是秒,第二个*表 示至少执行写操作的次数;在一定时间内至少执行一定数量的写操作时,就自动保存快照;可设置多个条件。

如果想禁用RDB持久化的策略,只要不设置任何save指令,或者给save传入一个空字符串参数也可以
如果用户开启了RDB快照功能,那么在Redis持久化数据到磁盘时如果出现失败,默认情况下,Redis会停止接受所有的写请求。

这样做的好处在于可以让用户很明确的知道内存中的数据和磁盘上的数据已经存在不一致 了。如果下一次RDB持久化成功,redis会自动恢复接受写请求。

  • dbfilename:数据快照文件名(只是文件名,不包括目录),默认dump.rdb,还可以把它当作冷备份使用
  • dir:数据快照的保存目录(这个是目录),默认是当前路径
  • stop-writes-on-bgsave-error:如果配置成no,表示你不在乎数据不一致或者有其他的手段发现和控制这种不一致,那么在快照写入失败时,也能确保 redis继续接受新的写请求
  • rdbcompression:对于存储到磁盘中的快照,可以设置是否进行压缩存储。 如果是的话,redis会采用LZF算法进行压缩。默认是开启的,如果你不想消耗CPU来进行压缩的话,可以设置为关闭此功能
  • rdbchecksum:在存储快照后,还可以让redis使用CRC64算法来进行数据校验 ,但是这样做会增加大约10%的性能消耗,默认是开启的,如果希望获取到最大的性能提升 ,可以关闭此功能
  • rdb-del-sync-files:在没有持久性的情况下删除复制中使用的RDB文件启用 。默认情况下,此选项是禁用的。

RDB模式要注意的一些问题:

  • fork一个进程时,内存的数据也被复制了,即内存会是原来的两倍。
  • 每次快照持久化都是将内存数据完整写入到磁盘一次,并不是增量的同步数据。如果数据量大的话,而且写操作比较多,必然会 引起大量的磁盘io操作,可能会严重影响性能。
  • 由于快照方式是在一定间隔时间做一次的,所以如果redis意外 down掉的话,就会丢失最后一次快照后的所有修改。

触发快照情况:

  • 根据配置规则进行自动快照
  • 用户执行save(阻塞所有客户端请求)或bgsave(后台异步处理)命令
  • 执行flushall命令,会清除内存中所有数据,然后生产rdb文件(数据全没了)
  • 执行复制replication时

AOF

将配置项appendonly设置为yes就可以打开AOF功能,默认是关闭的。

默认的AOF持久化策略是每秒钟fsync一次,fsync是指把缓存中的写指令记录到磁盘中,在这种情况下,Redis仍可以保持很高的性能。

  • OS会在内核中缓存Redis的写操作

  • 缓存不会立即写到磁盘上,会有一定的延迟,理论上是有数据丢失的可能性

  • 你可以修改配置,强制不要缓存直接写入磁盘中

  • Fsync的方式有三种,推荐每秒启用一次的everysec

  • 优点:更好的保护数据不丢失 、性能高、可做紧急恢复

  • 缺点:文件比RDB文件大、写的QPS比RDB低

相关配置:

  • appendonly:是否开启AOF,默认关闭

  • appendfilename:设置AOF的日志文件名

  • appendfsync:设置AOF日志如何同步到磁盘,fsync()调用,用来告诉操作系统立即将缓存的指令写入磁盘,有三个选项always(一般不用),everysec(推荐使用),no(操作系统自己决定同步时间)

  • no-appendfsync-on-rewrite:设置当redis在rewrite的时候,是否允许appendsync。因为redis进程在进行AOF重写的时候,fsync()在主进程中的调用会被阻止,也就是redis的持久化功能暂时失效。默认为no ,这样能保证数据安全

  • auto-aof-rewrite-min-size:设置一个最小值,是为了防止在aof很小时就触发重写

  • auto-aof-rewrite-percentage:设置自动进行AOF重写的基准值,也就是重写启动时的AOF文件大小,假如redis自启动至今还没有进行过重写,那么启动时aof文件的大小会被作为基准值。这个基准值会和当前的aof大小进行比较。如果当前aof大小超出所设置的增长比例,则会 触发重写。如果设置auto-aof-rewrite-percentage为0,则会关闭此重写功能

  • AOF持久化采用文件追加的方式,这会导致AOF文件越来越大,因为AOF相当与和写相关的操作日志,所以AOF设置了重写机制

  • 重写机制就是当AOF超过一定大小,就会启动文件内容压缩,只保留可以恢复数据的最小指令集

  • 主动触发重写命令bgrewriteaof

重写基本流程:

  • 1 在重写开始前,redis会创建一个“重写子进程”,这个子进程会读取现有的 AOF文件,并将其包含的指令进行分析压缩并写入到一个临时文件中。
  • 2 与此同时,主进程会将新接收到的写指令一边累积到内存缓冲区中,一边继续写入到原有的AOF文件中,这样做是保证原有的AOF文件的可用性,避免在重写过程中出现意外。
  • 3 当“重写子进程”完成重写工作后,它会给父进程发一个信号,父进程收到信号后就会将内存中缓存的写指令追加到新AOF文件中
  • 4 当追加结束后,redis就会用新AOF文件来代替旧AOF文件,之后再有新的写指 令,就都会追加到新的AOF文件中
  • 5 重写aof文件的操作,并没有读取旧的aof文件,而是将整个内存中的数据库内容用命令的方式重写了一个新的aof文件,这点和快照有点类似

AOF日志恢复
如果在追加日志时,恰好遇到磁盘空间满或断电等情况,导致日志写入不完整,也没有关系,Redis提供了redis-check-aof工 具,可以用来进行日志修复,基本步骤如下:

  • 备份被写坏的AOF文件
  • 运行redis-check-aof –fix进行修复
  • 用diff -u来看下两个文件的差异,确认问题点
  • 重启redis,加载修复后的AOF文件

混合模式

先使用RDB进行快照存储,然后使用 AOF持久化记录所有的写操作,当重写策略满足或手动触发重写的时候,将最新的数据存储为新的RDB记录。

这样的话,重启服务的时候会从RDB和AOF两部分恢复数据, 既保证了数据完整性,又提高了恢复数据的性能。

通过 aof-use-rdb-preamble 配置项可以打开混合方式,默认是yes。
所以日志文件中会同时包含了RDB数据和AOF数据

数据恢复顺序:

  • 1 判断是否开启AOF持久化,若开启了AOF,则使用AOF持久化文件恢复数据
  • 2 如果AOF文件不存在,否则使用RDB持久化文件恢复数据
  • 3 如果AOF文件和RDB文件都不存在则直接启动Redis
  • 4 如果AOF或RDB文件出现错误,则启动失败返回错误信息

附录1:Redis事务基本命令

  • 1:multi:设置事务开始
  • 2:exec:执行事务
  • 3:discard:放弃事务
  • 4:watch:监控键值,如果键值被修改或删除,后面的一个事务就不 会执行
  • 5:unwatch:取消watch

关于watch:Redis使用Watch来提供乐观锁定,类似于CAS;可以被调用多次;当 EXEC 被调用后,所有的之前被监视的键值会被取消监视,不管事务是否被取消或者执行。并且当客户端连接丢失的时候,所有东 西都会被取消监视

Redis之路系列拓展篇(4)

hzqiuxm阅读(1778)

4 拓展篇—功夫在诗外

6.0新特性

相对都比较鸡肋,谨慎在生产环境使用

ACL安全策略

Redis6版本推出了ACL(Access Control List)访问控制权限 的功能,基于此功能,可以设置多个用户,并且给每个用户单独设 置命令权限和数据权限。

ACL文件配置方式有两种,一种是在config文件中直接配置,另一种是在外部aclfile中配置,配置命令是一样的
ACL是使用DSL定义的,该规则是从上到下,从左到右的,具体可以参考附录中的具体规则说明

RESP3和客户端缓存

RESP(REdisSerializationProtocol),是 Redis服务端与客户端之间通信的协议。
在Reds6之前的版本,使用的是RESP2协议,数据都是以字符串数组的形式返回给客户端,客户端需要根据类型进行解析,增加了客户端实现的复杂度。
RESP3协议可以按类型返回数据。 可以使用HELLO命令在RESP2和RESP3协议之间进行切换

新增的客户端缓存的设计导致客户端缓存和redis缓存绑定,十分不灵活,会导致资源浪费,没有很大的实际使用价值。

Redis中值的变化每次需要同步到客户端缓存,但是客户端并非会用到没的变化,导致资源浪费,数据管理更加复杂了,实际价值不大

I/O多线程

注意:主流程操作还是单线程
开启IO多线程:io-threads-do-reads yes 配置线程数量,如果设为1就是主线程模式:io-threads 4

目前官方建议:至少4核的机器才开启IO多线程,并且除非真的遇到了性能瓶颈,否则不建议开启此配置 。 建议配置的线程数少于机器总线程数,线程并不是越多越好 ,多于8个线程意义不大

集群代理

集群代理把集群抽象成为单个Redis实例,客户端通过集群 代理访问集群,就像访问单个Redis一样,不去关心很多集群上 的问题,比如:集群模式下multiple操作的限制及跨slot操作限 制等等,这些由集群代理去处理。

目前是一个实验性的功能,需要单独安装、单独配置、单独运行,不建议生产环境使用。

Lua脚本

Lua是一个高效、简洁、轻量级、可扩展的脚本语言,可以很方便的嵌入到其它语言或中间件中使用,Redis从2.6版支持Lua。

使用脚本好处

  • 1:减少网络开销
  • 2:原子操作:Redis会把脚本当作一个整体来执行,中间不会插入其它命令
  • 3:复用功能

基本数据类型

Lua是一个动态类型的语言,一个变量可以存储任何类型的值

基础语法

  • 默认全局变量,但是在Redis中不允许使用,局部变量使用local关键字进行声明
  • 变量名必须是非数字开头,只能包含字母、数字和下划线
  • 变量名是区分大小写
  • 逻辑操作符用单词表示:and、or、not
  • 单行注释使用 --来表示,多行注释使用--[[ 开始,到 ]] 结束
  • 支持多重赋值:local a,b = 1,2,3 (a=1,b=2),不过不建议,难维护易错
  • 使用 .. 用来连接字符串
  • 使用 #来获取长度,比如:print(#'helloworld')
  • 只有nil和false是假,其它都是真
  • 循环语句有for、while、repeat三种

与Redis结合

在Redis中执行lua脚本命令: redis-cli --eval 脚本 [key...] , [arg...]
在脚本中调用Redis命令:redis.call,遇到错误会直接返回;想要继续执行使用redis.pcall命令

Lua数据类型与Redis返回值类型对应关系:

其它命令:

  • evalsha:可以通过脚本摘要来运行,作用同eval
  • script load:将脚本加入缓存,返回值就是SHA1摘要
  • script exists:判断脚本是否已经缓存
  • script flush:清空脚本缓存
  • script kill:强制终止脚本的执行

发布订阅模式

Redis支持简单的发布订阅模式,可以实现进程间的消息传递,对业务要求不高的可以选择使用
下面是一些常用的操作命令:

  • 1:publish:发布消息,格式是publish channel 消息
  • 2:subscribe:订阅频道,格式是subscribe channel,可以是多个channel
  • 3:psubscribe:订阅频道,格式是psubscribe channel,支持glob风格的通配符
  • 4:unsubscribe:取消订阅,格式是unsubscribe channel,不指定频道表示取消所有subscribe命令的订阅
  • 5:punsubscribe:取消订阅,格式是punsubscribe channel,不指定频道表示取消所有psubscribe命令的订阅

注意这里匹配模式的时候,是不会将通配符展 开的,是严格进行字符串匹配的,比如:punsubscribe * 是无法退定 c1.* 的,必须严格使用punsubscribe c1.*才可以

常用优化手段

  • 使用管道Pipeline,有利于减少网络通信和传输
  • 精简键名和键值
  • 合理设计存储的数据结构和数据关系,减少数据冗余
  • 尽量使用mset来赋值,类似还有lpush、zadd等批量操作的命令
  • 如果可能,尽量使用Lua脚本来辅助获取和操作数据
  • 尽量使用hash结构来存储对象
  • 使用hash结构时,应尽量保证每个key下面<field,value>的数目不超过限制(默认值是64)
  • 配置使用类似ziplist以优化list等数据结构
  • 一定要设置maxmemory,保证redis不会因为内存导致崩溃

客户端

Lettuce简介

Lettuce是一个可伸缩线程安全的Redis客户端。多个线程可以共享同一个RedisConnection。它利用优秀netty NIO框架来高效地管理多个连接。

Lettuce核心组件有:RedisURI、Connection、RedisClient、RedisCommands

  • RedisURI:封装Redis的连接信息
  • Connection:Redis的连接,包含一些具体方式(单机、哨兵、集群、订阅发布等)
  • RedisClient:Redis的客户端,如果连接集群则为RedisClusterlient
  • RedisCommands:Redis的命令操作API接口,提供了三种调用方式:同步、异步、响应式

基本使用

官网地址:https://lettuce.io/docs/getting-started.html

基本依赖:

dependencies {
  compile 'io.lettuce:lettuce-core:6.1.4.RELEASE
}

基本使用示例:

RedisClient redisClient = RedisClient.create("redis://password@localhost:6379/0");
StatefulRedisConnection<String, String> connection = redisClient.connect();
RedisCommands<String, String> syncCommands = connection.sync();

syncCommands.set("key", "Hello, Redis!");

connection.close();
redisClient.shutdown();

与Spring的集成

@Configurationclass AppConfig { @Beanpublic LettuceConnectionFactory redisConnectionFactory() { return new LettuceConnectionFactory(new RedisStandaloneConfiguration("server", 6379)); } }

附录1:Lua脚本常用标准库与函数

Lua的标准库提供了很多使用的功能,Redis支持其中大部分:

  • 1:Base:提供一些基础函数
  • 2:String:提供用于操作字符串的函数
  • 3:Table:提供用于表操作的函数
  • 4:Math:提供数据计算的函数
  • 5:Debug:提供用于调试的函数

除了标准库外,Redis还会自动加载cjson和cmsgpack库,以提供对 Json和MessagePack的支持,在脚本中分别通过cjson和cmsgpack两 个全局变量来访问相应功能

在Redis中常用的标准库函数:

  • 1:string.len(string)
  • 2:string.lower(string)
  • 3:string.upper(string)
  • 4:string.rep(s, n):返回重复s字符串n次的字符串
  • 5:string.sub(string,start[,end]),索引从1开始,-1表示最后一 个
  • 6:string.char(n…):把数字转换成字符
  • 7:string.byte (s [, i [, j]]):用于把字符串转换成数字
  • 8:string.find (s, pattern [, init [, plain]]):查找目标模板 在给定字符串中出现的位置,找到返回起始和结束位置,没找到返 回nil
  • 9:string.gsub (s, pattern, repl [, n]):将所有符合匹配模式的 地方都替换成替代字符串。并返回替换后的字符串,以及替换次数 。四个参数,给定字符串,匹配模式、替代字符串和要替换的次数
  • 10:string.match (s, pattern [, init]):将返回第一个出现在给 定字符串中的匹配字符串,基本的模式有:. 所有字符,%a字母, %c控制字符,%d数字,%l小写字母,%p 标点符号字符,%s 空格, %u 大写字母,%w 文字数字字符,%x 16进制数字等
  • 11:string.reverse (s):逆序输出字符串
  • 12:string.gmatch (s, pattern):返回一个迭代器,用于迭代所有出现 在给定字符串中的匹配字符串
  • 13:table.concat(table[,sep[,i[,j]]]):将数组转换成字符串,以sep 指定的字符串分割,默认是空,i和j用来限制要转换的表索引的范围 ,默认是1和表的长度,不支持负索引
  • 14:table.insert(table,[pos,]value):向数组中插入元素,pos为指定 插入的索引,默认是数组长度加1,会将索引后面的元素顺序后移
  • 15:table.remove(table[,pos]):从数组中弹出一个元素,也就是删除这 个元素,将后面的元素前移,返回删除的元素值,默认pos是数组长度 table.sort(table[,sortFunction]):对数组进行排序,可以自定义 排序函数
  • 16:Math库里面常见的:abs、ceil、floor、max、min、pow、sqrt、 sin、cos、tan等
  • 17:math.random([m[,n]]):获取随机数,如果是同一个种子的话, 每次获得的随机数是一样的,没有参数,返回0-1的小数;只有m, 返回1-m的整数;设置了m和n,返回m-n的整数
  • 18:math.randomseed(x):设置生成随机数的种子

附录2:ACL规则

启用和禁用用户

  • on:启用用户
  • off:禁止用户

允许和禁止调用

  • +:将命令添加到用户可以调用的命令列表中
  • -:将命令从用户可以调用的命令列表中移除
  • +@:允许用户调用 类别中的所有命令
  • -@:禁止用户调用 类别中的所有命令
  • +|subcommand:允许使用已禁用命令的特定子命令
  • allcommands:+@all的别名,包括当前存在的命令以及将来通过模块加载的所有命令
  • nocommands:-@all的别名,禁止调用所有命令

允许或禁止访问key

  • ~:添加可以在命令中使用的键模式
    • resetkeys:使用当前模式覆盖所有允许的模式

为用户配置有效密码

  • >:将此密码添加到用户的有效密码列表中
  • <:从有效密码列表中删除此密码
  • #:将此SHA-256哈希值添加到用户的有效密码列表中
  • !:从有效密码列表中删除该哈希值
  • nopass:移除该用户已设置的所有密码
  • resetpass:清空该用户的所有密码列表
  • reset:重置用户状态为初始状态

PS:具体命令可以通过acl help 查看

Redis之路系列集群篇(3)

hzqiuxm阅读(1371)

3 集群篇—众人拾柴火焰高

复制

基本概述

Redis支持复制的功能,以实现当一台服务器的数据更新后,自动将新的数据异步同步到其它数据库。

Redis复制实现中,把数据库分为主数据库master和从数据库slave ,主数据库可以进行读写操作,从数据库默认是只读的,当主数据库数据变化的时候,会自动同步给从数据库。

主从复制的结构支持一个Mater带多个Slave,也可以Slave带Slave模式

复制的好处:

  • 可以实现读写分离
  • 在主数据崩溃时可以实现数据恢复
  • 可进行水平扩容支撑高并发

基本配置

  • 配置原则 :主数据库不做配置,从数据库中设置 replicaof 主数据库ip 主数据库port

具体的配置请查阅附录一

基本原理

  • 全量复制

  • slave启动时,会向master发送psync请求,如果这是slave重新连 接master,那么master仅仅会复制给slave缺少的数据; 如果是第 一次连接master,那么会触发一次全量复制
  • 主数据库接到psync请求后,如果是全量复制,会在后台保存快照 ,就是实现RDB持久化,并将保存快照期间接收到的命令缓存起来
  • 快照完成后,主数据库会将快照文件和所有缓存的命令发送给从数据库
  • 从数据库接收后,会载入快照文件并执行缓存的命令,从而完成复 制的初始化

  • 增量复制

  • 如果是重新连接,Master会检查backlog里面的offset, master和 slave都会保存一个复制的offset还有一个master id,offset是保 存在backlog中的。Master只会把已经复制的offset后面的数据复 制给Slave,类似断点续传
  • 在数据库使用阶段,主数据库会自动把每次收到的写命令同步到从 服务器
  • Slave在复制的时候,不会阻塞Master的正常工作;也不会阻塞对 自己的查询操作,它会用旧的数据集来提供服务; 但是复制完成的 时候,需要删除旧数据集,加载新数据集,这个时候就会暂停对外 服务了

PS:Master一定要持久化

  • 乐观复制策略:Redis采用乐观复制的策略,容忍在一定时间内主从数据库 的内容不同,当然最终的数据会是一样的。

这个策略保证了性能, 在复制的时候,主数据库并不阻塞,照样处理客户端的请求。 Redis提供了配置来限制只有当数据库至少同步给指定数量 的从数据库时,主数据库才可写,否则返回错误。配置是:min-replicas-to-write、min-replicas-max-lag

  • 无硬盘复制:Master直接创建一个子进程,来生成RDB文件的内容,并通 过网络直接传送给Slave,也就是RDB文件不保存到磁盘上。

数据丢失解决方案

  • 异步复制导致丢失:master宕机,在主备切换的过程中,可能会导致数据丢失。
  • 主从脑裂导致丢失:网络原因导致master短时间脱离集群,集群产生了新master,但客户端仍然往旧mater写数据,网络恢复后,旧master变成slave与新mater同步导致数据丢失。

解决方案:配置min-replicas-to-write和min-replicas-max-lag两个参数,比如: min-replicas-to-write 1 、 min-replicas-max-lag 10。

要求至少有1个slave,数据复制和同步的延迟不能超过10秒。

一旦所有的slave,数据复制和同步的延迟都超过了10秒钟, 那么这个时候,master就不会再接收任何请求了。

脑裂情况下,一个master跟其它slave丢了连接,那么这两个配置可以确保,如果不能继续给指定数量的slave发送数据,而且slave 超过10秒没有给自己应答消息,那么就直接拒绝客户端的写请求。

哨兵

基本概述

哨兵是Redis 复制集集群的重要组件,它的主要作用有:

  • 集群监控:监控主从数据库运行是否正常
  • 故障转移:当主数据库出现故障时,自动将从数据库转换成为主数据库
  • 配置中心:客户端通过连接哨兵来获得当前Redis服务的主节点地址
  • 消息通知:哨兵可以将故障转移的结果发送给客户端

开启哨兵功能需要建立一个个sentinel.conf文件,里面设置要监控的主数据库的名字,比如:sentinel monitor 监控的主数据库的名字 127.0.0.1 6380 1

其中数字1表示的是哨兵判断主节点是否法神故障的最低票数,这个文件在运行期间会被sentinel动态进行更改,可以同时监控多个主数据库,一行一个配置即可,详细的配置参考附录二。

哨兵集群在整个主从集群中的功能关系结构图如下:

主从节点存放数据,而哨兵节点用来自动监控和维护集群,不存放数据
每个哨兵节点维护了3个定时任务:

  • 1 哨兵向master-slave节点发送info命令来获取复制集的结构
  • 2 哨兵之间通过pub/sub系统来互相感知装态和信息
  • 3 哨兵向master-slave节点发送ping命令来进行心跳检测

基本原理

  • SDown:主观下线,在心跳检测的定时任务中,如果其它节点超过一定时间没有回复, 哨兵节点就会将其进行主观下线。也就是说,主观下线的意思是一个哨兵节点“主观地”判断某个节点下线

  • ODown:客观下线,哨兵节点在对主节点进行主观下线后,会通过sentinel is-master-down-by-addr命令询问其它哨兵节点该主节点的状态;如果判断主节点下线的哨兵数量达到一定数值,则对该主节点进行客观下线。

哨兵四大基本原理:自动发现、选举领导、故障转移、配置传播

  • 自动发现
    哨兵互相之间的相互自动发现,是通过redis的pub/sub系统实现的 ,每个哨兵都会往sentinel:hello这个channel里发送一个消息,这时 候所有其他哨兵都可以消费到这个消息,并感知到其他的哨兵的存在。

每隔两秒钟,每个哨兵都会往自己监控的某个master+slave对应的 sentinel:hello channel里发送一个消息,内容是自己的host、ip和 runid还有对这个master的监控配置。

每个哨兵也会去监听自己监控的每个master+slaves对应的 sentinel:hello channel,然后去感知到同样在监听这个 master+slave的其他哨兵的存在。

每个哨兵跟其他哨兵交换对master的监控配置,互相进行监控配置 的同步

  • 选举领导:选举出领导者哨兵

哨兵选举涉及两个参数:quorum(确认客观下线的最少的哨兵数量),majority(授权进行主从切换的最少的哨兵数量)

每次一个哨兵要做主备切换,首先需要quorum数量的哨兵认为master客观下线,然后选举出一个哨兵来做切换,这个哨兵还需要得到majority哨兵的授权,才能正式执行切换

当主节点被判断客观下线以后,各个哨兵节点会进行协商, 选举出一个领导者哨兵节点,并由该领导者节点对其进行故障转移操作。

监视该主节点的所有哨兵都有可能被选为领导者,选举使用的算法是Raft算法;Raft算法的基本思路是先到先得:即在一轮选举中,哨兵A向B发送成为领导者的申请,如果B没有同意过其他哨兵,则会同意A成为领导者

  • 故障转移:选举出的领导者哨兵,开始进行故障转移操作,选举出新mater节点,大体可以分为3个步骤

1 在从节点中选择新的主节点:选择的原则是,首先过滤掉不健康的从节点; 然后选择优先级最高的从节点(由slave-priority指定);如果优先级无法区分,则选择复制offset最大的从节点;如果仍无法区分,则选择runid最小的从节点

2 更新主从状态:通过slaveof no one命令,让选出来的从节点成为主节点; 并通过slaveof命令让其他节点成为其从节点

3 将已经下线的主节点设置为新的主节点的从节点,当其重新上线后,它会成为新的主节点的从节点

  • 配置传播
    新的master选出过后,执行切换的那个哨兵,会从要切换到的新master那里得到一个configuration epoch,这就是一个 version号,每次切换的version号都必须是唯一的。

如果第一个选举出的哨兵切换失败了,那么其他哨兵,会等待failover-timeout时间,然后接替继续执行切换,此时会重新获取一个新的configuration epoch,作为新的version号。

哨兵完成master切换之后,会在自己本地更新生成最新的master配置,然后同步给其他的哨兵,就是通过之前说的pub/sub 消息机制。

这时version号就很重要了,因为各种消息都是通过一个 channel去发布和监听的,所以一个哨兵完成一次master的切换之 后,新的master配置是跟着新的version号的。

其他的哨兵都是根据版本号的大小来更新自己的master配置的。

使用建议

  • 哨兵节点的数量应为多个,哨兵本身应该集群,保证高可用
  • 哨兵节点的数量应该是奇数
  • 各个哨兵节点的配置应一致

另外如果哨兵节点部署在Docker等容器里面,尤其要注意端口的正确映射,还需要知道哨兵集群+主从复制,并不能保证数据零丢失

分片

由于复制中,每个数据库都是拥有完整的数据,因此复制的 总数据存储量,受限于内存最小的数据库节点,如果数据量过大, 复制就无能为力了。

这个时候就需要用到分片技术,分片是将数据拆分到多个Redis实例的过程,这样每个Redis实例将只包含完整数据的一部分。

常见的分片方式有:按范围分片和哈希分片

分片实现方式

  • 在客户端进行分片
  • 通过代理来进行分片,比如:Twemproxy
  • 查询路由:就是发送查询到一个随机实例,这个实例会保证转发你的查询到正确的节点,Redis集群在客户端的帮助下,实现了查询路由的一种混合形式,请求不是直接从Redis实例转发到另一个, 而是客户端收到重定向到正确的节点(客户端启动时,加上-c参数)
  • 在服务器端进行分片, Redis采用哈希槽(hash slot)的方式在服务器端进行分片:Redis集群有16384个哈希槽,使用键的CRC16 编码对16384取模来计算一个键所属的哈希槽

分片缺点

  • 不支持涉及多键的操作,如mget,如果所操作的键都在同一个节点 ,就正常执行,否则会提示错误
  • 分片的粒度是键,因此每个键对应的值不要太大
  • 数据备份会比较麻烦,备份数据时你需要聚合多个实例和主机的持 久化文件
  • 扩容的处理比较麻烦
  • 故障恢复的处理会比较麻烦,可能需要重新梳理Master和Slave的 关系,并调整每个复制集里面的数据

结论:尽量避免去使用分片概念

集群架构

基本概念

由于数据量过大,单个复制集难以承担,因此需要对多个复制集进行集群,形成水平扩展,每个复制集只负责存储整个数据集的一部分,这就是Redis的集群。

分片可以模拟实现集群效果,但是有非常多的缺点,已经不推荐使用
现在Redis支持集群,在不降低性能的情况下,还提供了网络分区后的 可访问性和支持对主数据库故障的恢复。

  • redis集群支持多个Master,每个Master可以挂载多个Slave,便于支持读写分离
  • 集群Cluster内置了高可用的支持,无需再去使用哨兵的功能
  • 集群中所有节点彼此互相连接,内部使用二进制协议(gossip)来进行优化传输速度和带宽
  • 客户端与任一节点直接连接即可访问集群数据
  • 集群负责把插槽分配到各个物理服务节点,由集群来负责维护节点、插槽、数据之间关系

使用集群后,都只能使用默认的0号数据库

每个Redis集群节点需要两个TCP连接打开,正常的TCP端口用来服 务客户端,例如6379,加10000的端口用作数据端口,必须保证防火墙打开这两个端口

Redis集群不保证强一致性,这意味着在特定的条件下,Redis集群 可能会丢掉一些被系统收到的写入请求命令。

手工创建集群

集群的最低标准6个redis实例:3个master节点,每个master带一个slave节点
集群创建的步骤大致 如下:

  • 1 集群配置:将每个数据库的cluster-enabled配置选项打开,然 后再修改如下内容:pidfile、port、logfile、dbfilename、 cluster-config-file
  • 2 启动数据库:分别启动这些Redis数据库,可以用info cluster查看信息
  • 3 连接节点:使用cluster meet,把所有的数据库都放到一个集群中,可以通过cluster info ,或者cluster nodes 查看信息
  • 4 设置slave::设置部分数据库为slave,使用cluster replicate
  • 5 分配插槽:使用cluster addSlots,这个命令目前只能一个一个加,如果要加区间的话,就得客户端编写代码来循环添加。有个实用的技巧:把所有的Redis停下来,然后直接修改node-xxx.conf 的配置文件,只需要配置master的数据库就可以,然后再重启数据库。分配完记得用使用cluster slots命令检查下

最后,通过cluster info查看集群信息,如果显示ok,那就可以使用了

插槽与预分区

插槽

插槽是Redis对Key进行分片的单元。在Redis的集群实现中 ,内置了数据自动分片机制,集群内部会将所有的key映射到16384个插槽中,集群中的每个数据库实例负责其中部分的插槽的读写。

键与插槽的关系:Redis会将key的有效部分,使用CRC16算法计算出散列值, 然后对16384取余数,从而把key分配到插槽中。

键名的有效部分规则是:如果键名包含{},那么有效部分就是{}中的值;否则就是取整个键名

可以看成是集群数据的逻辑划分,也是redis内存的逻辑划分

移动已分配的插槽,这个稍微麻烦点,尤其是有了数据过后,假设要迁移123号插槽从A 到B,大致步骤如下:

  • 1:在B上执行cluster setslot 123 importing A
  • 2:在A上执行cluster setslot 123 migrating B
  • 3:在A上执行cluster getkeysinslot 123 要返回的数量
  • 4:对上一步获取的每个键执行migrate命令,将其从A迁移到B
  • 5:在集群中每个服务器上执行cluster setslot 123 node B (集群其实会自动同步,不过有延迟)

避免键的临时丢失:上面迁移方案中的前两步就是用来避免在移动已分配插槽过 程中,键的临时丢失问题的,大致思路如下

1 当前两步执行完成后,如果客户端向A请求插槽123中的键时,如果 键还未被转移,A将处理请求
2 如果键已经转移,则返回,把新的地址告诉客户端,客户端将发起 新的请求以获取数据

当客户端向某个数据库发起请求时,如果键不在这个数据库里面,将会返回一个move重定向的请求,里面包含新的地址,客户端收到这个信息后,需要重新发起请求到新的地址去获取数据。

当然,大部分的Redis客户端都会自动去重定向,也就是这个过程对开发人员是透明的。redis-cli也支持自动重定向,只需要在启动时加入 -c 的 参数。

预分区

为了实现在线动态扩容和数据分区,Redis的作者提出了预分区的方案,实际就是在同 一台机器上部署多个Redis实例,当容量不够时将多个实例拆分到不同的机器上,这样就达到了扩容的效果。

  • 1:在新机器上启动好对应端口的Redis实例
  • 2:配置新端口为待迁移端口的从库
  • 3:待复制完成,与主库完成同步后,切换所有客户端配置到新的从库的端口
  • 4:配置从库为新的主库
  • 5:移除老的端口实例
  • 6:重复上述过程把要迁移的数据库转移到指定服务器上

以上拆分流程是Redis作者提出的一个平滑迁移的过程,不过该拆分方法还是很依赖Redis本身的 复制功能的,如果主库快照数据文件过大,这个复制的过程也会很久,同时会给主库带来压力。

集群故障恢复

故障判定

  • 集群中每个节点都会定期向其他节点发出ping命令,如果没有收到回复,就 认为该节点为疑似下线,然后在集群中传播该信息
  • 当集群中的某个节点,收到半数以上认为某节点已下线的信息,就会真的标 记该节点为已下线,并在集群中传播该信息
  • 如果已下线的节点是master节点,那就意味着一部分插槽无法写入了
  • 如果集群任意master挂掉,且当前master没有slave,集群进入fail状态
  • 如果集群超过半数以上master挂掉,无论是否有slave,集群进入fail状态
  • 当集群不可用时,所有对集群的操作做都不可用,客户端会收到The cluster is down错误信息

故障恢复

发现某个master下线后,集群会进行故障恢复操作,来将一个slave 变成master,基于Raft算法,大致步骤如下:

  • 1:某个slave向集群中每个节点发送请求,要求选举自己为master
  • 2:如果收到请求的节点没有选举过其他slave,会同意
  • 3:当集群中有超过节点数一半的节点同意该slave的请求,则该Slave选举成功
  • 4:如果有多个slave同时参选,可能会出现没有任何slave当选的情况,将会等 待一个随机时间,再次发出选举请求
  • 5:选举成功后,slave会通过 slaveof no one命令把自己变成master

如果故障后还想集群继续工作,可设置cluster-require-full-coverage为 no,默认yes。

另外要弄清楚master挂掉了,重启还可以加入集群,只是变成Slave;但挂掉的slave重启,如果对应的master变化了,是不能加入集群 的,除非修改它们的配置文件,将其原master指向新master。

只要主从关系建立,就会触发主和该从采用save方式持久化数据, 不论你是否禁止save。

在集群中,如果默认主从关系的主挂了并立即重启,如果主没有做持久化,数据会完全丢失,从而从的数据也被清空。

建议恢复机制交给集群自己去处理,不要使用脚本来控制master的启停,操作不当会导致数据完全丢失

附录一:复制常用配置

关于复制一些基本操作命令:

  • info replication :可以查看复制节点的相关信息(info是一个非常强大的命令,可以查看很多信息比如内存、cpu等)
  • slaveof:可在运行期间修改slave节点的信息,如果该数据库已经是某个主数据库的从数据库,那么会停止和原主数据库的同步关系 ,转而和新的主数据库同步
  • slaveof no one:使当前数据库停止与其他数据库的同步,转成主数据库,不影响它的slave节点
  • replicaof :指定某一个redis作为另一个redis的从服务器,通过指定IP和端口来设置主redis
  • masterauth:如果主redis设置了验证密码的话(使用requirepass来设置),则在从redis的配置中要使用masterauth来设置校验密码,否则的话,主redis会拒绝从redis的访问请求
  • replica-read-only:设置从Redis为只读
  • repl-ping-replica-period:设置从redis会向主redis发出PING包的周期,默认是10秒
  • replica-serve-stale-data:设置当从redis失去了与主redis的连接,或者主从同步正在进行中时,redis该如何处理外部发来的访问请求,默认是yes

如果设置为yes(默认),则从redis仍会继续响应客户端的 请求。如果设置为no,则从redis会对客户端的请求返回“SYNC with master in progress”,当然也有例外,当客户端发来INFO 请求和SLAVEOF请求,从redis还是会进行处理。

  • repl-timeout:设置主从同步的超时时间,要确保这个时限比 repl-ping-replica-period的值要大,否则每次主redis都会认为从redis超时
  • repl-disable-tcp-nodelay:设置在主从同步时是否禁用 TCP_NODELAY,如果开启,那么主redis会使用更少的TCP包和更少的带宽来向从redis传输数据。但是这可能会增加一些同步的延迟 ,大概会达到40毫秒左右。如果关闭,那么数据同步的延迟时间会降低,但是会消耗更多的带宽
  • repl-backlog-size:设置同步队列长度。队列长度(backlog)是 主redis中的一个缓冲区,在与从redis断开连接期间,主redis会用这个缓冲区来缓存应该发给从redis的数据。这样的话,当从redis重新连接上之后,就不必重新全量同步数据,只需要同步这部分增量数据即可。默认是1M,可根据实际业务情况进行调整。
  • repl-backlog-ttl:设置主redis要等待的时间长度,如果主redis 等了这么长时间之后,还是无法连接到从redis,那么缓冲队列中的数据将被清理掉。设置为0,则表示永远不清理。默认是1个小时,应该足够了。
  • replica-priority:设置从redis优先级,在主redis持续工作不 正常的情况,优先级高的从redis将会升级为主redis。而编号越小 ,优先级越高。当优先级被设置为0时,这个从redis将永远也不会 被选中。默认的优先级为100。
  • min-replicas-to-write:设置执行写操作所需的最少从服务器数量,如果至少有这么多个从服务器, 并且这些服务器的延迟值都 少于 min-replicas-max-lag 秒, 那么主服务器就会执行客户端 请求的写操作,该配置保证其高可用性,默认是0
  • min-replicas-max-lag:设置最大连接延迟的时间, min-replicas-to-write和min-replicas-max-lag中有一个被置为0,则这个特性将被关闭。默认min-replicas-max-lag为10
  • repl-diskless-sync:是否开启无盘复制,通过网络完成复制而不是RDB文件,默认是no
  • repl-diskless-sync-delay:设置无盘复制延时开始秒数,默认是5秒 ,意思是当PSYNC触发的时候,master延时多少秒开始向slave传送数 据流,以便等待更多的slave连接可以同时传送数据流,因为一旦 PSYNC开始后,如果有新的slave连接master,只能等待下次PSYNC。可以配置为0取消等待,立即开始
  • repl-diskless-load:是否使用无磁盘加载,这是个实验性的功能,知道下就可以

附录二:哨兵常用配置

  • bind:服务监听地址,用于客户端连接,默认本机地址
  • protected-mode:安全保护模式
  • port:监听的端口号
  • daemonize:是否以后台daemon方式运行
  • pidfile:pid文件位置
  • logfile:log文件位置
  • dir:工作目录
  • sentinel monitor :设置要监控的 master服务器
  • sentinel auth-pass :连接master服务的密码
  • sentinel down-after-milliseconds :指定多少毫秒之后,主节点没有应答哨兵,此时哨兵主观上认为主节点下线
  • sentinel parallel-syncs :表示允许并行同步的 slave个数,当Master挂了后,哨兵会选出新的Master,此时剩余的slave会向新的master发起同步数据
  • sentinel failover-timeout :故障转移的超时时间,进行故障转移时,如果超过设置的毫秒,表示故障转移失败
  • sentinel notification-script :配置当某一事件发生时所需要执行的脚本
  • sentinel client-reconfig-script :客户 端重新配置主节点参数脚本

附录三:常见集群操作命令

  • CLUSTER INFO:获取集群的信息
  • CLUSTER NODES:获取集群当前已知的所有节点,以及这些节点的相关信息
  • CLUSTER MEET :将ip和port所指定的节点添加到集群当中
  • CLUSTER FORGET :从集群中移除 node_id 指定的节点
  • CLUSTER REPLICATE :将当前节点设置为 node_id 指定 的节点的从节点
  • CLUSTER SAVECONFIG:将节点的配置文件保存到硬盘里面
  • CLUSTER ADDSLOTS [slot ...]:将一个或多个槽分配给当 前节点
  • CLUSTER DELSLOTS [slot ...]:从当前节点移除一个或多 个槽
  • CLUSTER FLUSHSLOTS:移除分配给当前节点的所有槽
  • CLUSTER SETSLOT NODE :将槽分配 给 node_id 指定的节点,如果槽已经分配给另一个节点,那么先 让另一个节点删除该槽>,然后再进行分配
  • CLUSTER SETSLOT MIGRATING :将本节点的槽 迁移到指定的节点中
  • CLUSTER SETSLOT IMPORTING :从指定节点导 入槽到本节点
  • CLUSTER SETSLOT STABLE :取消对槽的导入(import)或 迁移(migrate)
  • CLUSTER KEYSLOT :计算键 key 应该被放置在哪个槽
  • CLUSTER COUNTKEYSINSLOT :返回槽目前包含的键值对数 量
  • CLUSTER GETKEYSINSLOT :返回count个槽中的键
  • migrate 目的节点ip 目的节点port 键名 数据库号码 超时时间 [copy] [replace]:迁移某个键值对

附录四:常见集群配置

  • cluster-enabled :是否开启集群模式
  • cluster-config-file: 集群配置文件,由Redis集群节点自动维护每次配置的改变
  • cluster-node-timeout:超时时间,集群节点不可用的最大时间
  • cluster-replica-validity-factor:集群副本有效因子,如果发生故障的Master数据看起来太旧,则其副本将避免进行故障转移
    如果设置为0,则slave将总是尝试故障转移。如果设置为一个正数,那么最大失去连接的时间是node timeout乘以这个factor

  • cluster-migration-barrier:迁移屏障,一个master最少要有多少个slave的数量,才允许数据迁移

  • cluster-require-full-coverage:集群需要全覆盖,故障后是否继续运行
  • cluster-replica-no-failover:集群副本无故障转移,此选项设置为yes时,可防止从设备尝试对其进行故障转移
  • cluster-allow-reads-when-down:是否允许集群在宕机时读取

附录五:使用命令脚本管理集群

Redis Cluster 在5.0之后取消了ruby脚本 redis-trib.rb 的支持,集成到redis-cli里,直接使用redis-clit的参数-- cluster 来取代
查看命令:redis-cli --cluster help

  • create:创建集群,如果指定slave数量,集群会根据redis实例自行划分,但是master和slave配对关系是自由组合的;一般不要指定slave数量,采用add-node方式添加节点,手动指定master和salve的对应关系
  • check:检查集群状态
  • info:查看集群信息
  • fix:修复或恢复集群
  • reshard:重分片,插槽迁移
  • rebalance:平衡集群节点插槽
  • add-node:集群中增加节点,可以指定master和slave对应关系,一般用在手动映射master和slave关系场景中
  • del-node:集群中删除节点
  • call:执行一些命令
  • set-timeout:设置超时时间,单位毫秒
  • import:导入数据到集群里来
  • backup:备份

AMQ简明教程(12)

hzqiuxm阅读(5201)

AMQ集群

Queue consumer clusters

ActiveMQ支持Consumer对消息高可靠性的负载平衡消费,如果一个Consumer死掉,该消息会转发到其它的Consumer消费的Queue上。
如果一个Consumer获得消息比其它Consumer快,那么他将获得更多的消息。
因此推荐ActiveMQ的Broker和Client使用failover://transport的方式来配置链接

Broker clusters

大部情况下是使用一系列的Broker和Client链接到一起。如果一个Broker死掉了,Client可以自动链接到其它Broker上。实现以上行为需要用failover协议作为Client。

如果启动了多个Broker,Client可以使用static discover或者 Dynamic discovery容易的从一个broker到另一个broker直接链接。

这样当一个broker上没有Consumer的话,那么它的消息不会被消费的,然而该broker会通过存储和转发的策略来把该消息发到其它broker上。

特别注意:ActiveMQ默认的两个broker,static链接后是单方向的,broker-A可以访问消费broker-B的消息,如果要支持双向通信,需要在netWorkConnector配置的时候,设置duplex=true 就可以了。

消息会较为平均的分配给2个集群,而不是每个消费者。即使某个消费者集群的消费者比其他集群中多,它获得的消息总数仍然差不多。不适合机器性能不均等的架构。

原因:networkConnector配置的可用属性conduitSubscriptions :默认true,标示是否把同一个broker的多个consumer当做一个来处理

负载均衡的时候一般设置为false, 设置为false后,会按照消费者个数来分配。

Master Slave

在5.9的版本里面,废除了Pure Master Slave的方式,目前支持:

1:Shared File System Master Slave:基于共享储存的Master-Slave:多个broker实例使用一个存储文件,谁拿到文件锁就是master,其他处于待启动状态,如果master挂掉了,某个抢到文件锁的slave变成master

2:JDBC Master Slave:基于JDBC的Master-Slave:使用同一个数据库,拿到LOCK表的写锁的broker成为master

3:Replicated LevelDB Store:基于ZooKeeper复制LevelDB存储的Master-Slave机制,这个是5.9新加的
具体的可以到官方察看: http://activemq.apache.org/masterslave.html

JDBC Master Slave的方式

利用数据库作为数据源,采用Master/Slave模式,其中在启动的时候Master首先获
得独有锁,其它Slaves Broker则等待获取独有锁。
推荐客户端使用Failover来链接Brokers。
具体如下图所示:

Master失败
如果Master失败,则它释放独有锁,其他Slaver则获取独有锁,其它Slaver立即获得独有锁后此时它将变成Master,并且启动所有的传输链接。同时,Client将停止链接之
前的Master和将会轮询链接到其他可以利用的Broker即新Master。如上中图所示

Master重启
任何时候去启动新的Broker,即作为新的Slave来加入集群,如上右图所示

JDBC Master Slave的配置

使用来配置消息的持久化,自动就会使用JDBC MasterSlave的方式。

Cucumber简明教程

hzqiuxm阅读(28142)

Cucumber简明教程

入门篇

简单介绍

  • 用途:BDD(行为驱动开发)自动化测试产品,可以和目前很多语言结合在一起。
  • 有明确的可执行规范,自动化测试,记录系统的实际行为

  • 特点:它使用自然语言来描述测试,使得非程序员可以理解他们
  • 官方安装地址:https://cucumber.io/docs/installation/
  • 依赖包:
dependencies {
    testCompile 'io.cucumber:cucumber-java8:4.3.1'
    testCompile 'io.cucumber:cucumber-junit:4.3.1'
    testCompile 'info.cukes: cucumber-java:1.2.5' //2016年之前的包
}
  • 如果你用的是IEAD可以检查是否自动安装了该插件:Cucumber for Java plugin,如果没有自己手动安装下

Gherkin语法部分

概念介绍

  • Feature:一个测试用例集
  • SCENARIOS:类似一个具体测试用例或场景
  • STEPS:测试步骤,每个SCENARIOS包含多个STEPS,STEPS可以使用如下关键词:Gievn,When,Then,But,And等
  • Given:创建测试环境需要的前提条件
  • When:触发某个业务事件
  • Then:验证事件产生的结果
  • And:多个前提条件时使用,连接前一个条件,作为正向条件
  • But:多个前提条件时使用,连接前一个条件,作为反向条件
  • Background:执行SCENARIOS之前会执行Bankground,在一个Feature里只能有一个,作用上有点SCENARIOS,作为公共的步骤
  • arguments:步骤中传入参数,支持单个参数和复杂参数datatable
  • SCENARIOS Outline:重复场景数据

基本原理

对国家语言支持

  • 支持40多种语言
  • https://github.com/cucumber/cucumber/blob/master/gherkin/gherkin-languages.json

几个feature文件的例子

  • 一个加减法的例子
Feature: Basic Add Test

  Background: give x and y value
    Given x and y value

  Scenario: Addition
    Given x is 4 and y is 5
    When invoke add Method
    Then the result is 9

  Scenario: autoX
    Given x is 1
    When invoke autoX Method
    Then the result is 2

  Scenario: Sub
    And sub operation
    When invoke calculate button
    Then the result is x-y
  • 一个复杂参数例子
Feature: Complex data type

  Scenario: multiple then keywords
    Given the user account infomation
    Then we can found user "hzqiuxm", with password "123456", phone "13989461462"
    Then we can found user "linjiangxian", with password "123456", phone "13989461462"
    Then we can found user "queqiaoxian", with password "123456", phone "13989461462"
  Scenario:
    Given use complex data
    Then 验证下面的一些用户账号信息
      | name    | password | phone |
      | hzqiuxm | 123456   | 13989461462 |
      | linjiangxian | 123456   | 13989461462 |
      | queqiaoxian | 123456   | 13989461462 |

  • 一个公共场景和中文支持的例子
Feature: With Scentrio Outline

  Background: 公共的登录操作
    Given 进行用户登录来测试Scentrio Outline

  Scenario Outline: 用户名或密码错误
    When 使用错误用户名 "<UserName>" 和密码 "<Password>" 来登录
    Then 不正确的用户名或密码

    Examples:
      | UserName | Password |
      | hzqiuxm  | 123321   |
      | simon    | 123321   |

  Scenario: 正确的用户名密码登录测试
    When 使用正确用户名 "hzqiuxm", 密码 "123456" 来登录
    Then 用户名密码正确,登录成功

操作步骤详情部分

Step Definitions

  • 不要定义重复的或模棱两可(正则匹配多个符合)的steps
  • 正则表达式的使用(参数要使用是小括号分组,字符串要用双引号)
  • 想多种情况下匹配又不想抓取参数时,使用?:正则表达式来进行说明
  • DataTable数据格式:dataTable类型,用户自定义类型(https://github.com/cucumber/cucumber/tree/master/datatable),list map类型,list list类型
  • DataTable的compare来做一些结果对比,用于CURD或其它操作结果的一些检测

Tagging

  • 可以按照标签来执行场景用例,使用"@"符号,例子:@v1.0.0 @hzqiuxm
  • 使用~ 取反,放在一个不同""里表示and关系,放在同一个""里表示or关系
tags = {"@v1.0.0","not @santai"}  //执行v1.0.0标签并且不包含santai标签的场景用例

Hooks

  • 类似Junit中的before,after作用,执行顺序:Before Hook, Background,Scenario,After Hook
  • @Before代表之前,@After代表之后
  • 多个Before后After可以使用Order属性值来控制,默认是按照代码中顺序来执行
  • Before和After可以结合tag属性,控制其作用范围,默认情况下是所有feature都有效的,tag属性也只是and和or的关系

Options

  • 作用在启动类上,用来控制测试报告report输出、plugin插件、标签Tag选择、环境配置、Feature文件选择、严格模式等
@CucumberOptions(plugin = {"pretty","json:target/cucumber-report3.json"},tags = {"@v1.0.0","~@santai"})
  • Feature:只执行指定路径下的feature
  • gule:指定feature对应的测试类路径
  • tags:指定执行的标签规范
  • dryRun:不会真正的去执行steps,但会检查哪个feature没被实现
  • strict:严格模式下出现未实现的steps或断言失败就会失败报错
  • monochrome:影响控制台输出的样式效果
  • 支持的输出格式:html:target/Cucumber;json:target_json/Cucumber.json;junit:target_json/Cucumber_junit.xml

第三方整合

  • Jenkins整合,在Jenkins中安装cucumber 插件

自动构建后就会生产相关的报告

  • 结合Assured进行RESTful API测试
http://rest-assured.io/

https://www.baeldung.com/rest-assured-tutorial/

  • 结合selenium进行自动化测试
// https://mvnrepository.com/artifact/org.seleniumhq.selenium/selenium-java
compile group: 'org.seleniumhq.selenium', name: 'selenium-java', version: '3.14.0'

几个gradle下的cucumber插件

  • https://github.com/samueltbrown/gradle-cucumber-plugin
  • https://plugins.gradle.org/plugin/se.thinkcode.cucumber-runner
  • https://plugins.gradle.org/plugin/com.github.spacialcircumstances.gradle-cucumber-reporting
  • https://github.com/awbdallas/gradle-cucumber-jvm-plugin

个人总结

  • cucumber是敏捷开发团队常用的一种测试框架,它鼓励了系统开发环节中各个参与者来进行协作,其中也包括非技术人员
  • cucumber中的测试场景一般由纯自然语言来进行描述,很易懂,因此,非技术人员也可以来编写测试用例,然后通过技术人员来进行实现它。
  • Cucumber可以让人们用近似自然的语言去描述Feature和场景,根据Feature驱动开发。用作软件技术人员和非技术之间验收测试的桥梁。
  • 如果刚开始践行BDD,通常最好让开发人员编写
  • 如果只是用作测试自动化工具,可以由测试人员和开发人员编写
  • 特性(Feature)文件应该描述特性,而不是应用程序的组成部分,每个特性文件应有一个好的命名,并保持特性的专注
  • 避免特性与领域逻辑的不一致性,确保使用客户的领域语言。这一活动的最佳做法是让客户也参与编写故事。
  • 用组织代码的思想来组织你的特性与场景(Scenary)
  • 灵活使用标签Tag
  • 您的方案应该描述系统的预期行为,而不是实现。换句话说,它应该描述什么,而不是如何描述

Redis之路系列应用篇(2)

hzqiuxm阅读(1588)

2 应用篇—纸上得来终觉浅

大量键值对保存

场景案例

有这么一个需求场景,一个图片存储系统,图片的ID刚好可以对应和存储对象ID,所以采用了String类型来保存,键值的长度都是10位数。

刚开始,保存了大约1亿张图片,发现占用内存约6.4G。此时还算正常,当图片数量继续增加到2亿时开始出现由于要生成RDB而导致响应变慢了。
请问如何优化?

首先我们得找到变慢的原因,直接原因是RDB持久化文件太大所致
按道理来说,10位数整型无法存储,使用long型8个字节绝对够了,键值加一起也就16个字节;1个字节1B,16个字节16B;

1亿张图片为16亿B,约等于1.6G,为什么实际情况是6.4G?

那我们不禁要问,理论与实际为何相差这么大?
要回答这个问题,就需要对String的类型做个彻底的解剖。

疑团解析

之前介绍过Redis值中的String类型底层存储结构是简单动态字符串(SDS),其实它的存储结构如下:

比如上图中,key1和key2两个元素,它们都通过a、b、c三个无偏哈希函数进行计算落到不同的位置上,记为1。

当向布隆过滤器询问 key 是否存在时,跟 add 一样,也会把 hash 的几个位置都算出来,看看位数组中这几个位置是否都位 1,只要有一个位为 0,那么说明布隆过滤器中这个 key 不存在。如果都是 1,这并不能说明这个 key 就一定存在,只是极有可能存在,因为这些位被置为 1 可能是因为其它的 key 存在所致(这就是误判)。

如果这个位数组比较大,错误率就会很低,如果这个位数组比较小,错误率就会比较大。当然位数组太大,所需要的hash函数就越多,会影响计算效率的。

我们发现为了存储已用长度、实际分配长度、结束符号信息需要额外的9个字节!比我们实际数据本身还要大。

这还没完,对于 String 类型来说,除了 SDS 的额外开销,还有一个来自于 RedisObject 结构体的开销。

因为 Redis 的数据类型有很多,而且,不同数据类型都有些相同的元数据要记录(比如最后一次访问的时间、被引用的次数等),所以,Redis 会用一个 RedisObject 结构体来统一记录这些元数据,同时指向实际数据。

一个 RedisObject 包含了 8 字节的元数据和一个 8 字节指针,这个指针再进一步指向具体数据类型的实际数据所在,当SDS是Int或Long类型时,指针就不需要了,指针位置可以直接存放数据,节省空间开销。

本场景中采用的是后一种:数值类型的。所以又额外多出来8个字节信息要存储。
但是好像加起来也不到64个字节呀?没错,这还没有完!因为在全局哈希表中,还要存储指针,entry值,链式指针

虽然三个指针只占了24个字节,但是Redis通常会多分配一些内存,一般往上按照2的幂次数取到32字节,以此来减少分配次数。

所以虽然数据本身只占了16个字节,但是还需要额外的48个字节存储其它信息。

这就是1亿个10位键值对真实存储情况。

优化手段

明白了简单动态字符串存储原理后,也理解了为什么1.6G的数据为什么占用了6.4G内存。那能换成哪种结构来节约内存呢?

回顾下存储方式的底层数据结构,我们可以发现有个叫压缩列表的数据结构,它是一种非常节省内存的结构。
表头有三个字段 zlbytes、zltail 和 zllen,分别表示列表长度、列表尾的偏移量,以及列表中的 entry 个数。压缩列表尾还有一个 zlend,表示列表结束。

  • prev_len,表示前一个 entry 的长度。prev_len 有两种取值情况:1 字节或 5 字节。取值 1 字节时,表示上一个 entry 的长度小于 254 字节。虽然 1 字节的值能表示的数值范围是 0 到 255,但是压缩列表中 zlend 的取值默认是 255,因此,就默认用 255 表示整个压缩列表的结束,其他表示长度的地方就不能再用 255 这个值了。所以,当上一个 entry 长度小于 254 字节时,prev_len 取值为 1 字节,否则,就取值为 5 字节。

这些 entry 会挨个儿放置在内存中,不需要再用额外的指针进行连接,这样就可以节省指针所占用的空间。
在上述场景中每个对象大小都是固定的8字节,所以prev_len占1个字节,加上编码方式1字节,自身长度4字节,实际数据8字节,一个entry大约14字节就够了
String的额外开销最大的部分为dictEntry部分,大约占了32个字节,因为每一个键值对entry都对应一个dictEntry。而压缩列表将entey都按顺序放在一起,若干个entry只需要对应一个dictEntry,可以节省下许多额外的空间。

不过redis官方一直在优化各种数据结构的保存机制,不同的版本得到的值可能不太一样,大家千万不要拘泥与值的大小,而是通过存储机制的不同,选择适合的数据结构。这里有一个小工具,可以在线计算各种键值对长度和使用数据类型的内存消耗大小,在实际应用中一定要先测试下所用版本的真实情况,版本不同差别很大。
推荐的神奇小工具:Redis容量预估-极数云舟

海量keys统计

场景1 聚合计算

  • 统计每天的新增用户数和第二天的留存用户数

需要系统中保存所有登录过的用户ID和每一天登录过的用户ID;所有登录过的用户ID可以使用Set来保存,key为user:id,value就是具体的userId值

每天的登录用户也可以使用SET来保存,不过需要有时间戳,可以把时间戳设计在key中,比如key设计为:user:id:20210202就表示2021年2月2号登录的用户ID集合

每天的新增用户,其实就是在当前的用户集合中把存在在累计用户集合中的用户剔除掉,所以只要用日用户Set与累计用户set做差集即可

第二天的留存用户,相当于在前一天的新增用户数还存在于当天的日用户Set中,很明显就是求二者的交集

场景2 排序计算

  • 获取商品评论评论中的最新评论

出现最XXX的字眼时,我们很容易联想到包含了排序,所以使用ZSet方式存储,我们自己来决定每个元素的权重值,比如时间戳先后。

实现起来就很简单了,因为ZSet就是按照权重自动排序的,我们要做的就是使用命令直接来获取即可

比如:ZRANGEBYSCORE comments N-99 N 假设最新评论权重是N,那么这个语句就代表获取最新的100条评论

场景3 二值状态统计

  • 统计一亿用户中连续10天打卡签到的数量

打卡的状态很有限,只有两种情况,要不打了,要不没打,所以我们称之为二值状态的统计问题。
在签到统计时,每个用户一天的签到用 1 个 bit 位就能表示,一年的签到也只需要用 365 个 bit 位,根本不用太复杂的集合类型。

我们就可以选择Redis 提供的扩展数据类型 Bitmap ,它的底层数据结构实现其实也用String类型,我们可以把它看出是一个bit数组。

多少个1,就表示有多少个用户连续10天打卡签到。

有人看到有1亿位的bitmap是不是被吓了一跳,是不是担心内存开销过大,能存的下吗?我们尝试来计算下,1亿除以8转换为字节,然后除以1024转换为KB,再除以1024转换为MB,大约等于12,那么10天的数据也就占了120M的内存,如果再结合TTL的使用,及时释放掉不需要的签到记录,内存压力不算太大。

只要是能够转换为二值状态统计的问题,我们都可以使用bitmap去做,这样可以有效节省内存空间。

场景4 基数统计

  • 统计1000个千万级访问量网页的UV

统计网页的UV我们需要进行去重操作,那么第一反应就是使用Set了,然后使用SCARD命令很方便就得到UV数了。

但是由于网页的访问量实在是打,是千万级别,那么一个Set就要记录千万个用户ID了,这样的页面有1000个,那内存消耗就有点大了。

我们再进一步思考,我们统计UV是否需要特别精准呢?一般是不需要特别精准的,只要差值别超过一个数量级,就有它的统计学意义了。

此时,我们可以使用Redis 提供的 HyperLogLog 了,是一种用于统计基数的数据集合类型,它的最大优势就在于,当集合元素数量非常多时,它计算基数所需的空间总是固定的,而且还很小。

在 Redis 中,每个 HyperLogLog 只需要花费 12 KB 内存,就可以计算接近 2^64 个元素的基数。你看,和元素越多就越耗费内存的 Set 和 Hash 类型相比,HyperLogLog 就非常节省空间。

HyperLogLog 的统计规则是基于概率完成的,所以它给出的统计结果是有一定误差的,标准误算率是 0.81%。这也就意味着,你使用 HyperLogLog 统计的 UV 是 100 万,但实际的 UV 可能是 101 万。
虽然误差率不算大,但是,如果你的业务场景需要精确统计结果的话,最好还是继续用 Set 或 Hash 类型,计算好你需要的内存。

统计计算宝典

该表只是提供一些常规场景的思路和建议,并不能涵盖Redis各种统计场景,大家可以不断更新此表。

大数据过滤

需求场景

某平台需要给用户不断推荐新的新闻内容,它每次推荐时要进行去重,去掉那些已经看过的内容,请问怎么去做?

大部分人的第一反应是通过Set来存储新闻的唯一编号,利用set天然去重。

当然我们的解决方案不可能这么简单,试想下如此多的历史记录全部缓存起来,那得浪费掉多大存储空间呀?且访问是按照线性增长的,时间越久性能就越差,宝贵内存资源浪费也越严重。此时我们就可以使用Redis中的布隆过滤器来解决。

布隆过滤器就是专门用来解决这种去重问题的,可以节约大量的空间,但存在一定的误判。

我们可以把布隆过滤器看成一个不那么精确的set,当它说某个值存在时,可能是不存在的;但是当它说不存在,那肯定是不存在的。用在上面场景里就是:当它说新闻是新的时候,那一定是新的;当它说新闻不是新的时候,有可能是新的,这个结果满足了上述场景要求。

插件安装使用

布隆过滤器是以插件的形式发挥作用的,可以前往Releases · RedisBloom/RedisBloom · GitHub进行下载源码编译安装,遵从以下步骤

  • 解压缩后使用make编译生成动态链接库redisbloom.so
  • 拷贝动态链接库: cp redisbloom.so usr/local/bin/
  • redis.conf配置文件中添加动态链接库:loadmodule redisbloom.so
  • 重启redis

布隆过滤器主要操作:

模拟新闻编号判断:

好像很准确啊,一个也没有误判,那是因为我们数据量太小。根据笔者实验,一般几百数据量的情况下就会出现误判。

另外其实我们是可以通过bf.reserve参数来重新设置过滤器的误判率的:

在我们实际的互联网环境下,有很多技术都利用了布隆过滤器的原理,比如爬虫系统判断URL是否爬过。

还有NOSQL领域的: HBase、Cassandra 还有 LevelDB、RocksDB 内部都有布隆过滤器结构,布隆过滤器可以显著降低数据库的 IO 请求数量。当用户来查询某个 row 时,可以先通过内存中的布隆过滤器过滤掉大量不存在的 row 请求,然后再去磁盘进行查询。

邮箱系统的垃圾邮件过滤功能也普遍用到了布隆过滤器,因为用了这个过滤器,所以平 时也会遇到某些正常的邮件被放进了垃圾邮件目录中,这个就是误判所致,概率很低。

分布式锁设计

基于Redis的分布式锁设计一般分为单机的redis和集群的redis

单个Redis分布式锁

业界通用的设计方案就是利用SETNX 和 DEL 命令组合来实现加锁和释放锁操作。伪代码如下:

// 加锁
SETNX lock_key 1
// 业务逻辑
DO THINGS
// 释放锁
DEL lock_key
//超时释放
Expire lock_key

上述伪代码还存在两个潜在风险:1 业务逻辑发生异常或宕机,导致释放机制失效;2 业务逻辑执行时间过长,锁超时或被其它线程释放了,导致并发问题

第一个风险我们可以加上一个异常捕获,先解决异常情况下的锁释放问题,然后利用redis提供的新方法:

SET key value [EX seconds | PX milliseconds]  [NX]
命令示例:Set lock:bizxxx true ex 5 nx

让redis保证SETNX和Expire指令的原子性;

第二个风险我们可以引入随机数(客户端唯一标识也行),验证随机数保证了锁不会被其它线程释放掉,由于随机数的判断和删除缺乏原子性,我们还需要引入LUA脚本,保证随机数品判断匹配和删除的逻辑的原子性。

整个过程伪代码:

//unlock.script是Lua脚本
redis-cli  --eval  unlock.script lock_key , unique_value 

//释放锁 比较unique_value是否相等,避免误释放
if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

Redis集群分布式锁

当我们要实现高可靠的分布式锁时,就不能只依赖单个的命令操作了,我们需要按照一定的步骤和规则(分布式锁算法)进行加解锁操作,否则,就可能会出现锁无法工作的情况。

Redis 的开发者 Antirez 提出了分布式锁算法 Redlock。

Redlock 算法的基本思路,是让客户端和多个独立的 Redis 实例依次请求加锁,如果客户端能够和半数以上的实例成功地完成加锁操作,那么我们就认为,客户端成功地获得分布式锁了,否则加锁失败。

这样一来,即使有单个 Redis 实例发生故障,因为锁变量在其它实例上也有保存,所以,客户端仍然可以正常地进行锁操作,锁变量并不会丢失。
整个分布式加锁可以分为3个步骤:

  • 1 客户端获取当前时间
  • 2 客户端按顺序依次向 N 个 Redis 实例执行加锁操作
  • 3 一旦客户端完成了和所有 Redis 实例的加锁操作,客户端就要计算整个加锁过程的总耗时

加锁成功要满足两个条件:1 客户端从超过半数(大于等于 N/2+1)的 Redis 实例上成功获取到了锁;2 客户端获取锁的总耗时没有超过锁的有效时间。

在满足了这两个条件后,我们需要重新计算这把锁的有效时间,计算的结果是锁的最初有效时间减去客户端为获取锁的总耗时。如果锁的有效时间已经来不及完成共享数据的操作了,我们可以释放锁,以免出现还没完成数据操作,锁就过期了的情况。

当然不推荐大家去根据算法去实现分布式锁,可以的话还是采用开源或已成熟的解决方案

分布式锁小结

我个人认为Redis的分布式锁是一个比较轻量的解决方案,可以满足我们99%的业务场景,但是如果你的业务要求是99.99%,甚至更高,那么你应该采用其它分布式锁技术比如:zk、etcd等,确保万无一失。

附录一:位数与存储大小计算

要计算多少位数需要多少个字节来保存,我们首先要清楚2的多少次方的计算结果能覆盖我们要保存的数据位数。

比如我们要保存一个10位数,10位数在数值大小上是十亿,我们要保证覆盖最大十亿数,那么到达百亿就能满足了。

根据2的指数运算:

2的34次方的时候达到了百亿,也就是2的34次方对应需要多少个字节呢?我们都知道1个字节占8位,34位大约是4.25个字节。

但是我们要知道在计算机语言中,数据是有类型的,int型最大的能表示的数为2的32次方,也就是十亿级别的,既然int型数据不能满足要求,那就只能采用long类型了

long的类型能表示的数为2的64次方,这个数太大了,足足有20位,满足10位数绰绰有余,而64位,占了8个字节。

附录二:HyperLogLog 原理

HyperLogLog使用非常简单,其实现依据说起来也很简单,但是证明理解起来就没那么简单了,需要用到概率学的知识

先讲下依据,HyperLogLog 是利用概率结果来估算实验次数,是不是看起来有点神奇和懵逼?

举个具体的例子:某天吃完饭,你和你女朋友玩抛硬币游戏,你女朋友负责抛硬币,她抛了的轮数记为n,每一次都会记录正面是在本轮中第K次出现的。然后她告诉你K的最大值,让你猜n的值。
作为理工男的你马上意识到这是个伯努利过程在脑海里进行了概率计算:

算了下kmax在回合出现的概率是(1/2)^k * max,得到: n = 2^k * max,当你女朋友告诉你最大K=3,你胸有成竹的脱口而出:8!

结局是她只抛了1次,于是你输了,负责刷碗。

并不是你的计算不对,而是单次的概率不符合大数定律,而Philippe Flajolet教授吸取了你的教训,引入了桶的概念,再利用调和平均数减少误差。

其中m是桶的数量,const是修正常数,它的取值会根据m而变化。
笔者写了一个简化版测试程序,计算了下误差值,真实算法更复杂,误差值也更低

100000 96673.07 误差:0.03
200000 196276.45 误差:0.02
300000 295135.37 误差:0.02
400000 396655.24 误差:0.01
500000 506963.14 误差:0.01
600000 603743.24 误差:0.01
700000 759724.06 误差:0.09
800000 803806.54 误差:0.00
900000 892418.41 误差:0.01

上面代码用了1024个桶,而在 Redis 的 HyperLogLog 实现中用到的是 16384 个桶,也就是 2^14,每个桶的 maxbits 需要 6 个 bits 来存储,最大可以表示 maxbits=63,于是总共占用内存就是 2^14 * 6 / 8 = 12k字节。

帮助我们理解原理的工具:Sketch of the Day: HyperLogLog — Cornerstone of a Big Data Infrastructure – AK Tech Blog (neustar.biz)

附录三:布隆过滤器原理

每个布隆过滤器对应到 Redis 的数据结构里面就是一个大型的位数组和几个不一样的无偏 hash 函数。所谓无偏就是能够把元素的hash值算得比较均匀。

向布隆过滤器中添加 key 时,会使用多个 hash 函数对 key 进行 hash 算得一个整数索引值然后对位数组长度进行取模运算得到一个位置,每个hash函数都会算得一个不同的位置。再把位数组的这几个位置都置为 1 就完成了 add 操作。

Redis之路系列基础篇(1)

hzqiuxm阅读(1651)

1 基础篇—千里之行始于足下

基于redis6

安装与运行

无论你一名极客还是一名工程师,Redis安装我都推荐源码安装,请前往官方下载地址:http://redis.io/download 进行源码下载,偶数为稳定版 奇数为不稳定版。

如果你是类linux系统,使用wget命令直接远程下载源码 wget https://download.redis.io/releases/redis-{具体版本号}.tar.gz

编译与安装命令:make && make install
默认安装地址:/usr/local/bin,准备好redis.conf配置文件,以下几个配置建议修改:

daemonize yes  后台启动
port xxxx  修改默认端口
requireoass xxxx   添加访问密码
bind 127.0.0.1 如果想其它服务器可以访问,注释掉

服务端启动运行命令: ./redis-server ../conf/redis.conf

服务端停止运行命令:pkill redis-server 或者使用客户端发出关闭命令: ./redis-cli shutdown

客户端链接命令:./redis-cli -h 服务器IP -p 端口 -u 用户 -a 密码,建议更换默认端口,添加访问认证密码

redis版本号查看命令:redis-server -v

redis自带工具集

  • redis-benchmark:性能测试工具,测试redis在你的系统及配置下的读写性能
  • redis-check-aof:用于修复出问题的AOF
  • redis-check-rdb:用于修复出问题的rdb
  • redis-sentinel:redis的集群管理工具

两种线程模型

  • 单线程模型:socket读写、解析数据、执行处理、返回数据等操作都是由一个主线程来完成的。通过对epoll函数的包装来做到。

    单线程原因:瓶颈在内存和网络不在cpu、多线程可能不安全、复杂度增加、线程上下文切换性能损耗等

  • 多线程模型:redis6开始支持I/O多线程,因为之前的瓶颈主要在I/O数据读写性能

高性能根本原因:抽象了一套事件模型,使用多路复用机制(epoll),使得I/O读写都是非阻塞的,从而具备高性能的网络处理能力;同时基于内存进行数据处理。

value存储形式

我们日常中所提到的String(字符串)、List(列表)、Hash(哈希)、Set(集合)和 Sorted Set(有序集合)都只是Redis 键值对中值的数据类型,也就是数据的保存形式。

严格来说并不是Redis数据结构,Redis底层数据结构实现其实一共有6种:分别是简单动态字符串(SDS)、双向链表、压缩列表、哈希表、跳表和整数数组。具体对应关系,后面介绍底层存储结构时会进一步介绍。

先理解和掌握value的5种基本存储形式

String

key是字符串,如果存在空格必须加上双引号,最大的容量是512M

类似Memcache的键值存储,

  • 使用场景:缓存、计数、共享会话、限速

List

底层实现是链表

类似数据结构中的队列,不过支持双向操作。

  • 使用场景:消息队列、文章列表

Hash

按hash的方式存放字符串

相当于关系数据的行数据

  • 使用场景:存储类似于关系数据库的行

Set

是通过hashTable实现的

存储很多数据名单又不重复,判断某个元素是否存在相当方便,聚合运算效率也很高(存放好友,联系人,共同好友)

  • 使用场景:标签,分类,社交

Zet

是通过散列表和跳跃表来实现的

在Set基础上增加了有序的特点,增加了一个double类型的分数作为权重,用于排序

  • 使用场景:排行榜、社交点赞

存储基本结构

整体上看,无论是哪种存储形式,Redis都是键-值的形式,为了实现这种从键到值的快速访问,Redis使用了一个哈希表来保存所有的键值对。

一个哈希表,其实就是一个数组,数组的每个元素称为一个哈希桶。所以,我们常说,一个哈希表是由多个哈希桶组成的,每个哈希桶中保存了键值对数据。

每个键值对包含了键部分和值部分,键部分都是String类型,值部分由大家所熟知的(上面介绍的)5种存储结构组成,而底层用来实现的数据结构有6种,它们之间的对应关系如下:

上面这个哈希表保存了所有的键值对,也称作全局哈希表。当Hash表查询数据的时间复杂度是O(1),操作又是内存级别的,所以数据会非常快。

不过当redis存储的数据越来越大的时候,难免就会产生hash冲突,形成链式hash,hash链上的元素只能逐个查找,时间复杂度是O(n),所以效率就会下降。

当链式hash过程的时候,redis会进行rehash操作,保证元素分布均匀。rehahs的过程是一个渐进式的过程。可以分为3个步骤:
1 先构造一个更大的全局哈希表;
2 映射元素到新的全局哈希表;
3 释放掉旧的hash表。

这里的关键是第2步,redis不是一次性完成所有元素拷贝的,而是在某索引位置发生请求或空闲时,才进行拷贝。

巧妙的把一次性大量拷贝分摊到了多次请求的过程中,既不影响正常请求也能保证数据快速拷贝(后续AOF重写技术也利用了这个设计)。

熟悉了底层数据结构的实现,对我们如何使用redis提供了技术指导,操作的复杂度取决于数据结构的复杂度:

  • 对Hash和Set的单元素操作,其实现是哈希表,复杂度都是O(1),它们现在也支持了多元素操作,M个元素复杂度就是O(M)
  • 对List、Hash、Set遍历操作时其实现是通过链表或数组实现,复杂度一般为O(N),我们尽量避免这种操作
  • 对集合类型元素统计操作(LLEN,SCARD),一般会有单独的字段来存储这个值,所以复杂度为O(1)
  • 特别注意对与压缩列表和双向列表都会记录头尾指针偏移量,对与头尾类操作(LPOP,RPUSH),复杂度为O(1)

通用规则

  • List、Hash、Set、Zset被称作容器型数据
  • 不存在就创建原则:容器型数据不存在,就会自动创建一个,再进行操作
  • 没有就释放原则:容器型数据中如果没有元素了,就删除掉并释放内存
  • 能有过期时间:所有数据结构都可以设置过期时间
  • 过期时间擦除:对有过期时间的字符串进行修改操作会抹除掉过期时间

过期机制

设计理念基于性能与效率的折中,采用定期删除与惰性删除机制

定期删除:Redis会在后台,默认每秒10次的执行如下操作: 随机 选取100个key校验是否过期,如果有25个以上的key过期了,立刻 额外随机选取下100个key(不计算在10次之内)。也就是说,如果过 期的key不多,Redis最多每秒回收200条左右,如果有超过25%的 key过期了,它就会做得更多,这样即使从不被访问的数据,过期 了也会被删除掉。

惰性删除:当client主动访问key时,会先对key进行超时判断,过 时的key会立刻删除
处理过期keys的相关命令:

  • expire:设置过期时间,格式是expire key值 秒数
  • expireat:设置过期时间,格式是expireat key值 到秒的时间戳
  • ttl:查看还有多少秒过期,格式是ttl key值,-1表示永不过期,-2表示已 过期
  • persist:设置成永不过期,格式是persist key值,删除key的过期设置;另 外使用set或者getset命令为键赋值的时候,也会清除键的过期时间
  • pttl:查看还有多少毫秒过期,格式是pttl key值
  • pexpire:设置过期时间,格式是pexpire key值 毫秒数
  • pexpireat:设置过期时间,格式是pexpireat key值 到毫秒的时间戳

附录一:常用命令

1 连接操作命令

  • quit:关闭连接(connection)
  • auth:简单密码认证
  • help cmd: 查看cmd帮助,例如:help quit

2 持久化

  • save:将数据同步保存到磁盘
  • bgsave:将数据异步保存到磁盘
  • lastsave:返回上次成功将数据保存到磁盘的Unix时戳
  • shundown:将数据同步保存到磁盘,然后关闭服务

3 远程服务控制

  • info:提供服务器的信息和统计
  • monitor:实时转储收到的请求
  • slaveof:改变复制策略设置
  • config:在运行时配置Redis服务器

4 对key操作的命令

  • exists(key):确认一个key是否存在
  • del(key):删除一个key
  • type(key):返回值的类型
  • keys(pattern):返回满足给定pattern的所有key(?、*、[]、\x等通配符)
  • randomkey:随机返回key空间的一个
  • keyrename(oldname, newname):重命名key
  • dbsize:返回当前数据库中key的数目
  • expire:设定一个key的活动时间(s)
  • ttl:获得一个key的活动时间
  • select(index):切换到某个数据库(更像是命名空间)
  • move(key, dbindex):移动当前数据库中的key到dbindex数据库
  • flushdb:删除当前选择数据库中的所有key
  • flushall:删除所有数据库中的所有key

5 String

  • set(key, value):给数据库中名称为key的string赋予值value
  • get(key):返回数据库中名称为key的string的value
  • getset(key, value):给名称为key的string赋予上一次的value
  • mget(key1, key2,…, key N):返回库中多个string的value
  • setnx(key, value):添加string,名称为key,值为value
  • setex(key, time, value):向库中添加string,设定过期时间time
  • mset(key N, value N):批量设置多个string的值
  • msetnx(key N, value N):如果所有名称为key i的string都不存在
  • incr(key):名称为key的string增1操作
  • incrby(key, integer):名称为key的string增加integer
  • decr(key):名称为key的string减1操作
  • decrby(key, integer):名称为key的string减少integer
  • append(key, value):名称为key的string的值附加value
  • substr(key, start, end):返回名称为key的string的value的子串

6 List

  • rpush(key, value):在名称为key的list尾添加一个值为value的元素
  • lpush(key, value):在名称为key的list头添加一个值为value的 元素

  • lpushx/rpushx:只有当list存在时才会从左/右边依次追加元素

  • linsert:插入元素,格式是linsert list的key before|after 定 位查找的值 添加的值

  • llen(key):返回名称为key的list的长度
  • lrange(key, start, end):返回名称为key的list中start至end之间的元素
  • ltrim(key, start, end):截取名称为key的list
  • lindex(key, index):返回名称为key的list中index位置的元素
  • lset(key, index, value):给名称为key的list中index位置的元素赋值
  • lrem(key, count, value):删除count个key的list中值为value的元素,0表示全部
  • lpop(key):返回并删除名称为key的list中的首元素
  • rpop(key):返回并删除名称为key的list中的尾元素
  • blpop(key1, key2,… key N, timeout):lpop命令的block版本。
  • brpop(key1, key2,… key N, timeout):rpop的block版本。
  • rpoplpush(srckey, dstkey):返回并删除名称为srckey的list的尾元素,并将该元素添加到名称为dstkey的list的头部

7 Set

  • sadd(key, member):向名称为key的set中添加元素member,member可以有多个

  • smembers(key) :返回名称为key的set的所有元素

  • srem(key, member) :删除名称为key的set中的元素member
  • spop(key) :随机返回并删除名称为key的set中一个元素
  • smove(srckey, dstkey, member) :移到集合元素
  • scard(key) :返回名称为key的set的基数
  • sismember(key, member) :member是否是名称为key的set的元素

  • srandmember(key) :随机返回名称为key的set的一个元素

  • sinter(key1, key2,…key N) :求交集
  • sinterstore(dstkey, (keys)) :求交集并将交集保存到dstkey的集合
  • sunion(key1, (keys)) :求并集
  • sunionstore(dstkey, (keys)) :求并集并将并集保存到dstkey的集合
  • sdiff(key1, (keys)) :求差集,保留key1独有
  • sdiffstore(dstkey, (keys)) :求差集并将差集保存到dstkey的集合

8 Hash

  • hset(key, field, value):向名称为key的hash中添加元素field
  • hget(key, field):返回名称为key的hash中field对应的value
  • hmget(key, (fields)):返回名称为key的hash中field i对应的value
  • hmset(key, (fields)):向名称为key的hash中添加元素field

  • hsetnx:如果项不存在则赋值,存在时什么都不做,格式是 hsetnx Hash的Key 项的key 项的值

  • hexists(key, field):名称为key的hash中是否存在键为field的域
  • hdel(key, field):删除名称为key的hash中键为field的域
  • hlen(key):返回名称为key的hash中元素个数
  • hkeys(key):返回名称为key的hash中所有键
  • hvals(key):返回名称为key的hash中所有键对应的value
  • hgetall(key):返回名称为key的hash中所有的键(field)及其对应的value

  • hincrby(key, field, integer):将名称为key的hash中field的value增加integer

  • hincrbyfloat:增减Float数值,格式是hincrbyfloat Hash的Key 项的key 正负float

9 Zset

  • zadd:添加元素,格式是zadd zset的key score值 项的值,Score 和项可以是多对,score可以是整数,也可以是浮点数,还可以是 +inf表示正无穷大,-inf表示负无穷大
  • zrange:获取索引区间内的元素,格式是zrange zset的key 起始 索引 终止索引 (withscores)
  • zrangebyscore:获取分数区间内的元素,格式是zrangebyscore zset的key 起始score 终止score (withscores),默认是包含端点 值的,如果加上“(”表示不包含;后面还可以加上limit来限制
  • zrem:删除元素,格式是zrem zset的key 项的值,项的值可以是 多个
  • zcard:获取集合中元素个数,格式是zcard zset的key
  • zincrby:增减元素的Score,格式是zincrby zset的key 正负数 字 项的值
  • zcount:获取分数区间内元素个数,格式是zcount zset的key 起 始score 终止score
  • zrank:获取项在zset中的索引,格式是zrank zset的key 项的值
  • zscore:获取元素的分数,格式是zscore zset的key 项的值,返 回项在zset中的score
  • zrevrank:获取项在zset中倒序的索引,格式是zrevrank zset的 key 项的值
  • zrevrange:获取索引区间内的元素,格式是zrevrange zset的 key 起始索引 终止索引 (withscores)
  • zrevrangebyscore:获取分数区间内的元素,格式是 zrevrangebyscore zset的key 终止score 起始score (withscores)
  • zpopmax:从集合中弹出分数最高的成员,返回该成员和分值,然后从集合 中将其移出
  • zpopmin:从集合中弹出分数最低的成员,返回该成员和分值,然后从集合 中将其移出
  • bzpopmax:在参数中的所有集合均为空的情况下,阻塞连接。参数中包含多 个有序集合时,按照参数中key的顺序,返回第一个非空key中分数最大的成 员和对应的分数。参数 timeout 可以理解为客户端被阻塞的最大秒数值,0 表示永久阻塞。
  • bzpopmin:在参数中的所有集合均为空的情况下,阻塞连接。参数中包含多 个有序集合时,按照参数中key的顺序,返回第一个非空key中分数最小的成 员和对应的分数。参数 timeout 可以理解为客户端被阻塞的最大秒数值,0 表示永久阻塞。
  • zremrangebyrank:删除索引区间内的元素,格式是 zremrangebyrank zset的key 起始索引 终止索引
  • zremrangebyscore:删除分数区间内的元素,格式是命令 zset 的key 起始score 终止score
  • zinterstore:交集,格式是ZINTERSTORE dest-key key-count key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX]
  • zunionstore:并集,格式是ZUNIONSTORE dest-key key-count key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX]

10 Sort排序

可以使用sort命令对List、Set、ZSet里面的值进行排序

SORT source-key [BY pattern] [LIMIT offset count] [GET pattern [GET pattern ...]] [ASC|DESC] [ALPHA] [STORE dest-key]

by:设置排序的参考键,可以是字符串类型或者是Hash类型里面的 某个Item键,格式是 Hash键名:->Item键。设置了by参考键, sort将不再依据元素的值来排序,而是对每个元素,使用元素的值 替换参考键中的第一个””,然后获取相应的值,再对获得的值 进行排序。如果参考键不存在,默认为0。如果参考键值一样,再以元素本身的值进行排序。
get:指定sort命令返回结果包含的键的值,形如: Hash键名:*- >Item键,可以指定多个get,返回的时候,一行一个。如果要返回 元素的值,用get #。

  • 对较大数据量进行排序会严重影响性能,所以:1 尽量减少排序集合中数据;2 使用limit限制获取数据量;3 可以考虑使用Store来缓存结果

附录二:对于多快常识

附录三:配置文件

可以在redis-cli里面使用config命令来获取或者设置Redis 配置,这样可以做到不用重新启动Redis来改变配置(仅支持部分配置)。

命令: config get/set 配置名

不支持动态配置的属性有(不是全部):

注意:只推荐在开发和测试环境使用!重启后失效!

配置分类

Redis定义了一些基本的度量单位,只支持bytes,不支持bit,配置对带小写不敏感

Redis配置文件(基于6.0版本)中可以配置的内容根据其作用分,大致包含24种:

这里简单罗列下项目开发中可能经常用到的部分(橙色),红色部分以及相关配置会放在后续专门的主题中来进行介绍,基本可以通过阅读配置文件注释来得到使用方法

网络部分

  • bind:Redis服务监听地址,用于客户端连接,默认本机地址
  • protected-mode :安全保护模式,默认开启,配置bind ip或者设置访问密码访问,关闭后,外部网络可以直接访问
  • port:监听的端口号,默认服务端口是6379(强烈建议更换)
  • tcp-backlog:设置tcp的backlog,backlog其实是一个已经完成三次握手的连接队列,默认是511。在高并发环境下,需要一个高 backlog值来避免慢客户端连接问题。注意Linux内核会将这个值减小到/proc/sys/net/core/somaxconn的值,所以需要确认增大 somaxconn和tcp_max_syn_backlog两个值来达到想要的效果
  • unixsocket:指定 unix socket 的路径
  • unixsocketperm:指定 unix socket file 的权限
  • timeout:连接空闲超时时间,0表示永不关闭,默认值
  • tcp-keepalive:单位为秒,如果设置为0,则不会进行Keepalive 检测,建议设置成60,默认是300

通用部分

  • daemonize:是否以后台daemon方式运行,默认是no
  • supervised:可以通过upstart和systemd管理Redis守护进程,这个参数是和具体的操作系统相关的
  • pidfile:pid进程文件位置,默认会生成在/var/run/redis.pid,启动多个时候配置到不同目录
  • loglevel:log信息级别,共分四级,即debug、verbose、notice 、warning
  • logfile:log文件位置,如果设置为空字符串,则redis会将日志 输出到标准输出。假如你在daemon情况下将日志设置为输出到标准 输出,则日志会被写到/dev/null中
  • syslog-enabled:是否把日志输出到syslog中
  • syslog-ident:指定syslog里的日志标志
  • syslog-facility:指定syslog设备,值可以是USER或LOCAL0- LOCAL7
  • databases:开启数据库的数量,编号从0开始,默认的数据库是编号为0的数据库,可以使用select来选择相应的数据库
  • always-show-logo:是否显示Redis的ASCII艺术logo

安全部分

  • acllog-max-len:ACL日志存储在内存中并消耗内存,设置此项可以设置最大值来回收内存

  • requirepass:设置Redis连接密码

  • rename-command:将命令重命名。为了安全考虑,可以将某些重要的、危险的命令重命名。当你把某个命令重命名成空字符串的时候就等于取消了这个命令

客户端部分

  • maxclients:设置redis同时可以与多少个客户端进行连接。

默认情况下为10000个客户端。当你无法设置进程文件句柄限制时, redis会设置为当前的文件句柄限制值减去32,因为redis会为自身内部处理逻辑留一些句柄出来。

如果达到了此限制,redis则会拒 绝新的连接请求,并且向这些连接请求方发出“max number of clients reached”以作回应。

内存管理部分

  • maxmemory:设置redis可以使用的内存量,最多是物理机的一半。

一旦到达内存使用上限 ,redis将会试图移除内部数据,移除规则可以通过maxmemory-policy来指定。

如果redis无法根据移除规则来移除内存中的数据 ,或者设置了“不允许移除”,那么redis则会针对那些需要申请内存的指令返回错误信息,比如SET、LPUSH等。

但是对于无内存申请的指令,仍然会正常响应,比如GET等。

如果你的redis是主redis(说明你的还有从redis),那么在设置内存使用上限时,需要在系统中留出一些内存空间给同步队列缓存,只有在你设置的是“不移除”的情况下,才不用考虑这个因素。

  • maxmemory-samples:设置样本数量,LRU算法和最小TTL算法都并非是精确的算法,而是估算值,所以你可以设置样本的大小,redis默认会检查这么多个key 并选择其中LRU的那个。

  • maxmemory-policy:设置内存移除规则

注意:无论使用上述哪一种移除规则,如果没有合适的key可以移除的话,redis都 会针对写请求返回错误信息

  • replica-ignore-maxmemory:从 Redis 5 开始,默认情况下,replica 节点会忽略 maxmemory 设置(除非在发生 failover 后,此节点被提升为 master 节点)。

这意味着只有 master 才会执行过期删除策略,并且 master 在删除键之后会对 replica 发送 DEL 命令。

惰性删除部分

  • lazyfree-lazy-eviction:对redis内存使用达到maxmeory,并设置有淘汰策略时,在被动淘汰键时,是否采用lazy free机制。

因为此场景开启lazy free, 可能使用淘汰键的内存释放不及时,导致redis内存超用,超过 maxmemory的限制,一般不启用

  • lazyfree-lazy-expire:对设置有TTL的键,达到过期后,被redis清理删除 时是否采用lazy free机制。此场景建议开启,因TTL本身是自适应调整的速度

  • lazyfree-lazy-server-del:对有些指令在处理已存在的键时,会带有一个 隐式的DEL键的操作。

如rename命令,当目标键已存在,redis会先删除目标键,如果这些目标键是一个big key,那就会引入阻塞删除的性能问题。 此参数设置就是解决这类问题,建议可开启

  • replica-lazy-flush:对slave进行全量数据同步,slave在加载 master的RDB文件前,会运行flushall来清理自己的数据场景,参数设置决定是否采用异常flush机制。

如果内存变动不大,建议可开启。可减少全量同步耗时,从而减少主库因输出缓冲区爆涨引起 的内存使用增长

  • lazyfree-lazy-user-del:修改DEL的默认行为,使得命令的行为 完全像UNLINK

脚本与日志部分

  • lua-time-limit:设置lua脚本的最大运行时间,单位是毫秒,如果此值设置为0或负数,则既不会有报错也不会有时间限制,默认是5秒

  • slowlog-log-slower-than:判断是否慢日志的执行时长,单位是微秒,负数则会禁用慢日志功能,而0则表示强制记录每一个命令

  • slowlog-max-len:慢日志的长度。当一个新的命令被写入日志时 ,最老的一条会从命令日志队列中被移除

监控与事件部分

  • latency-monitor-threshold:能够采样不同的执行路径,来知道 redis阻塞在哪里,这使得调试各种延时问题变得简单,设置一个 毫秒单位的延时阈值来开启延时监控

  • notify-keyspace-events:设置是否开启Pub/Sub 客户端关于键空间发生的事件,有很多通知的事件类型,默认被禁用,因为用户通常不需要该特性,并且该特性会有性能损耗,设置成空字符串就是禁用

工程师工具箱系列(3)

hzqiuxm阅读(1795)

Arthas插件入门

Java诊断利器

安装与准备

  • windows下推荐安装arthas,直接下载jar包
curl -O https://arthas.aliyun.com/arthas-boot.jar  //下载
java -jar arthas-boot.jar  //启动
  • linnx/mac下的推荐安装arthas,使用脚本as.sh脚本
curl -L https://arthas.aliyun.com/install.sh | sh

启动后arthas自动检测可以应用的程序:

要选择应用程序,输入对应的序号就行

dashboard命令示意:

  • 在idea中安装arthas插件:直接在应用市场中搜索arthas即可

使用的基本流程如下:

Arthas插件使用场景

启动arthas并已应用到应用程序

查看某个变量值

  • 静态变量的例子
    假设要获取下面这个static field变量值

首先选中这个变量,右键弹出菜单,然后选择Arthas Command一级菜单,再选择Ognl To Get Static Method Field二级菜单命令

成功后弹出以下窗口,点击copy sc command

获取到一个在命令行执行的命令:sc -d com.wangji92.arthas.plugin.demo.controller.StaticTest
复制到命令行下执行如下:1处是复制来要执行的命令,2处是得到的hashcode

复制hashcode到插件弹窗,填入输入框中,执行前最好先clean cache下,然后点击copy command,得到最终过的查询脚本语句:

ognl -x 3 '@com.wangji92.arthas.plugin.demo.controller.StaticTest@INVOKE_STATIC_NAME' -c 2dd83cd2

复制到命令窗口,输入后执行,得到变量当前的值

ognl方式调用Bean方法

SpringContext也是静态的,所以我们也能通过ognl命令来获取,获得了上下文容器后,可以调用Bean,基本可以为所欲为了。

获取的方式是通过:ApplicationObjectSupport#getApplicationContext

我们来尝试调动下CommonController这个Bean的getRandomInteger方法,拿到classload的hashcode后,采用下面的命令(这是ognl语法糖,不清楚也没关系,后面会介绍如何通过插件来达到同样效果):

ognl -x 3 '#springContext=@com.wangji92.arthas.plugin.demo.common.ApplicationContextProvider@context,#springContext.getBean("commonController").getRandomInteger()' -c 2dd83cd2

看到结果输出了一个随机数453

接下来我们尝试通过插件来完成同样的效果,在这之前,我们要先要把项目的SpringContext配置下
首先代码中要有能获取SpringContext的方法

然后把这个类的路径配置到插件的 Spring static Context Ognl setting
配置内容如下:

@com.wangji92.arthas.plugin.demo.common.ApplicationContextProvider@context

完成后我们选择commonController#getRandomInteger方法,按照下面顺序操作

得到下面弹窗,然后根据步骤1-4分别与命令行交替执行下,如果classloader没变,那直接执行第4步即可

观察第4步获得的命令,就是之前介绍的ognl语法糖

ognl -x 3 '#springContext=@com.wangji92.arthas.plugin.demo.common.ApplicationContextProvider@context,#springContext.getBean("commonController").getRandomInteger()' -c 2dd83cd2

再次执行,同样得到一个随机数,不过这次是240

PS:顺便说一嘴,插件提供了3种方式来调用Bean方法,先有个认知,其它方式具体的操作后续的章节再详细展开,包括带参数的如何调用

1 是通过静态的容器获取Bean进行调用(需要额外获取springContext的实现类比如:
ApplicationContextProvider)

2 是通过Watch方式进行调用(不需要额外实现类,单需要额外触发)

3 是通过tt的方式进行调用(不需要额外实现类,需要额外触发)

tt(TimeTunel)方式调用Bean的方法

同样是选中你要执行的方法,然后在菜单中选择tt方式,弹窗中非常明显的给出了执行的步骤

1 先copy command在后台执行

tt -t org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter invokeHandlerMethod -n 5

2 通过浏览器或Postman工具随便调用个接口额外触发下,后台会捕获请求引用,可以看到命令行后台的刷新了,看到了调用类以及方法名,这是属于第二步的工作

3 执行第三步,copy command得到:

tt -w 'target.getApplicationContext().getBean("commonController").getRandomInteger()' -x 3 -i 1000

粘贴到后台执行,可以看到调用正常,进行多次调用也没问题(watch的方式和tt类似,更加简单点就不演示了)

ognl调用带参数方法

等待被调用的方法,包含两个参数,一个是Integer,一个是List,前者是一个简单对象,后者属于复杂对象

按照示意图使用插件调用

通过插件,我们发现插件也只能提供简单参数的自动填充,而复杂对象需要我们自己去构造(参考对应文档)

完成后的命令如下:

ognl  -x  3  '#user=new com.wangji92.arthas.plugin.demo.controller.User(),#user.setName("wangji"),#user.setAge(27L),@com.wangji92.arthas.plugin.demo.controller.StaticTest@invokeStaticMethodParamObjListUser(0,{#user,#user})' -c 2dd83cd2

拷贝到命令行中执行,可以得到一个调用结果

Arthas本身是一个非常强大的运维类工具,Arthas插件给我们提供了许多的方便,替代了我们去记忆很长的脚本语法。

要想灵活的使用它,你还需要完整的阅读官方文档,去了解下,本文就不做详述了,本文目的是入门,相信你看完后至少应该入门了。

插件支持的命令全景图:

文末是一些参考资料,值得你更加全面和深入的进行学习。

资源总览

  • Arthas文档地址:https://arthas.aliyun.com/doc/
  • B站视频:https://www.bilibili.com/video/BV1K7411Q7mW/
  • 语雀文档:https://www.yuque.com/docs/share/01217521-2fdb-4261-8904-ef6e20d4f5ea?#
  • 示例工程地址:https://github.com/WangJi92/arthas-plugin-demo
  • 扩展阅读博客:http://hengyunabc.github.io/
  • idea插件下载地址:https://plugins.jetbrains.com/plugin/13581-arthas-idea
  • 插件源码地址:https://github.com/WangJi92/arthas-idea-plugin

SpringDataJPA系列(7)

hzqiuxm阅读(1443)

7 Jackson注解在实体中应用

常用的Jackson注解

Springboot中默认集成的是Jackson,我们可以在jackson依赖包下看到Jackson有多个注解

一般常用的有下面这些:

一个实体的示例

测试方法如下:

按照上述图片中的序号做个简单的说明:

  • 1处:指定序列化时候的顺序,先createDate然后是email

  • 2处:将name进行序列化时改成my_name

  • 3处:更新时间序列化时按照给出的格式要求

  • 4处:不序列化sex,报文中并没有出现

  • 5处:不序列化的字段

Jackson在Spring中应用场景

  • SpringMVC的请求参数转换

  • 微服务之间调用报文的转换

  • 将一些数据缓存到redis或其它缓存中时

  • 将调用解耦,使用JMS消息序列化时

工程师工具箱系列(2)

hzqiuxm阅读(2467)

hasor一瞥入门

简介

Hasor有着自己的独立的生命周期与Spring的不同,是一套完整的体系,提供了注入DataQL、Dataway、hasor-web等等,让你的代码无需在写Controller、Service、Dao、BO、VO、mapper等等东西,这是一个数据聚合项目。

Hasor 本身是由多个不同系列框架组合而成的一个框架体系。这些子框架的能力涵盖了 IoC、Aop、WebMVC、数据库以及其它方方面面。这一切的基础要归功于 Hasor 的插件化能力。

Hasor 帮助您设计更好的 API,它独有的框架扩展能力可以使新的能力完全无缝的集成到统一的 API 体系中。我们构建了通用功能,使您能够扩展 Hasor,而不是向核心框架添加每个特性。

Hasor 的扩展能力更像是一个乐高玩具的接口,任何人都可以通过非常简单的方式提供乐高积木,然后轻松的将它们融合到一起。在使用的过程中完全感受不到背后是多个不同的框架在协作。

特点

Hasor 的设计思想是 “微内核+插件”。微内核是指提供少量必要的功能支持,其余功能全部通过插件化方式实现。这样一来扩展 Hasor 就只需要无限制的添加插件而不是修改核心框架。

Hasor 独有的 API 融合机制会,让框架新的能力完全无缝的集成到统一的 API 体系中。下面这张图是 Hasor 的现有框架体系

环境准备

引入依赖

以主流Maven和Springboot工程为例

<!-- dataway -->
<dependency>
    <groupId>net.hasor</groupId>
    <artifactId>hasor-spring</artifactId>
    <version>${hasor.version}</version>
</dependency>
<dependency>
    <groupId>net.hasor</groupId>
    <artifactId>hasor-dataway</artifactId>
    <version>${hasor.version}</version>
</dependency>
<!-- druid 连接池 -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.1.21</version>
</dependency>
<!--mysql 驱动-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.19</version>

数据库脚本

考虑到不同版本字段不同,数据脚本最好从依赖包中获取,目前支持好几种数据库,大家根据数据库类型自选

以下是4.2.5版本脚本

CREATE TABLE interface_info (
  api_id          varchar(64)  NOT NULL COMMENT 'ID',
  api_method      varchar(12)  NOT NULL COMMENT 'HttpMethod:GET、PUT、POST',
  api_path        varchar(512) NOT NULL COMMENT '拦截路径',
  api_status      varchar(4)   NOT NULL COMMENT '状态:-1-删除, 0-草稿,1-发布,2-有变更,3-禁用',
  api_comment     varchar(255) NOT NULL COMMENT '注释',
  api_type        varchar(24)  NOT NULL COMMENT '脚本类型:SQL、DataQL',
  api_script      mediumtext   NOT NULL COMMENT '查询脚本:xxxxxxx',
  api_schema      mediumtext   NOT NULL COMMENT '接口的请求/响应数据结构',
  api_sample      mediumtext   NOT NULL COMMENT '请求/响应/请求头样本数据',
  api_option      mediumtext   NOT NULL COMMENT '扩展配置信息',
  api_create_time varchar(32)  NOT NULL COMMENT '创建时间',
  api_gmt_time    varchar(32)  NOT NULL COMMENT '修改时间',
  PRIMARY KEY (api_id),
  UNIQUE KEY uk_interface_info (api_path)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Dataway 中的API';
CREATE TABLE interface_release (
  pub_id           varchar(64)  NOT NULL COMMENT 'Publish ID',
  pub_api_id       varchar(64)  NOT NULL COMMENT '所属API ID',
  pub_method       varchar(12)  NOT NULL COMMENT 'HttpMethod:GET、PUT、POST',
  pub_path         varchar(512) NOT NULL COMMENT '拦截路径',
  pub_status       varchar(4)   NOT NULL COMMENT '状态:-1-删除, 0-草稿,1-发布,2-有变更,3-禁用',
  pub_comment      varchar(255) NOT NULL COMMENT '注释',
  pub_type         varchar(24)  NOT NULL COMMENT '脚本类型:SQL、DataQL',
  pub_script       mediumtext   NOT NULL COMMENT '查询脚本:xxxxxxx',
  pub_script_ori   mediumtext   NOT NULL COMMENT '原始查询脚本,仅当类型为SQL时不同',
  pub_schema       mediumtext   NOT NULL COMMENT '接口的请求/响应数据结构',
  pub_sample       mediumtext   NOT NULL COMMENT '请求/响应/请求头样本数据',
  pub_option       mediumtext   NOT NULL COMMENT '扩展配置信息',
  pub_release_time varchar(32)  NOT NULL COMMENT '发布时间(下线不更新)',
  PRIMARY KEY (pub_id),
  KEY idx_interface_release_api  (pub_api_id),
  KEY idx_interface_release_path (pub_path)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Dataway API 发布历史。'

文件配置

配置项目工程中properties.yml文件

  # 数据源
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.jdbc.Driver
    druid:
      driver-class-name: com.mysql.cj.jdbc.Driver
      username: root
      password: 123321qq
      url: jdbc:mysql://localhost:3306/mp?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=GMT%2B8&allowMultiQueries=true

# 是否启用 Dataway 功能(必选:默认false)
HASOR_DATAQL_DATAWAY: true
# 是否开启 Dataway 后台管理界面(必选:默认false, 生产环境不要打开!)
HASOR_DATAQL_DATAWAY_ADMIN: true
# SQL执行器方言设置(可选,建议设置)
HASOR_DATAQL_FX_PAGE_DIALECT: mysql
# dataway  API工作路径(可选,默认:/api/)
HASOR_DATAQL_DATAWAY_API_URL: /interface/
# dataway-ui 的工作路径(可选,默认:/interface-ui/)
HASOR_DATAQL_DATAWAY_UI_URL: /config/

Hasor配置

  • 配置启动类,需要加上二个注解:@EnableHasor,@EnableHasorWeb

  • 配置Module接管Spring容器和数据源

这里要注意,V4.2.2开始,Dataway前后端不再处理任何编码,需要自己指定请求和响应的编码,否则可能出现中文乱码

运行测试

运行项目Application启动类,启动过程会出现Hasor boot的样式,说明集成成功了

启动成功后,打开你的本地地址:http://127.0.0.1:10086/config

一开始整个页面是空的,你可以点击new操作按钮来添加你的接口
整个页面的布局分为三大块空间:

  • 1号区域:API列表区
  • 2号区域:调用接口参数配置区,以及相关操作
  • 3号区域:响应结果区

【示例】新增一个查询接口,查询user表里name包含某个字的人
1号区域中写下SQL语句:select * from user where `name` like concat('%',#{userName},'%')

2号区域中写下查询条件示例:{"userName":"567"},点击执行后

3号区域中出现响应结果:

如果接口没问题,你进行保存后再执行冒烟测试(从数据库取出示例数据进行测试),通过后你就可以发布了,发布后会在interface_release表新增一条数据

注意:同一个接口可以被发布多次,只有发布过的接口才能被访问

这里为了演示简单,采用的是常用的sql语句方式,你可以使用功能更加强大的DataQL,编写语法稍微会复杂写。不过可以支持函数式方式编写任何你想要的逻辑,格式化你需要的返回数据。

使用Postman进行调用演示:

小结

hasor的一些设计思想还是值得我们借鉴的,特别是对轻业务逻辑的系统非常适用。还有与Spring如何集成的一些思想和方法也具有很高的参考的价值。

我们可以适当加以改造实现接口配置化,同时把加入权限功能,可以解放企业中非业务层重复劳动。特别是一些报表和查询类的系统,真的是非常灵活,可以解放好多的生产力。

推荐阅读:https://www.hasor.net/doc/

欢迎加入极客江湖

进入江湖关于作者