强大的缓存数据库——Redis体验与详解
1. Redis介绍和安装
1.1 简介
- Redis是完全开源免费的,遵循BSD协议, 是一个高性能(NOSQL)的Key-Value数据库.
- Redis是一个开源的使用ANSI C语言编写,支持网络,可基于内存亦可持久化的日志型,Key-Value数据库,并支持多种语言的API
- 从2010年3月15日起, Redis的开发工作由VMware主持
- 从2013年5月开始, Redis的开发由Pivotal赞助 (VMware在资助redis项目的开发和维护)
1.2 NoSQL 介绍
NoSQL泛指非关系型数据库, 随着互联网兴起 ,传统关系型数据库已经显得力不从心,暴露了很多难以解决的问题, 而非关系型数据库则由于其本身的特点得到了 非常迅速的发展, NoSQL数据库产生就是为了解决大规模数据集合多重数据种类带来的挑战, 尤其是大数据应用难题
SQL和NoSQL区别
- SQL: 关系型数据库, 表与表之间可以有关联关系
- NoSQL: 非关系型数据库, 数据和数据之间没有关联关系
NoSQL数据库的四大分类
- 键值(Key-Value)存储数据库
这类数据库主要使用一个哈希表,这个表中有特定的键和一个指针指向特定的数据 .key-value模型对于IT系统来说 优势在于简单, 容易部署.例如: ==Redis==, Oracle BDB - 列存储数据库
这部分数据库通常是用来应对分布式存储的海量数据,键依然存在,但是它们的特点是指向了多个列,这些列是由列家族来安排的,例如:==HBase== , Riak - 文档型数据库
它与键值存储方式类似 , 该类型和数据模板是版本化的文档, 半结构化的文档以特定的格式存储, 如JSON,文档型数据库可以看作是键值数据库的升级版,允许之间嵌套键值, 而且文档数据库比简直数据库的查询效率更高 ,例如: CouchDB , ==MongoDB== - 图形(Graph)数据库
图形结构的数据库使用灵活的图形模型,并且能扩展到多个服务器, 如: ==Neo4J==
NoSQL应用场景
- 数据模型比较简单
- 需要灵活更强的IT系统
- 对数据库性能要求较高
- 不需要高度的数据一致性
- 对于给定key, 比较容易映射复杂值得环境
Redis相较其他Key-Value缓存产品的优势
- Redis支持数据的持久化, 可以将内存中的数据保存在磁盘上, 重启的使用可以再次加载进行使用
- Redis不仅仅支持简单的key-value类型的数据, 同时还提供了list ,set, zset, hash等5种数据类型的存储
- Redis支持数据的备份 ,集群等高可用功能
1.3 特点
- 性能极高: redis的读: 11_0000次/s 写: 8_0000次/s
- 丰富的数据类型: Redis支持String , List , Hash , Set , Ordered Set数据类型操作
- 原子: Redis的所有操作都是原子性的, 要么成功执行,要么失败,没有执行.
单个操作是原子性的, 多个操作也支持事务, 即原子性, 通过MULTI和EXEC指令包起来 - 丰富的特性: Redis还支持pub/sub模式,通知 , key过期等特性
1.4 Redis总结
Redis是一个简单的,高效的,分布式的, 基于内存的缓存工具
架设好服务器后, 通过网络连接(类似数据库连接) ,提供key-value式缓存服务
简单是redis突出的特色 , 可以保证核心功能的稳定和优异
Redis单个key存入512M大小
redis支持多种类型的数据结构(String ,lsit,set,zset,hash)
redis是单线程 原子性
redis可以持久化, 因为使用了RDB和AOF机制
redis支持集群,而且redis支持库(0-15) 16个库
redis还可以做消息队列, 比如聊天室 , IM
在企业开发中, 可以用作数据库 ,缓存(热点数据, 读多写少), 和消息中间件等
优点:
- 丰富的数据类型
- 高速读写,redis使用自己实现的分离器, 代码量很短, 没有使用lock,因此效率极高
缺点:
- 持久化 . Redis直接将数据存储到内存中, 要将数据保存在磁盘上, Redis可以使用两种方式实现持久化过程,
- 定时快照RDB: 每个一段时间将整个数据库写到磁盘上,每次均是写全部数据, 代价非常高
- 基于语句追加AOF: 只追踪变化的数据, 但是追加的log可能过大, 同时所有的操作均重新执行一遍, 恢复速度慢
- 耗内存,占用内存过高
1.5 Redis安装
windows安装
https://jingyan.baidu.com/article/0f5fb099045b056d8334ea97.html
Linux安装(CentOS)
安装gcc
Redis是C语言开发,安装Redis需要先将官网下载的源码进行编译,编译依赖gcc环境,如果没有gcc环境,需要安装gcc
yum -y install gcc automake autoconf libtool make
注意:运行yum时出现/var/run/yum.pid已被锁定,PID为xxxx的另一个程序正在运行的问题解决
rm -f /var/run/yum.pid
安装Redis
命令1: wget http://download.redis.io/releases/redis-4.0.1.tar.gz
命令2:tar zxvf redis-4.0.1.tar.gz
命令3: cd redis-4.0.1
命令4(编译): make 或 make MALLOC=libc 如下图代表成功:
命令5:make PREFIX=/usr/local/redis install
(安装编译后的文件) 安装到指目录:
注意:PREFIX必须大写、同时会自动为我们创建redis目录,并将结果安装此目录
命令6: cd /usr/local/redis
查看
命令7:查看bin目录下,如图:
启动redis
进入对应的安装目录 /usr/local/redis
执行命令: ./bin/redis-server
启动Redis客户端
进入Redis客服端(Clone Session克隆一个窗口):
进入对应的安装目录 cd /usr/local/redis
执行命令: ./bin/redis-cli
启动Redis 客户端命令:
redis-cli –h IP地址 –p 端口
退出客户端命令:Ctrl+C
2. 配置文件详解和常用命令
Linux配置Redis
Redis 的配置文件位于 Redis 安装目录下,文件名为 redis.conf
(Windows 名为 redis.windows.conf)。
Redis端口号或启动有默认配置。但一般我们都会通过手动配置完成
回到根目录找到解压文件中的reids.conf
命令:cp redis.conf /usr/local/redis 将配置文件复制到安装文件的目录下
至此,Redis配置全部完成
Redis.conf配置文件详解
-
Redis默认不是以守护进程的方式运行,可以通过该配置项修改,使用yes启用守护进程
daemonize no -
当Redis以守护进程方式运行时,Redis默认会把pid写入/var/run/redis.pid文件,可以通过pidfile指定
pidfile /var/run/redis.pid -
指定Redis监听端口,默认端口为6379,为什么选用6379作为默认端口,因为6379在手机按键上MERZ对应的号码,而MERZ取自意大利歌女Alessia Merz的名字
port 6379 -
绑定的主机地址
bind 127.0.0.1
5.当 客户端闲置多长时间后关闭连接,如果指定为0,表示关闭该功能
timeout 300 -
指定日志记录级别,Redis总共支持四个级别:debug、verbose、notice、warning,默认为verbose
loglevel verbose -
日志记录方式,默认为标准输出,如果配置Redis为守护进程方式运行,而这里又配置为日志记录方式为标准输出,则日志将会发送给/dev/null
logfile stdout -
设置数据库的数量,默认数据库为0,可以使用SELECT
命令在连接上指定数据库id
databases 16 -
指定在多长时间内,有多少次更新操作,就将数据同步到数据文件,可以多个条件配合
==save \<seconds> \<changes>==
Redis默认配置文件中提供了三个条件:
save 900 1
save 300 10
save 60 10000
分别表示900秒(15分钟)内有1个更改,300秒(5分钟)内有10个更改以及60秒内有10000个更改。 -
指定存储至本地数据库时是否压缩数据,默认为yes,Redis采用LZF压缩,如果为了节省CPU时间,可以关闭该选项,但会导致数据库文件变的巨大
rdbcompression yes -
指定本地数据库文件名,默认值为==dump.rdb==
dbfilename dump.rdb -
指定本地数据库存放目录
dir ./ -
设置当本机为slav服务时,设置master服务的IP地址及端口,在Redis启动时,它会自动从master进行数据同步
slaveof \<masterip> \<masterport> -
当master服务设置了密码保护时,slave服务连接master的密码
masterauth \<master-password> -
==设置Redis连接密码,如果配置了连接密码,客户端在连接Redis时需要通过AUTH \<password>命令提供密码,默认关闭==
requirepass foobared -
设置同一时间最大客户端连接数,默认无限制,Redis可以同时打开的客户端连接数为Redis进程可以打开的最大文件描述符数,如果设置 maxclients 0,表示不作限制。当客户端连接数到达限制时,Redis会关闭新的连接并向客户端返回max number of clients reached错误信息
maxclients 128 -
指定Redis最大内存限制,Redis在启动时会把数据加载到内存中,达到最大内存后,Redis会先尝试清除已到期或即将到期的Key,当此方法处理 后,仍然到达最大内存设置,将无法再进行写入操作,但仍然可以进行读取操作。Redis新的vm机制,会把Key存放内存,Value会存放在swap区
==maxmemory \<bytes>== -
指定是否在每次更新操作后进行日志记录,Redis在默认情况下是异步的把数据写入磁盘,如果不开启,可能会在断电时导致一段时间内的数据丢失。因为 redis本身同步数据文件是按上面save条件来同步的,所以有的数据会在一段时间内只存在于内存中。默认为no
==appendonly no== -
指定更新日志文件名,默认为appendonly.aof
appendfilename appendonly.aof -
指定更新日志条件,共有3个可选值:
no:表示等操作系统进行数据缓存同步到磁盘(快)
always:表示每次更新操作后手动调用fsync()将数据写到磁盘(慢,安全)
everysec:表示每秒同步一次(折中,默认值)
appendfsync everysec -
指定是否启用虚拟内存机制,默认值为no,简单的介绍一下,VM机制将数据分页存放,由Redis将访问量较少的页即冷数据swap到磁盘上,访问多的页面由磁盘自动换出到内存中(在后面的文章我会仔细分析Redis的VM机制)
vm-enabled no -
虚拟内存文件路径,默认值为/tmp/redis.swap,不可多个Redis实例共享
vm-swap-file /tmp/redis.swap -
将所有大于vm-max-memory的数据存入虚拟内存,无论vm-max-memory设置多小,所有索引数据都是内存存储的(Redis的索引数据 就是keys),也就是说,当vm-max-memory设置为0的时候,其实是所有value都存在于磁盘。默认值为0
vm-max-memory 0 -
Redis swap文件分成了很多的page,一个对象可以保存在多个page上面,但一个page上不能被多个对象共享,vm-page-size是要根据存储的 数据大小来设定的,作者建议如果存储很多小对象,page大小最好设置为32或者64bytes;如果存储很大大对象,则可以使用更大的page,如果不 确定,就使用默认值
vm-page-size 32 -
设置swap文件中的page数量,由于页表(一种表示页面空闲或使用的bitmap)是在放在内存中的,,在磁盘上每8个pages将消耗1byte的内存。
vm-pages 134217728 -
设置访问swap文件的线程数,最好不要超过机器的核数,如果设置为0,那么所有对swap文件的操作都是串行的,可能会造成比较长时间的延迟。默认值为4
vm-max-threads 4 -
设置在向客户端应答时,是否把较小的包合并为一个包发送,默认为开启
glueoutputbuf yes -
指定在超过一定的数量或者最大的元素超过某一临界值时,采用一种特殊的哈希算法
hash-max-zipmap-entries 64
hash-max-zipmap-value 512 -
指定是否激活重置哈希,默认为开启(后面在介绍Redis的哈希算法时具体介绍)
activerehashing yes -
指定包含其它的配置文件,可以在同一主机上多个Redis实例之间使用同一份配置文件,而同时各个实例又拥有自己的特定配置文件
include /path/to/local.conf
Redis中的内存维护策略
redis作为优秀的中间缓存件,时常会存储大量的数据,即使采取了集群部署来动态扩容,也应该即使的整理内存,维持系统性能。
在redis中有两种解决方案,
一 , 为数据设置超时时间,
二 , 采用LRU算法动态将不用的数据删除。内存管理的一种页面置换算法,对于在内存中但又不用的数据块(内存块)叫做LRU,操作系统会根据哪些数据属于LRU而将其移出内存而腾出空间来加载另外的数据。
-
volatile-lru:设定超时时间的数据中,删除最不常使用的数据.
-
==allkeys-lru==:查询所有的key中最近最不常使用的数据进行删除,这是应用最广泛的策略.
-
volatile-random:在已经设定了超时的数据中随机删除.
-
allkeys-random:查询所有的key,之后随机删除.
-
volatile-ttl:查询全部设定超时时间的数据,之后排序,将马上将要过期的数据进行删除操作.
-
noeviction:如果设置为该属性,则不会进行删除操作,如果内存溢出则报错返回.
volatile-lfu:从所有配置了过期时间的键中驱逐使用频率最少的键
allkeys-lfu:从所有键中驱逐使用频率最少的键
https://www.jianshu.com/p/c8aeb3eee6bc
自定义配置Redis
1、进入对应的安装目录 /usr/local/redis
修改 redis.conf 配置文件 vim redis.conf (进入命令模式 通过/内容 查找相应字符串)
2、Redis配置默认必须修改:
daemonize no 修改为 ==daemonize yes== 后台开启线程(设置为守护线程)
将==#bind 127.0.0.1== 注释掉 方便后续的其他设备的连接
==requirepass ryujung== 设置密码 (设置redis的认证密码为ryujung)
Redis采用的是单进程多线程的模式。当redis.conf中选项daemonize设置成yes时,代表开启守护进程模式。在该模式下,redis会在后台运行,并将进程pid号写入至redis.conf选项pidfile设置的文件中,此时redis将一直运行,除非手动kill该进程。但当daemonize选项设置成no时,当前界面将进入redis的命令行界面,exit强制退出或者关闭连接工具(putty,xshell等)都会导致redis进程退出。 服务端开发的大部分应用都是采用后台运行的模式
Redis启动
指定使用的配置文件启动服务端:
./bin/redis-server ./redis.conf
客户端启动:
- 本地客户端登录(带密码)
用redis-cli 密码登陆(redis-cli -a password)
远程服务上执行命令
如果需要在远程 redis 服务上执行命令,同样我们使用的也是 redis-cli 命令。
语法:
redis-cli -h host -p port -a password
redis-cli –h IP地址 –p 端口 –a 密码
Redis关闭
-
第一种关闭方式:(断电、非正常关闭。容易数据丢失)
查询PIDps -ef | grep -i redis
kill -9 PID -
第二种关闭方式(正常关闭、数据保存)
./bin/redis-cli shutdown
关闭redis服务,通过客户端进行shutdown如果redis设置了密码,需要先在客户端通过密码登录,再进行shutdown即可关闭服务端
./bin/redis-cli -a ryujung shutdown
Redis常用命令
DEL key
在key存在时删除key,返回删除的记录数
DUMP key
序列化给定的key , 并的返回被序列化的值
EXISTS key
检查给定的key是否存在
EXPIRE key seconds
为给定的key设置10s过期时间(秒为单位)
EXPIRE key milliseconds
设置key的过期时间以毫秒计
ttl key
查看指定key的存活时间(-1代表永久,默认都设置为永久)-2代表当前key无效
PTTL key
以毫秒为单位返回key 的剩余过期时间
PERSIST key
移除key 的过期时间(key必须尚未过期), 使其变成永久可以
KEYS pattern
查找所有符合给定模式的key,其中 * 代表任意字符 , ? 代表一个字符
Randomkey
随机返回一个key
RENAME key newkey
将指定的key改名为newkey值
MOVE key db
将指定的key转移到指定的数据库中
TYPE b
返回指定key的数据类型
WATCH key [key ...]
标记所有指定的key 被监视起来,在事务中有条件的执行(乐观锁)。
配置相关的指令
-
BGREWRITEAOF
RedisBGREWRITEAOF
命令用于异步执行一个 AOF(AppendOnly File)文件重写操作。重写会创建一个当前AOF文件的体积优化版本。即使BGREWRITEAOF
执行失败,也不会有任何数据丢失,因为旧的AOF文件在BGREWRITEAOF
成功之前不会被修改。
AOF 重写由 Redis 自行触发,BGREWRITEAOF
仅仅用于手动触发重写操作。127.0.0.1:6379> bgrewriteaof Background append only file rewriting started
-
INFO
该命令以一种易于理解和阅读的格式,返回关于Redis服务器的各种信息和统计数值。
通过给定可选的参数 section ,可以让命令只返回某一部分的信息:server
: Redis服务器的一般信息clients
: 客户端的连接部分memory
: 内存消耗相关信息persistence
: RDB和AOF相关信息stats
: 一般统计replication
: 主/从复制信息cpu
: 统计CPU的消耗commandstats
: Redis命令统计cluster
: Redis集群信息
-
keyspace
: 数据库的相关统计 -
CONFIG SET
命令用于在服务器运行期间重写某些配置,而不用重启Redis。你可以使用此命令更改不重要的参数或从一个参数切换到另一个持久性选项。仅在服务启动时生效,不会修改配置文件内容,重启失效。
可以通过CONFIG GET *
获得CONFIG SET
命令支持的配置参数列表,该命令是用于获取有关正在运行的Redis实例的配置信息的对称命令。
所有使用CONFIG SET
设置的配置参数将会立即被Redis加载,并从下一个执行的命令开始生效。127.0.0.1:6379> config set aof-use-rdb-preamble yes ##将配置中的aof持久化方式设置使用rdb前缀(混合持久化方式方式) OK
-
BGSAVE
后台保存DB。会立即返回 OK 状态码。 Redis forks, 父进程继续提供服务以供客户端调用,子进程将DB数据保存到磁盘然后退出。如果操作成功,可以通过客户端命令LASTSAVE来检查操作结果。在默认情况下, Redis 将数据库快照保存在名字为 dump.rdb的二进制文件中。你可以对 Redis 进行设置, 让它在“ N 秒内数据集至少有 M 个改动”这一条件被满足时, 自动保存一次数据集。你也可以通过调用
SAVE
或者BGSAVE
, 手动让 Redis 进行数据集保存操作。 -
LASTSAVE
执行成功时返回UNIX时间戳。客户端执行 BGSAVE 命令时,可以通过每N秒发送一个LASTSAVE
命令来查看BGSAVE 命令执行的结果,由LASTSAVE
返回结果的变化可以判断执行结果。
应用场景
EXPIRE key SECONDS
- 限时的优惠活动信息
- 网站数据缓存(定时更新的数据,例如积分排行榜)
- 手机验证码
- 限制网站访客访问频率
INCR key_name
-
string通常用于保存单个字符串或JSON字符串数据
-
因string时二进制安全的, 所以可以吧一个图片文件的内容作为字符串来存储
-
计数器(常规key-value缓存应用, 常规计数 ,微博数, 粉丝数)
INCR等指令本事就具有原子操作的特性, 所以可以利用redis的INCR,INCRBY,DECR,DECRBY等指令来实现原子技术的效果, 不存在并发安全问题
key的命名建议
redis 单个key存入512M大小
- key不要太长,尽量不要超过1024字节,不仅消耗内存, 而且还会降低查询效率
- key也不要太短, 保持一定的可读性
- 在一个项目中, key最好使用统一的命名模式, 例如
user:123:password
使用冒号连接而不使用下划线是为了避免和sql数据库中的数据名称中的下划线混淆
3. Docker安装Redis
下载镜像文件
docker pull redis:6.2.6
创建实例并启动
docker run -p 6379:6379 --name redis -v /mydata/redis/data:/data -v /mydata/redis/redis.conf:/etc/redis/redis.conf -d redis:6.2.6 redis-server --appendonly yes
使用redis镜像执行redis-cli命令连接
docker exec -it redis redis-cli
4. Redis常用数据类型和应用场景
4.1 字符串String
简介
- string时redis中的最基本的数据类型, 一个key对应一个value
- string类型时而进行安全的, 意思是redis的string可以包含任何数据, 比如jpg图片或者序列化的对象
- string类型是redis最基本的数据类型,一个键最大存储512MB
二级制安全
二级制安全是指, 在传输数据时, 保证二进制数据的信息安全,数据严格按照二进制的数据读取,也就是不被篡改,破译等, 如果被攻击, 能够及时检测出来
特点:
- 编码,解码发生在客户端完成, 执行效率高
- 不需要频繁的编解码, 不会出现乱码
常用命令
set key_name value
用于设置给定key的值 ,如果key已经存储值, set就会覆写旧制, 且无视类型, 参数:EX SECONDS
设置过期时间,NX
当key不存在时才进行赋值SETNX key value
如果当前key不存在, 则设置key并赋值,该命令时解决分布式锁的解决方案之一GET key_name
该命令用于获取指定key的值, 如果key不存在则返回nil ,如果存储的不是字符串类型,返回一个错误GETRANGE key start end
获取存储在指定key中字符串的子串, 截取的范围是start和end 两个偏移量,包含start和end , 注意start从0开始,类似java数据的索引GETBIT key offset
对key所存储的GETSET key_name value
设置指定的key 值,并返回key的旧值, 当key不存在时, 返回nilSTRLEN key
返回key所存储的 字符串值得长度DEL key_name
删除指定得key ,如果存在,返回删除的条目数INCR key_name
将key中存储的数字值增1, 如果key不存在, 那么key的值会先被初始化为0 , 然后再执行INCR操作INCRBY key_name increment
将key中存储的数字加上指定的增量之incrementDECR key_name
将key中的值自减1 ,如果key不存在, 那么key值会被先被初始化为0 , 然后再执行DECR 操作DECRBY key_name decrement
将key中存储的数字减去指定的偏移量decrementAPPEND key_name value
字符串拼接
4.2 哈希(Hash)
基本使用语法
赋值操作
HSET key field value
为指定的key,设置field和valueHMSET key field value [field1 value1 ...]
为指定的key,设置field和value ,可以同时为多个field-value(域-值)对设置到哈希表key中
读取操作
HGET key field
获取存储再hash中的值, 根据field得到valueHMGET key field [field1 field2 ...]
HGETALL key
返回hash表中所有的字段的值HKEYS key
获取所有哈希表中的字段HLEN key
获取哈希表中字段的数量
删除操作
HDEL key field [field2]
删除一个或多个HASH字段
其他操作
HSETNX key field value
只有再字段field不存在时,才进行赋值HINCRBY key field increment
为哈希表key中指定的字段加上增量incrementHINCRBYFLOAT key field increment
为哈希表key 中的指定字段的浮点数加一个增量incrementHEXISTS key field
查看哈希表key中,指定的字段是否存在
应用场景
- 常用于存储一个对象
- 为什么不用string存储一个对象?
hash时最接近关系型数据库的数据类型, 可以将数据库中一条记录或程序中一个对象转换为hashmap存放再redis中.
用户ID为查找的key,存储的value用户对象包含姓名, 年龄,生日等字段, 如果要使用普通的存储方式,主要有两种:
第一种, 将用户ID作为查找key ,把其他信息封装成一个对象以序列化的方式存储, 这种方式的缺点是, 增加了序列化 /反序列化的开销, 并且在需要修改其中一项 信息时, 需要将整个对象取回, 并且修改操作需要对并发进行保护 ,引入CAS等复杂问题
第二种 ,是将这个用户信息对象有多少成员就存成多少个key-value对, 用用户ID+对应的属性名称作为唯一标识, 来取得对应属性的值, 虽然省区了序列化开销和并发问题, 但是用户ID为重复存储, 如果存在大量的这种数据, 内存浪费还是非常严重的
总结
redis提供的hash很好的解决 了这个问题, redis 的hash实际是内部存储的value为一个hashmap,并提供了 直接存取这个map的成员接口
Java连接Redis
- 导入maven依赖
<!-- 引入jedis客户端来连接redis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.1.0</version>
</dependency>
- 编写测试代码
class JedisDemoForRedisApplicationTests {
private static final String HOST = "192.168.0.101";
private static final int PORT = 6379;
Jedis jedis;
@BeforeEach
public void init() {
jedis = new Jedis(HOST, PORT);
jedis.auth("ryujung");
System.out.println(jedis.ping());
}
@AfterEach
void destroy() {
if (jedis!=null) {
jedis.close();
}
}
/**
* redis有哪些命令, jedis就有哪些方法
*/
@Test //测试string类型
void test1() {
// jedis.set("strName","李狗蛋");
String res = jedis.get("strName");
System.out.println("redis中key为strName的值为: "+res);
}
/**
* redis作用: 为了减轻数据库(MySQL)的访问压力
* 需求: 判断某key 是否存在,存在就从redis中获取,
* 不存在就查询数据库并存放进redis 中
*/
@Test
void test2() {
String key = "applicationName";
if(jedis.exists(key)) {
System.out.println("redis中存在,将直接返回:"+jedis.get(key));
}else {
String res = "微信下棋小程序";//模拟从数据库中获取的数据
jedis.set(key,res);
System.out.println("redis中不存在, 从数据库中获取并存入redis中");
}
}
}
第一遍执行结果:
PONG
redis中key为strName的值为: 李狗蛋
PONG
redis中不存在, 从数据库中获取并存入redis中
第二遍执行的结果
PONG
redis中key为strName的值为: 李狗蛋
PONG
redis中存在,将直接返回:微信下棋小程序
使用jedis连接池操作hash类型数据
编写实体类,模拟使用hash类型存储对象
@Data
@AllArgsConstructor
public class User {
private int id;
private String name;
private int age;
private String birthday;
}
编写测试类
/**
* 测试连接池工具类完成哈希类型数据的操作
* 需求:查询redis中是否存在存储指定对象的key
* 如果存在则直接从redis返回(打印)
* 如果不存在则从数据库中获取(模拟)并存储到redis中并打印
* -- 最原始方式
*/
@Test
void opsHashTypeTest() {
jedis = JedisPoolUtil.getJedis();
String key = "user";
if(jedis.exists(key)) {
//redis中存在该key
System.out.println("redis中存在该key,直接返回: "+jedis.hgetAll(key));
}else {
//redis中不存在该key
HashMap<String, String> hash = new HashMap<>();
hash.put("id", "1");
hash.put("name", "ryujung");
hash.put("age", "25");
hash.put("birthday", "1994-09-05");
jedis.hmset(key, hash);//模拟从数据库获取对象
System.out.println("redis中不存在,从数据库获取,并存入redis中: "+jedis.hgetAll(key));
}
JedisPoolUtil.close(jedis);
}
/**
* 对以上方法的优化
*/
@Test
void opsHashTypeTest2() {
//模拟根据id查询数据的情况
String id = "1";
Jedis jedis = JedisPoolUtil.getJedis();
String key = "user:"+id;//统一redis中key的格式,便于维护
if(jedis.exists(key)) {
System.out.println("redis中存在该数据,直接返回:"+jedis.hgetAll(key));
}else {
//redis 中不存在该数据,模拟根据id从数据库中查询该数据
User user = new User(1,"ryujung",25,"1994-09-05");
//将对象中的所有属性封装到map中作为参数,以hash类型数据存储进redis中
HashMap<String, String> map = new HashMap<>();
map.put("id",String.valueOf(user.getId()));
map.put("name",user.getName());
map.put("age",String.valueOf(user.getAge()));
map.put("birthday",user.getBirthday());
jedis.hset(key,map);//存入redis中
System.out.println("redis中不存在该数据,从数据库中获取该数据并存储到redis中:"+user);
}
JedisPoolUtil.close(jedis);
}
第一次执行结果(两个测试)
redis中不存在,从数据库获取,并存入redis中: {birthday=1994-09-05, name=ryujung, age=25, id=1}
redis中不存在该数据,从数据库中获取该数据并存储到redis中:User(id=1, name=ryujung, age=25, birthday=1994-09-05)
第二次执行结果(两个测试)
redis中存在该key,直接返回: {birthday=1994-09-05, name=ryujung, age=25, id=1}
redis中存在该数据,直接返回:{birthday=1994-09-05, name=ryujung, age=25, id=1}
查看redis中
测试一保存的数据
测试二保存的数据:
4.3 列表List
Redis列表是简单的字符串列表, 按照插入顺序排序, 可以添加一个元素到列表的头部(左边), 或者尾部(右边),一个列表最多可以包2^32-1个元素
类似于Java中的LinkedList
4.3.1 基本语法
赋值语法
LPUSH key value [value ...]
将所有指定的值插入到存于 key 的列表的头部。如果 key 不存在,那么在进行 push 操作前会创建一个空列表。 如果 key 对应的值不是一个 list 的话,那么会返回一个错误。可以使用一个命令把多个元素 push 进入列表,只需在命令末尾加上多个指定的参数RPUSH key value [value ...]
向存于 key 的列表的尾部插入所有指定的值。如果 key 不存在,那么会创建一个空的列表然后再进行 push 操作。 当 key 保存的不是一个列表,那么会返回一个错误。可以使用一个命令把多个元素打入队列,只需要在命令后面指定多个参数。LPUSHX key value
只有当 key 已经存在并且存着一个 list 的时候,在这个 key 下面的 list 的头部插入 value。 与 LPUSH 相反,当 key 不存在的时候不会进行任何操作。RPUSHX key value
将值 value 插入到列表 key 的表尾, 当且仅当 key 存在并且是一个列表。 和 RPUSH 命令相反, 当 key 不存在时,RPUSHX 命令什么也不做。LPOP key
移除并返回key对应的list的第一个元素。
取值语法
RPOP key
移除并返回存于 key 的 list 的最后一个元素。BLPOP key [key ...] timeout
BLPOP 是阻塞式列表的弹出原语。 它是命令 LPOP 的阻塞版本,这是因为当给定列表内没有任何元素可供弹出的时候, 连接将被 BLPOP 命令阻塞。 当给定多个 key 参数时,按参数 key 的先后顺序依次检查各个列表,弹出第一个非空列表的头元素。BRPOP key [key ...] timeout
BRPOP 是一个阻塞的列表弹出原语。 它是 RPOP 的阻塞版本,因为这个命令会在给定list无法弹出任何元素的时候阻塞连接。 该命令会按照给出的 key 顺序查看 list,并在找到的第一个非空 list 的尾部弹出一个元素。
范围取值(取所有值)
LRANGE key start stop
返回存储在 key 的列表里指定范围内的元素。 start 和 end 偏移量都是基于0的下标,即list的第一个元素下标是0(list的表头),第二个元素下标是1,以此类推。偏移量也可以是负数,表示偏移量是从list尾部开始计数。 例如, -1 表示列表的最后一个元素,-2 是倒数第二个,以此类推。
返回所有: start = 0 ,end = -1时返回所有数据。
修改语法
LSET key index value
设置 index 位置的list元素的值为 value。 更多关于 index 参数的信息,详见 LINDEX。当index超出范围时会返回一个error。
插入语法
LINSERT key BEFORE|AFTER pivot value
把 value 插入存于 key 的列表中在基准值 pivot 的前面或后面。当 key 不存在时,这个list会被看作是空list,任何操作都不会发生。当 key 存在,但保存的不是一个list的时候,会返回error。
List其他常用命令
LLEN key
返回存储在 key 里的list的长度。 如果 key 不存在,那么就被看作是空list,并且返回长度为 0。 当存储在 key 里的值不是一个list的话,会返回error。LINDEX key index
返回列表里的元素的索引 index 存储在 key 里面。 下标是从0开始索引的,所以 0 是表示第一个元素, 1 表示第二个元素,并以此类推。 负数索引用于指定从列表尾部开始索引的元素。在这种方法下,-1 表示最后一个元素,-2 表示倒数第二个元素,并以此往前推。当 key 位置的值不是一个列表的时候,会返回一个error。LTRIM key start stop
修剪(trim)一个已存在的 list,这样 list 就会只包含指定范围的指定元素。start 和 stop 都是由0开始计数的, 这里的 0 是列表里的第一个元素(表头),1 是第二个元素,以此类推。
高级使用
RPOPLPUSH source destination
原子性地返回并移除存储在 source 的列表的最后一个元素(列表尾部元素), 并把该元素放入存储在 destination 的列表的第一个元素位置(列表头部)。BRPOPLPUSH source destination timeout
BRPOPLPUSH 是 RPOPLPUSH 的阻塞版本。 当 source 包含元素的时候,这个命令表现得跟 RPOPLPUSH 一模一样。 当 source 是空的时候,Redis将会阻塞这个连接,直到另一个客户端 push 元素进入或者达到 timeout 时限。 timeout 为 0 能用于无限期阻塞客户端。
4.3.2 应用场景
项目中常应用于: 1. 对数据量大的集合数据进行删减; 2. 任务队列;
- 列表数据显示、关注列表、粉丝列表、留言评价、数据分页、热点新闻(Top5)等
利用LRANGE 可以很方便地实现分页功能, 在博客系统中,每篇博文的评论也可以存入一个单独的list中。 - 订单系统的下单流程, 用户系统登录注册等
模拟订单系统的下单流程,使用stringRedisTemplate模板进行操作
public void listQueueInit(String cardId) {
String key = "prod:" + cardId;//完成付款后初始化的任务队列
template.opsForList().leftPushAll(key,
"1.商家已发货",
"2.快递公司已发出",
"3.已到达机场,将发往白云机场",
"4.到达白云机场,将发往花都区",
"5.到达花都区保利城伊顿H座",
"6.已签收",
"7.客户评价");
}
//完成了一个任务
public void finishOneTask(String cardId) {
String key = "prod:"+cardId+":finished";//已完成的队列
//将完成的任务从任务队列的右侧弹出并放进已完成队列的头部(左侧)
template.opsForList().rightPopAndLeftPush("prod:"+cardId, key);
}
//查询已完成的任务队列
public List<String> queryListQueueFinished(String cardId){
String key = "prod:"+cardId+":finished";
return template.opsForList().range(key, 0, -1);
}
//查询未完成的任务队列
public List<String> queryListQueue(String cardId){
String key = "prod:"+cardId;
return template.opsForList().range(key, 0, -1);
}
在订单完成后,会初始化一个任务队列,根据快递的进度,会将初始任务队列中的任务不断地将已完成地任务转移到已完成地任务队列中,保持任务随着进度进行更新。下面进行单元测试,首先将初始化一个模拟用户的下单操作,创建任务队列
@Test
void testTaskQueueByRedis() {
String cardId = "ryujung:20200327";
userService.listQueueInit(cardId);//初始化队列
List<String> finishedQueue = userService.queryListQueueFinished(cardId);
System.out.println("----已完成的任务----"+cardId);
finishedQueue.forEach(System.out::println);
List<String> queue = userService.queryListQueue(cardId);
System.out.println("----未完成的任务----"+cardId);
queue.forEach(System.out::println);
}
运行结果:
可以看到任务队列的初始化已经完成了, 已经可以从redis中到查询未完成任务和已完成任务了
接着,调用完成任务的方法,代表快递状态变化,将未完成任务队列中的尾部数据转移到已完成任务的头部,使用redis的RPOPLPUSH方法,并进行查询:
@Test
void testTaskQueueByRedis() {
String cardId = "ryujung:20200327";
userService.finishOneTask(cardId);//完成一个当前的任务,队列转移
List<String> finishedQueue = userService.queryListQueueFinished(cardId);
System.out.println("----已完成的任务----"+cardId);
finishedQueue.forEach(System.out::println);
List<String> queue = userService.queryListQueue(cardId);
System.out.println("----未完成的任务----"+cardId);
queue.forEach(System.out::println);
}
执行三次的结果:
可以看到队列已经成功转移了,查看redis中未完成队列数据:
已完成队列数据:
至此,完成了通过Redis的List数据类型的订单任务队列
4.4 集合Set
Redis的Set时String类型的无序集合, 集合成员是唯一的,没有重复元素。
Redis中集合是通过哈希表实现的, 所以增删改的复杂度都是O(1)
集合中最大的成员数未2^32-1(40多亿个成员)类似于Java的Hashtable集合。
4.4.1 基本使用语法
添加操作
SADD key member [member ...]
添加一个或多个指定的member元素到集合的 key中.指定的一个或者多个元素member 如果已经在集合key中存在则忽略.如果集合key 不存在,则新建集合key,并添加member元素到集合key中。如果key 的类型不是集合则返回错误.
查询操作
SCARD key
返回集合存储的key的基数 (集合元素的数量).SMEMBERS key
返回key集合所有的元素. 该命令的作用与使用一个参数的SINTER 命令作用相同。SISMEMBER key member
返回成员 member 是否是存储的集合 key的成员.SRANDMEMBER key [count]
仅提供key参数,那么随机返回key集合中的一个元素.
Redis 2.6开始,可以接受 count 参数,如果count是整数且小于元素的个数,返回含有 count 个不同的元素的数组,如果count是个整数且大于集合中元素的个数时,仅返回整个集合的所有元素,当count是负数,则会返回一个包含count的绝对值的个数元素的数组,如果count的绝对值大于元素的个数,则返回的结果集里会出现一个元素出现多次的情况.
仅提供key参数时,该命令作用类似于SPOP命令,不同的是SPOP命令会将被选择的随机元素从集合中移除,而SRANDMEMBER仅仅是返回该随记元素,而不做任何操作.
删除操作
SREM key member [member ...]
在key集合中移除指定的元素. 如果指定的元素不是key集合中的元素则忽略 如果key集合不存在则被视为一个空的集合,该命令返回0。如果key的类型不是一个集合,则返回错误SPOP key [count]
从存储在key的集合中移除并返回一个或多个随机元素。SMOVE source destination member
将member从source集合移动到destination集合中. 对于其他的客户端,在特定的时间元素将会作为source或者destination集合的成员出现.
如果source 集合不存在或者不包含指定的元素,这smove命令不执行任何操作并且返回0.否则对象将会从source集合中移除,并添加到destination集合中去,如果destination集合已经存在该元素,则smove命令仅将该元素充source集合中移除. 如果source 和destination不是集合类型,则返回错误.
集合对比
SDIFF key [key ...]
返回一个集合与给定集合的差集的元素. 不同集合中不相同的集合SDIFFSTORE destination key [key ...]
该命令类似于 SDIFF, 不同之处在于该命令不返回结果集,而是将结果存放在destination集合中。如果destination已经存在, 则将其覆盖重写.SINTER key [key ...]
返回指定所有的集合的成员的交集.SINTERSTORE destination key [key ...]
这个命令与SINTER命令类似, 但是它并不是直接返回结果集,而是将结果保存在 destination集合中。如果destination 集合存在, 则会被重写。SUNION key [key ...]
返回给定的多个集合的并集中的所有成员.SUNIONSTORE destination key [key ...]
该命令作用类似于SUNION命令,不同的是它并不返回结果集,而是将结果存储在destination集合中。如果destination 已经存在,则将其覆盖。
4.4.2 应用场景
常应用于:对两个集合间的数据进行交集、并集、差集运算
- 非常方便地实现如共同关注、共同喜好、二度好友等功能。对集合操作,还可以使用不同的结果返回给客户端还是存储到一个新的集合中
- 利用唯一性,可以统计访问网站的所有独立IP
4.5 有序集合Sorted Set(ZSet)
4.5.1 简介
- Redis有序集合和集合一样也是string类型元素的集合,且不允许重复的成员。
- 不同的是每个元素都会关联一个double类型的分数。Redis正是通过分数来为集合中的成员进行从小到大的排序。
- 有序集合的成员是唯一的,但分数(score)却可以重复。
- 集合是通过哈希表实现的,所以增删改的复杂度都为O(1)。集合中最大的成员为2^32-1(40多亿个成员)。
ZSet:有序,且不重复
4.5.2 基本使用语法
赋值语法
-
ZADD key [NX|XX] [CH] [INCR] score member [score member ...]
将所有指定成员添加到键为key有序集合(sorted set)里面。 添加时可以指定多个分数/成员(score/member)对。 如果指定添加的成员已经是有序集合里面的成员,则会更新改成员的分数(scrore)并更新到正确的排序位置。
如果key不存在,将会创建一个新的有序集合(sorted set)并将分数/成员(score/member)对添加到有序集合,就像原来存在一个空的有序集合一样。如果key存在,但是类型不是有序集合,将会返回一个错误应答。
分数值是一个双精度的浮点型数字字符串。+inf和-inf都是有效值。- XX: 仅仅更新存在的成员,不添加新成员。
- NX: 不更新存在的成员。只添加新成员。
- CH: 修改返回值为发生变化的成员总数,原始是返回新添加成员的总数 (CH 是 changed 的意思)。更改的元素是新添加的成员,已经存在的成员更新分数。 所以在命令中指定的成员有相同的分数将不被计算在内。注:在通常情况下,ZADD返回值只计算新添加成员的数量。
- NCR: 当ZADD指定这个选项时,成员的操作就等同ZINCRBY命令,对成员的分数进行递增操作。
-
ZINCRBY key increment member
为有序集key的成员member的score值加上增量increment。如果key中不存在member,就在key中添加一个member,score是increment(就好像它之前的score是0.0)。如果key不存在,就创建一个只含有指定member成员的有序集合。
当key不是有序集类型时,返回一个错误。
score值必须是字符串表示的整数值或双精度浮点数,并且能接受double精度的浮点数。也有可能给一个负数来减少score的值。
取值语法
-
ZCARD key
返回key的有序集元素个数。 -
ZCOUNT key min max
返回有序集key中,score值在min和max之间(默认包括score值等于min或max)的成员数量。 -
ZRANGE key start stop [WITHSCORES]
返回存储在有序集合key中的指定范围的元素。 返回的元素可以认为是按得分从最低到最高排列。 如果得分相同,将按字典排序。 -
ZREVRANGE key start stop [WITHSCORES]
返回有序集key中,指定区间内的成员。其中成员的位置按score值递减(从大到小)来排列。具有相同score值的成员按字典序的反序排列。 除了成员按score值递减的次序排列这一点外,ZREVRANGE命令的其他方面和ZRANGE命令一样。 -
ZRANK key member
返回有序集key中成员member的排名。其中有序集成员按score从小到大顺序排列。排名以0为底,也就是说,score值最小的成员排名为0。使用ZREVRANK命令可以获得成员按score值递减(从大到小)排列的排名。 -
ZREVRANK key member
返回有序集key中成员member的排名,其中有序集成员按score值从大到小排列。排名以0为底,也就是说,score值最大的成员排名为0。 -
ZSCORE key member
返回有序集key中,成员member的score值。如果member元素不是有序集key的成员,或key不存在,返回nil。 -
ZPOPMAX key [count]
删除并返回有序集合key
中的最多count
个具有最高得分的成员。如未指定,
count
的默认值为1。指定一个大于有序集合的基数的count
不会产生错误。 当返回多个元素时候,得分最高的元素将是第一个元素,然后是分数较低的元素。 -
ZPOPMIN key [count]
删除并返回有序集合key
中的最多count
个具有最低得分的成员。如未指定,
count
的默认值为1。指定一个大于有序集合的基数的count
不会产生错误。 当返回多个元素时候,得分最低的元素将是第一个元素,然后是分数较高的元素。
删除语法
-
ZREM key member [member ...]
当key存在,但是其不是有序集合类型,就返回一个错误。 -
ZREMRANGEBYRANK key start stop
移除有序集key中,指定排名(rank)区间内的所有成员。下标参数start和stop都以0为底,0处是分数最小的那个元素。这些索引也可是负数,表示位移从最高分处开始数。例如,-1是分数最高的元素,-2是分数第二高的,依次类推。 -
ZREMRANGEBYSCORE key min max
移除有序集key中,所有score值介于min和max之间(包括等于min或max)的成员。ZRANGEBYSCORE zset (1 5 -- 返回所有符合条件1 < score <= 5的成员; ZRANGEBYSCORE zset (5 (10 -- 返回所有符合条件5 < score < 10 的成员。
4.5.3 应用场景: 排行榜
- 可以将timeline以发表时间为score了来存储, 这样就是自动按照时间排好序的。
- 可以存储全班同学的成绩的Sorted Set, 其集合value可以是同学的学号,而score就可以是开始分数,在插入数据时,就已经进行的排序。
- 还可以使用Sorted Set来做带权重的队列,例如将普通消息的score为1,重要消息的score为2,然后工作线程可以选择按score的倒序来获取任务,让重要的任务优先执行。
4.6 Streams 流
4.7 地理空间 Geospatial
基本语法
赋值语法
-
GEOADD key longitude latitude member [longitude latitude member ...]
将指定的地理空间位置(经度、纬度、名称)添加到指定的key中。这些数据将会存储到sorted set这样的目的是为了方便使用GEORADIUS或者GEORADIUSBYMEMBER命令对数据进行半径查询等操作。该命令以采用标准格式的参数x,y,所以经度必须在纬度之前。这些坐标的限制是可以被编入索引的,区域面积可以很接近极点但是不能索引。具体的限制,由EPSG:900913 / EPSG:3785 / OSGEO:41001 规定如下:
有效的经度从-180度到180度。
有效的纬度从-85.05112878度到85.05112878度。
当坐标位置超出上述指定范围时,该命令将会返回一个错误。
127.0.0.1:6379> geoadd china 113.23 23.16 guangdong
(integer) 1
127.0.0.1:6379> geoadd china 104.06 30.67 chengdu
(integer) 1
127.0.0.1:6379> geoadd china 87.68 43.77 wulumuqi
(integer) 1
127.0.0.1:6379> geoadd china 108.92 34.27 xi'an
Invalid argument(s)
127.0.0.1:6379> geoadd china 108.92 34.27 xian
(integer) 1
127.0.0.1:6379> geoadd china 116.46 39.92 beijing
(integer) 1
127.0.0.1:6379> geoadd china 114.31 30.52 wuhan
(integer) 1
127.0.0.1:6379> geoadd china 89.19 42.91 tulufan
(integer) 1
## 可以通过java一次性导入地理位置
查询语句
-
GEODIST key member1 member2 [unit]
返回两个给定位置之间的距离。如果两个位置之间的其中一个不存在, 那么命令返回空值。
指定单位的参数 unit 必须是以下单位的其中一个:- m 表示单位为米。
- km 表示单位为千米。
- mi 表示单位为英里。
- ft 表示单位为英尺。
127.0.0.1:6379> geodist china tulufan wulumuqi km "155.1423" 127.0.0.1:6379> geodist china chengdu guangdong km "1233.8564"
-
GEOPOS key member [member ...]
从key里返回所有给定位置元素的位置(经度和纬度)。给定一个sorted set表示的空间索引,密集使用 geoadd 命令,它以获得指定成员的坐标往往是有益的。当空间索引填充通过 geoadd 的坐标转换成一个52位Geohash,所以返回的坐标可能不完全以添加元素的,但小的错误可能会出台。
因为 GEOPOS 命令接受可变数量的位置元素作为输入, 所以即使用户只给定了一个位置元素, 命令也会返回数组回复127.0.0.1:6379> geopos china beijing 1) 1) "116.45999997854232788" 2) "39.9199990416181052" 127.0.0.1:6379> geopos chian guangdong 1) (nil) 127.0.0.1:6379> geopos china guangdong 1) 1) "113.22999805212020874" 2) "23.1599994376353493"
-
GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count]
以给定的经纬度为中心, 返回键包含的位置元素当中, 与中心的距离不超过给定最大距离的所有位置元素。- m 表示单位为米。
- km 表示单位为千米。
- mi 表示单位为英里。
- ft 表示单位为英尺。
在给定以下可选项时, 命令会返回额外的信息:
-
WITHDIST: 在返回位置元素的同时, 将位置元素与中心之间的距离也一并返回。 距离的单位和用户给定的范围单位保持一致。
-
WITHCOORD: 将位置元素的经度和维度也一并返回。
-
WITHHASH: 以 52 位有符号整数的形式, 返回位置元素经过原始 geohash 编码的有序集合分值。 这个选项主要用于底层应用或者调试, 实际中的作用并不大。
命令默认返回未排序的位置元素。 通过以下两个参数, 用户可以指定被返回位置元素的排序方式:
- ASC: 根据中心的位置, 按照从近到远的方式返回位置元素。
- DESC: 根据中心的位置, 按照从远到近的方式返回位置元素。
127.0.0.1:6379> georadius china 108 23 2000 km ## 以108,23为中心2000公里范围内的城市 1) "chengdu" 2) "xian" 3) "guangdong" 4) "wuhan" 127.0.0.1:6379> georadius china 108 23 2000 km count 2 ## 限制返回的元素数量 1) "guangdong" 2) "chengdu" 127.0.0.1:6379> georadius china 108 23 2000 km withdist ## 返回元素并携带距离信息 1) 1) "chengdu" 2) "938.2348" ## 中心距离 2) 1) "xian" 2) "1256.7140" ## 中心距离 3) 1) "guangdong" 2) "535.4191" ## 中心距离 4) 1) "wuhan" 2) "1044.6259" ## 中心距离
-
GEORADIUSBYMEMBER key member radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count]
这个命令和 GEORADIUS 命令一样, 都可以找出位于指定范围内的元素, 但是GEORADIUSBYMEMBER
的中心点是由给定的位置元素决定的, 而不是像 GEORADIUS 那样, 使用输入的经度和纬度来决定中心点,即指定成员的位置被用作查询的中心。127.0.0.1:6379> georadiusbymember china chengdu 1500 km withdist ## 以成都为中心位置,查找其1500km为半径范围内的其他城市(元素) 1) 1) "chengdu" 2) "0.0000" 2) 1) "xian" 2) "606.7380" 3) 1) "guangdong" 2) "1233.8564" 4) 1) "wuhan" 2) "981.1582"
底层原理
GEO的底层是通过zset实现,可以通过zset的命令操作GEO,例如增删改等操作
127.0.0.1:6379> type china ##查看使用geoadd存储的key的类型
zset ## 显示为zset
127.0.0.1:6379> zrange china 0 -1 ##通过zset命令获取GEO元素
1) "wulumuqi"
2) "tulufan"
3) "chengdu"
4) "xian"
5) "guangdong"
6) "wuhan"
7) "beijing"
127.0.0.1:6379> zrem china xian ## 移除元素
(integer) 1 ## 成功移除
4.8 HyperLogLog 数据结构(统计)
简介
官网: HyperLogLog是一种概率数据结构,用于对唯一事物进行计数(从技术上讲,这是指估计集合的基数)。 通常,对唯一项目进行计数需要使用与要计数的项目数量成比例的内存量,因为您需要记住过去已经看到的元素,以避免多次对其进行计数。 但是,有一组算法会以内存换取精度:以Redis实施为例,您得出的带有标准误差的估计度量最终会小于1%。 该算法的神奇之处在于,您不再需要使用与计数的项目数量成比例的内存量,而是可以使用恒定数量的内存! 在最坏的情况下为12k字节,如果您的HyperLogLog(从现在开始将它们称为HLL)看到的元素很少,则少得多。
Redis中的HLL尽管在技术上是不同的数据结构,但被编码为Redis字符串,因此您可以调用GET来序列化HLL,然后调用SET来将其反序列化回服务器。
从概念上讲,HLL API就像使用Set来执行相同的任务。 您可以将每个观察到的元素添加到集合中,并使用SCARD检查集合中的元素数量,这是唯一的,因为SADD不会重新添加现有元素。
尽管您并未真正将项目添加到HLL中,但由于数据结构仅包含不包含实际元素的状态,因此API相同:
每次看到新元素时,都可以使用PFADD将其添加到计数中。
到目前为止,每次您要检索添加到PFADD中的唯一元素的当前近似值时,都使用PFCOUNT。
个人总结: 用于做基数统计的算法。例如要统计网站的访问认数,同i一个ip算一个人,不记录多次访问。
传统方式,set保存id,然后通过scard key
获取set集合中的元素数量就可以统计访问认数(不重复)。
可使用使用HyperLogLog数据结构来完成这个功能,HyperLogLog的特点为:
占用的内存使固定的, 存放2^64不同的元素的计数,只需要12kb内存。
基本语法
PFADD key element [element ...]
将除了第一个参数以外的参数存储到以第一个参数为变量名的HyperLogLog结构中.
这个命令的一个副作用是它可能会更改这个HyperLogLog的内部来反映在每添加一个唯一的对象时估计的基数(集合的基数).
如果一个HyperLogLog的估计的近似基数在执行命令过程中发了变化, PFADD 返回1,否则返回0,如果指定的key不存在,这个命令会自动创建一个空的HyperLogLog结构(指定长度和编码的字符串).
如果在调用该命令时仅提供变量名而不指定元素也是可以的,如果这个变量名存在,则不会有任何操作,如果不存在,则会创建一个数据结构(返回1)。
127.0.0.1:6379> pfadd hll1 a b c d e f g ## 将元素添加到名为hll 的HyperLogLog结构中
(integer) 1 ## 返回1代表内部的数量有变化
127.0.0.1:6379> pfadd hll1 a b c d ## 将重复的元素添加到HLL数据结构中
(integer) 0 ##返回0代表内部的数量没有发生变化(数据重复)
127.0.0.1:6379> pfadd hll1 h ## 再次添加新的元素
(integer) 1 ## 成功添加
-
PFCOUNT key [key ...]
当参数为一个key时,返回存储在HyperLogLog结构体的该变量的近似基数,如果该变量不存在,则返回0.127.0.0.1:6379> pfcount hll1 ##已存在 (integer) 8 127.0.0.1:6379> pfcount hll2 ##不存在 (integer) 0
当参数为多个key时,返回这些HyperLogLog并集的近似基数,这个值是将所给定的所有key的HyperLoglog结构合并到一个临时的HyperLogLog结构中计算而得到的.
HyperLogLog可以使用固定且很少的内存(每个HyperLogLog结构需要12K字节再加上key本身的几个字节)来存储集合的唯一元素.
返回的可见集合基数并不是精确值, 而是一个带有 0.81% 标准错误(standard error)的近似值.
注意: 这个命令的一个副作用是可能会导致HyperLogLog内部被更改,出于缓存的目的,它会用8字节的来记录最近一次计算得到基数,所以PFCOUNT命令在技术上是个写命令. -
PFMERGE destkey sourcekey [sourcekey ...]
将多个 HyperLogLog 合并(merge)为一个 HyperLogLog , 合并后的 HyperLogLog 的基数接近于所有输入 HyperLogLog 的可见集合(observed set)的并集.合并得出的 HyperLogLog 会被储存在目标变量(第一个参数)里面, 如果该键并不存在, 那么命令在执行之前, 会先为该键创建一个空的.
127.0.0.1:6379> pfadd hll2 ab cd ef gh ## 创建并添加元素到另一个HLL数据结构的key (integer) 1 ## 添加成功,HLL数据发生变化 127.0.0.1:6379> pfmerge hll3 hll1 hll2 ## 将hh1和hhl2的数据合并(merge)存储到新的hll3中 OK ## 操作成功 127.0.0.1:6379> pfcount hll3 (integer) 12 ## 查询新的hll3中数量 127.0.0.1:6379> pfcount hll2 (integer) 4 127.0.0.1:6379> pfcount hll1 (integer) 8 ## 验证正确性 ##也可以添加到已经存在的HLL结构中 127.0.0.1:6379> pfmerge hll1 hll2 OK 127.0.0.1:6379> pfcount hll1 (integer) 12
优点
相较传统的计数方式,HyperLogLog数据结构内存大小固定,而且更加节省内存,不像传统使用set来存储数据导致的随着元素的增加,占用的内存也不断增加。有良好的实用价值。
4.9 Bitmaps 位图
简单介绍
位图不是实际的数据类型,而是在String类型上定义的一组面向位的操作。 由于字符串是二进制安全Blob,并且最大长度为512 MB,因此它们适合设置多达2 ^ 32个不同的位(40亿)。
位操作分为两类:固定时间的单个位操作(如将一个位设置为1或0或获取其值),以及对位组的操作,例如计算给定位范围内设置的位的数量 (例如,人口计数)。
位图的最大优点之一是,它们在存储信息时通常可以节省大量空间。 例如,在以增量用户ID表示不同用户的系统中,仅使用512 MB内存就可以记住40亿用户的一位信息(例如,知道用户是否要接收新闻通讯)。
使用场景
统计用户信息,活跃、不活跃、登录、未登录、打卡等两个状态的信息。
统计用户画像,用于精准定位用户群体。每个标签使用一个bitmap
使用bitmap记录一周的打卡情况
将每个需要打卡的用户的名字设置为key,使用bitmap存储打卡信息,0代表未打卡,1代表已打卡。
127.0.0.1:6379> setbit user1:clock-in 1 0
(integer) 0
127.0.0.1:6379> setbit user1:clock-in 2 1
(integer) 0
127.0.0.1:6379> setbit user1:clock-in 3 1
(integer) 0
127.0.0.1:6379> setbit user1:clock-in 4 0
(integer) 0
127.0.0.1:6379> setbit user1:clock-in 5 1
(integer) 0
127.0.0.1:6379> setbit user1:clock-in 6 0
(integer) 0
127.0.0.1:6379> setbit user1:clock-in 7 0
(integer) 0
查看某一天是否打卡:
127.0.0.1:6379> getbit user1:clock-in 4
(integer) 0 ##未打卡
127.0.0.1:6379> getbit user1:clock-in 5
(integer) 1 ## 已打卡
统计这一周内,该用户的打卡次数
127.0.0.1:6379> bitcount user1:clock-in
(integer) 3 ##本周该用户打卡3次
5. SpringBoot2.x中Redis进行连接(RedisTemplate[Lettuce])
5.1 简介
Spring data 提供了redisTemplate模板
它封装了redis连接池管理的逻辑,业务代码无须关系获取,释放连接的逻辑
spring redis 同时支持了Jedis,Jredis,rjc客户端操作
在RedisTemplate中提供了几个常用的客户端方法:
private ValueOperations<K,V> valueOps;
private ListOperations<K,V> listOps;
private SetOperations<K,V> setOps;
private ZSetOperations<K,V> zSetOps;
@Resource(name="redisTemplate")
ListOperations<String,String> list;
5.2 使用步骤
-
导入maven依赖
<!-- 引入jedis客户端来连接redis --> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>3.1.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
-
给实体类加上一个序列化标识
public class User implements Serializable {
-
配置Redis的配置类并开启springboot对缓存的支持
/**
* Redis的配置类
*/
@Configuration
@EnableCaching
public class RedisConfig {
@Bean
public JedisConnectionFactory redisConnectionFactory() {
RedisStandaloneConfiguration config = new RedisStandaloneConfiguration("192.168.0.101", 6379);
config.setPassword("ryujung");
return new JedisConnectionFactory(config);
}
//更改操作对象的template的序列化方式为JSON格式
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
Jackson2JsonRedisSerializer<Object> j2jSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
template.setDefaultSerializer(j2jSerializer);
return template;
}
}
编写测试类
@Autowired
private UserService userService;
@Resource(name = "stringRedisTemplate")
private RedisTemplate<String, String> template;
@Test
void test() {
System.out.println(userService);
System.out.println(template.opsForValue().get("test"));
}
/**
* 测试redisTemplate
*/
@Test
void test1() {
String key = "applicartionName";
String result = userService.getString(key);
System.out.println(result);
}
测试结果:
com.qianfeng.service.impl.UserServiceImpl@475646d4
connection success!
redis中不存在该键值, 从数据库查询并保存进redis:stringRedisTemplatev操作redis练习.
stringRedisTemplatev操作redis练习.
test1第二次执行
redis中存在该键值, 直接返回:stringRedisTemplatev操作redis练习.
stringRedisTemplatev操作redis练习.
查看redis中的数据
可以看到已经成功保存到了redis中
RedisTemplate在设置键值时,还可以同时判断该值是否存在并设置过期时间,类似于setnx命令
Boolean setIfAbsent(K key, V value, long timeout, TimeUnit unit);
5.3 练习: 限制登陆次数功能
需求: 用户在2分钟内, 仅允许输入错误密码5次, 如果超过次数, 限制其登录1小时, (要求每次登录都要给相应的提示)
6. 其他功能
Redis订阅发布
简介
Redis发布订阅(pub/sub)是一种消息通信模式,发送者发送消息,订阅者接受收消息。
Redis客户端可以订阅任意数量的频道。
基本使用语法
-
PUBLISH channel message
将信息 message 发送到指定的频道 channel
返回值--integer-reply: 收到消息的客户端数量。127.0.0.1:6379> publish ryujung "hello,it's RyuJung." (integer) 1
-
SUBSCRIBE channel [channel ...]
订阅给指定频道的信息。
一旦客户端进入订阅状态,客户端就只可接受订阅相关的命令SUBSCRIBE、PSUBSCRIBE、UNSUBSCRIBE和PUNSUBSCRIBE除了这些命令,其他命令一律失效。127.0.0.1:6379> subscribe ryujung ## 订阅名为ryujung的频道 Reading messages... (press Ctrl-C to quit) 1) "subscribe" 2) "ryujung" 3) (integer) 1 1) "message" ##收到消息 2) "ryujung" ## 消息来源 3) "hello,it's RyuJung." ## 消息内容
-
`PSUBSCRIBE pattern [pattern ...]` 订阅给定的模式(patterns)。
支持的模式(patterns)有:- `h?llo` subscribes to hello, hallo and hxllo
- `h*llo` subscribes to hllo and heeeello
- `h[ae]llo` subscribes to hello and hallo, but not hillo
如果想输入普通的字符,可以在前面添加\
```shell
会话一
127.0.0.1:6379> psubscribe new. ##接收名称以new.开头的频道
Reading messages... (press Ctrl-C to quit) ## 开始等待接收消息
1) "psubscribe"
2) "new."
3) (integer) 1会话二
127.0.0.1:6379> publish new.ryujung "hello,It's a news."
(integer) 1
127.0.0.1:6379> publish new.health "It's a healthy news."
(integer) 1 ##向两个不同的队列中发送消息再次查看会话一
127.0.0.1:6379> psubscribe new.
Reading messages... (press Ctrl-C to quit)
1) "psubscribe"
2) "new."
3) (integer) 1
1) "pmessage"
2) "new."
3) "new.ryujung" ## 接收到队列一的消息
4) "hello,It's a news."
1) "pmessage"
2) "new."
3) "new.health" ## 接收到队列二的消息
4) "It's a healthy news."
```</li>
<li>
<p>`PUBSUB subcommand [argument [argument ...]]` `PUBSUB` 是自省命令,能够检测PUB/SUB子系统的状态。它由分别详细描述的子命令组成。</p>
<ul>
<li>`PUBSUB CHANNELS [pattern]` 列出所有符合匹配条件的并且有订阅者的信道,匹配模式同上</li>
</ul>
<p>```shell
127.0.0.1:6379> pubsub channels ## 查看所有频道
1) "ryujung"
127.0.0.1:6379> pubsub channels ryu* ##查看名字以ryu开头的频道
1) "ryujung"
```- `PUBSUB NUMSUB [channel-1 ... channel-N]` 列出指定信道的订阅者个数(不包括订阅模式的客户端订阅者)
```shell
127.0.0.1:6379> pubsub numsub ryujung ## 查看ryujung频道的订阅数量
1) "ryujung"
2) (integer) 1 ## 该频道有1个订阅者.
```<ul>
<li>`pubsub numpat` 返回订阅模式的数量(使用命令`PSUBSCRIBE`实现).注意, 这个命令返回的不是订阅模式的客户端的数量, 而是客户端订阅的所有模式的数量总和。</li>
</ul>
<p>```shell
127.0.0.1:6379> pubsub numpat
(integer) 2
``` -
`UNSUBSCRIBE [channel [channel ...]]` 指示客户端退订给定的频道,若没有指定频道,则退订所有频道.
如果没有频道被指定,即,一个无参数的 UNSUBSCRIBE 调用被执行,那么客户端使用 SUBSCRIBE 命令订阅的所有频道都会被退订。 在这种情况下,命令会返回一个信息,告知客户端所有被退订的频道。
-
`PUNSUBSCRIBE [pattern [pattern ...]]` 指示客户端退订指定模式,若果没有提供模式则退出所有模式。
如果没有模式被指定,即一个无参数的 PUNSUBSCRIBE 调用被执行,那么客户端使用 PSUBSCRIBE 命令订阅的所有模式都会被退订。 在这种情况下,命令会返回一个信息,告知客户端所有被退订的模式。
Redis事务
Redis事务的本质:一组命令的集合。一个事务中所有的命令都会被序列化,会按照顺序执行。
==Redis事务没有隔离级别的概念!==
Redis 的应用场景明显不是为了数据存储的高可靠而设计的,而是为了数据访问的高性能而设计,设计者为了简单性和高性能而部分放弃了原子性。==Redis的单条指令保证原子性,但是事务不保证原子性!==
事务的执行过程:
- 开启事务(multi)
- 命令入队(…)
- 执行事务(exec)或取消执行(discard)
127.0.0.1:6379> multi ##开启事务
OK
127.0.0.1:6379> lpush tx a ##以下为一组命令
QUEUED
127.0.0.1:6379> lpush tx b
QUEUED
127.0.0.1:6379> lpush tx c
QUEUED
127.0.0.1:6379> exec ## 依次执行队列中的所有命令
1) (integer) 1
2) (integer) 2
3) (integer) 3
127.0.0.1:6379> lrange tx 0 -1
1) "c"
2) "b"
3) "a"
127.0.0.1:6379> multi
OK
127.0.0.1:6379> lpush tx a
QUEUED
127.0.0.1:6379> lpush tx b
QUEUED
127.0.0.1:6379> lpush tx c
QUEUED
127.0.0.1:6379> discard ## 放弃执行并取消事务
OK
127.0.0.1:6379> lrange tx 0 -1
(empty list or set) ## 所有的指令都没有执行,给key为空
使用 check-and-set 操作实现事务乐观锁
WATCH 命令可以为 Redis 事务提供 check-and-set (CAS)行为。
被 WATCH 的键会被监视,并会发觉这些键是否被改动过了。 如果有至少一个被监视的键在 EXEC 执行之前被修改了, 那么整个事务都会被取消, EXEC 返回nil-reply来表示事务已经失败。
举个例子, 假设我们需要原子性地为某个值进行增 1 操作(假设 INCR 不存在)。
首先我们可能会这样做:
val = GET mykey
val = val + 1
SET mykey $val
上面的这个实现在只有一个客户端的时候可以执行得很好。 但是, 当多个客户端同时对同一个键进行这样的操作时, 就会产生竞争条件。举个例子, 如果客户端 A 和 B 都读取了键原来的值, 比如 10 , 那么两个客户端都会将键的值设为 11 , 但正确的结果应该是 12 才对。
有了 WATCH , 我们就可以轻松地解决这类问题了:
WATCH mykey
val = GET mykey
val = val + 1
MULTI
SET mykey $val
EXEC
使用上面的代码, 如果在 WATCH 执行之后, EXEC 执行之前, 有其他客户端修改了 `mykey` 的值, 那么当前客户端的事务就会失败。 程序需要做的, 就是不断重试这个操作, 直到没有发生碰撞为止。
这种形式的锁被称作乐观锁, 它是一种非常强大的锁机制。 并且因为大多数情况下, 不同的客户端会访问不同的键, 碰撞的情况一般都很少, 所以通常并不需要进行重试。
Redis数据淘汰策略
Maxmemory配置指令
`maxmemory`配置指令用于配置Redis存储数据时指定限制的内存大小。通过redis.conf可以设置该指令,或者之后使用CONFIG SET命令来进行运行时配置。
例如为了配置内存限制为100mb,以下的指令可以放在`redis.conf`文件中。
maxmemory 100mb
设置`maxmemory`为0代表没有内存限制。对于64位的系统这是个默认值,对于32位的系统默认内存限制为3GB。
当指定的内存限制大小达到时,需要选择不同的行为,也就是策略。 Redis可以仅仅对命令返回错误,这将使得内存被使用得更多,或者回收一些旧的数据来使得添加数据时可以避免内存限制。
当maxmemory限制达到的时候Redis会使用的行为由 Redis的maxmemory-policy配置指令来进行配置。
以下的淘汰策略是可用的:
- noeviction:返回错误当内存限制达到并且客户端尝试执行会让更多内存被使用的命令(大部分的写入指令,但DEL和几个例外)
- allkeys-lru: 尝试回收最少使用的键(LRU),使得新添加的数据有空间存放。
- volatile-lru: 尝试回收最少使用的键(LRU),但仅限于在过期集合的键,使得新添加的数据有空间存放。
- allkeys-random: 回收随机的键使得新添加的数据有空间存放。
- volatile-random: 回收随机的键使得新添加的数据有空间存放,但仅限于在过期集合的键。
- volatile-ttl: 回收在过期集合的键,并且优先回收存活时间(TTL)较短的键,使得新添加的数据有空间存放。
如果没有键满足回收的前提条件的话,策略volatile-lru, volatile-random以及volatile-ttl就和noeviction 差不多了。
选择正确的回收策略是非常重要的,这取决于你的应用的访问模式,不过你可以在运行时进行相关的策略调整,并且监控缓存命中率和没命中的次数,通过RedisINFO命令输出以便调优。
一般的经验规则:
- 使用allkeys-lru策略:当你希望你的请求符合一个幂定律分布,也就是说,你希望部分的子集元素将比其它其它元素被访问的更多。如果你不确定选择什么,这是个很好的选择。.
- 使用allkeys-random:如果你是循环访问,所有的键被连续的扫描,或者你希望请求分布正常(所有元素被访问的概率都差不多)。
- 使用volatile-ttl:如果你想要通过创建缓存对象时设置TTL值,来决定哪些对象应该被过期。
allkeys-lru 和 volatile-random策略对于当你想要单一的实例实现缓存及持久化一些键时很有用。不过一般运行两个实例是解决这个问题的更好方法。
为了键设置过期时间也是需要消耗内存的,==所以使用allkeys-lru这种策略更加高效,因为没有必要为键取设置过期时间当内存有压力时==。
Redis持久化
Redis 有两种持久化方案,RDB (Redis DataBase)和 AOF (Append Only File)。
RDB
概述
RDB 是Redis默认的持久化方案。在指定的时间间隔内,执行指定次数的写操作,则会将内存中的数据写入到磁盘中。即在指定目录下生成一个dump.rdb文件。Redis默认启动时会通过加载dump.rdb文件恢复数据。
RDB在保存RDB文件时父进程唯一需要做的就是fork出一个子进程,接下来的工作全部由子进程来做,父进程不需要再做其他IO操作,所以RDB持久化方式可以最大化redis的性能.
与AOF相比,在恢复大的数据集的时候,RDB方式会更快一些.
打开 redis.conf 文件,找到 SNAPSHOTTING 对应内容
################################ SNAPSHOTTING ################################
#
# Save the DB on disk:
#
# save
#
# Will save the DB if both the given number of seconds and the given
# number of write operations against the DB occurred.
#
# In the example below the behaviour will be to save:
# after 900 sec (15 min) if at least 1 key changed
# after 300 sec (5 min) if at least 10 keys changed
# after 60 sec if at least 10000 keys changed
#
# Note: you can disable saving completely by commenting out all "save" lines.
#
# It is also possible to remove all the previously configured save
# points by adding a save directive with a single empty string argument
# like in the following example:
#
# save ""
save 900 1
save 300 10
save 60 10000
触发条件
1 在指定的时间间隔内,执行指定次数的写操作
2 执行save(阻塞, 只管保存快照,其他的等待) 或者是bgsave (异步)命令
3 执行flushall 命令,清空数据库所有数据。
4 执行shutdown 命令,保证服务器正常关闭且不丢失任何数据。
通过RDB文件恢复数据
将dump.rdb 文件拷贝到redis的安装目录的bin目录下,重启redis服务即可。在实际开发中,一般会考虑到物理机硬盘损坏情况,选择备份dump.rdb 文件。
关闭RDB持久化
如果需要,建议先将指定的dump.rdb文件先进行备份
使用`redis-cli` 执行`config set save ""` 命令(运行状态),或者将配置文件中的`save ...` 都注释掉,添加一个`save ""` 即可关闭RDB持久化方式。可以不进行持久化或者开启AOF来切换到AOF持久化模式,建议开启RDB和AOF两种方式。
################################ SNAPSHOTTING ################################
#
# Save the DB on disk:
#
# save
#
# Will save the DB if both the given number of seconds and the given
# number of write operations against the DB occurred.
#
# In the example below the behaviour will be to save:
# after 900 sec (15 min) if at least 1 key changed
# after 300 sec (5 min) if at least 10 keys changed
# after 60 sec if at least 10000 keys changed
#
# Note: you can disable saving completely by commenting out all "save" lines.
#
# It is also possible to remove all the previously configured save
# points by adding a save directive with a single empty string argument
# like in the following example:
#
# save "" ## 开打这个注释
save 900 1 ## 注销着三行
save 300 10
save 60 10000
RDB 的优缺点
优点:
1 适合大规模的数据恢复,数据恢复速度快。
2 如果业务对数据完整性和一致性要求不高,RDB是很好的选择。
缺点:
1 数据的完整性和一致性不高,因为RDB可能在最后一次备份时宕机了。通常会每隔5分钟或者更久做一次完整的保存,万一在Redis意外宕机,你可能会丢失几分钟的数据.
2 备份时占用内存,因为Redis 在备份时会独立创建一个子进程,将数据写入到一个临时文件(此时内存中的数据是原来的两倍哦),最后再将临时文件替换之前的备份文件。
所以Redis 的持久化和数据的恢复要选择在夜深人静的时候执行是比较合理的。
AOF
概述
AOF :Redis 默认不开启。它的出现是为了弥补RDB的不足(数据的不一致性),所以它==采用日志的形式来记录每个写操作,并追加到文件中。==Redis 重启的会根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。当同时开启RDB(默认开启)和AOF(手动开启)后,在启动redis时会优先加载AOF文件中的内容作为数据恢复。
==同时开启两种持久化方式, 在这种情况下, 当redis重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整.==
############################## APPEND ONLY MODE ###############################
# By default Redis asynchronously dumps the dataset on disk. This mode is
# good enough in many applications, but an issue with the Redis process or
# a power outage may result into a few minutes of writes lost (depending on
# the configured save points).
#
# The Append Only File is an alternative persistence mode that provides
# much better durability. For instance using the default data fsync policy
# (see later in the config file) Redis can lose just one second of writes in a
# dramatic event like a server power outage, or a single write if something
# wrong with the Redis process itself happens, but the operating system is
# still running correctly.
#
# AOF and RDB persistence can be enabled at the same time without problems.
# If the AOF is enabled on startup Redis will load the AOF, that is the file
# with the better durability guarantees.
appendonly no ## aof同步模式默认是关闭的,修改为yes开启aof模式,优先级高于rdb,但恢复耗时长
# aof同步的文件名称,默认为"appendonly.aof",可以自行修改(不建议)
appendfilename "appendonly.aof"
# The fsync() call tells the Operating System to actually write data on disk
# instead of waiting for more data in the output buffer. Some OS will really flush
# data on disk, some other OS will just try to do it ASAP.
#
# Redis supports three different modes:
#
# no: don't fsync, just let the OS flush the data when it wants. Faster.
# always: fsync after every write to the append only log. Slow, Safest.
# everysec: fsync only one time every second. Compromise.
#
# The default is "everysec", as that's usually the right compromise between
# speed and data safety. It's up to you to understand if you can relax this to
# "no" that will let the operating system flush the output buffer when
# it wants, for better performances (but if you can live with the idea of
# some data loss consider the default persistence mode that's snapshotting),
# or on the contrary, use "always" that's very slow but a bit safer than
# everysec.
# If unsure, use "everysec". 如果不确定就是用"每秒同步"
# appendfsync always 每条写操作同时记录aof文件
appendfsync everysec ## 每秒进行依次aof文件记录(推荐)
# appendfsync no 从不进行写操作
使用默认的每秒fsync策略,Redis的性能依然很好(fsync是由后台线程进行处理的,主线程会尽力处理客户端请求),一旦出现故障,你最多丢失1秒的数据.
日志重写-- `bgrewriteaof`命令
因为 AOF 的运作方式是不断地将命令追加到文件的末尾, 所以随着写入命令的不断增加, AOF 文件的体积也会变得越来越大。举个例子, 如果你对一个计数器调用了 100 次 INCR , 那么仅仅是为了保存这个计数器的当前值, AOF 文件就需要使用 100 条记录(entry)。然而在实际上, 只使用一条 SET 命令已经足以保存计数器的当前值了, 其余 99 条记录实际上都是多余的。
由于AOF持久化是Redis不断将写命令记录到 AOF 文件中,随着Redis不断的进行,AOF 的文件会越来越大,文件越大,占用服务器内存越大以及 AOF 恢复要求时间越长。为了解决这个问题,Redis新增了重写机制,==当AOF文件的大小超过所设定的阈值时==,Redis就会启动AOF文件的内容压缩,只保留可以恢复数据的最小指令集。
为了处理这种情况, Redis 支持一种有趣的特性: 可以在不打断服务客户端的情况下, 对 AOF 文件进行重建(rebuild)。执行 `BGREWRITEAOF`命令, Redis 将生成一个新的 AOF 文件, 这个文件包含重建当前数据集所需的最少命令。Redis 2.2 需要自己手动执行 `BGREWRITEAOF` 命令; Redis 2.4 则可以自动触发 AOF 重写, 具体信息请查看 2.4 的示例配置文件。
# This is how it works: Redis remembers the size of the AOF file after the
# latest rewrite (if no rewrite has happened since the restart, the size of
# the AOF at startup is used).
#
# This base size is compared to the current size. If the current size is
# bigger than the specified percentage, the rewrite is triggered. Also
# you need to specify a minimal size for the AOF file to be rewritten, this
# is useful to avoid rewriting the AOF file even if the percentage increase
# is reached but it is still pretty small.
#
# Specify a percentage of zero in order to disable the automatic AOF
# rewrite feature.
# Redis会记住上次重写后的AOF文件大小。
## 渣翻:如果当前大小超过指定的百分数,就会触发重写操作,同时需要指定aof进行重写操作的最小文件大小,这样可以避免明明文件很小但是达到了百分比从而触发重写(浪费性能,没有必要),工程上可以调大一些。3G、4G
# 可以通过将百分数设置为0来关闭自动的重写操作。(不建议)
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
aof文件损坏修复
AOF文件是一个只进行追加的日志文件,所以不需要写入seek,即使由于某些原因(磁盘空间已满,写的过程中宕机等等)未执行完整的写入命令,你也也可使用`redis-check-aof`工具修复这些问题,为了模拟文件损坏,可以在aof文件后追加一行毫无意义的字符串。然后使用`redis-check-aof`进行以下修复
[root@localhost redis]# ./bin/redis-check-aof --fix data/appendonly.aof
0x 55: Expected prefix '*', got: 'a'
AOF analyzed: size=109, ok_up_to=85, diff=24
This will shrink the AOF from 109 bytes, with 24 bytes, to 85 bytes
Continue? [y/N]: y ## 发现aof文件有损坏的部分,是否截断?(会丢失最后一行数据) y确定
Successfully truncated AOF ## 修复成功
注意:在aof文件损坏的情况下,会导致redis无法正常启动和使用客户端访问(如果开启了aof持久化模式),所以要会如何修复AOF文件。
误操作如何恢复数据?
AOF 文件有序地保存了对数据库执行的所有写入操作, 这些写入操作以 Redis 协议的格式保存, 因此 AOF 文件的内容非常容易被人读懂, 对文件进行分析(parse)也很轻松。 导出(export) AOF 文件也非常简单: 举个例子, 如果你不小心执行了 FLUSHALL 命令, 但==只要 AOF 文件未被重写==, 那么只要停止服务器, 移除 AOF 文件末尾的 FLUSHALL 命令, 并重启 Redis , 就可以将数据集恢复到 FLUSHALL 执行之前的状态。
127.0.0.1:6379> set user RyuJung ## 保存一个数据
OK
127.0.0.1:6379> flushall ## 误操作,将数据库所有数据都删除了,糟糕!
OK
127.0.0.1:6379> shutdown ##为了恢复数据,需要将服务器停掉
not connected> quit ## 退出客户端
[root@localhost redis]# cat data/appendonly.aof ## 查看aop文件内容
*2
$6
SELECT
$1
0
*3
$3
set
$4
user
$7
RyuJung
*1
$8
flushall
## 将最后的flushall命令删除,在重启redis-server,登录redis-cli
127.0.0.1:6379> keys *
1) "user"
127.0.0.1:6379> get user
"RyuJung"
## 可以看到文件已经被恢复了。
RDB和AOF对比和选择
- 对于相同的数据集来说,AOF 文件的体积通常要大于 RDB 文件的体积。
- 根据所使用的 fsync 策略,==AOF 的速度可能会慢于 RDB== 。 在一般情况下, 每秒 fsync 的性能依然非常高, 而关闭 fsync 可以让 AOF 的速度和 RDB 一样快, 即使在高负荷之下也是如此。 不过在处理巨大的写入载入时,RDB 可以提供更有保证的最大延迟时间(latency)。
一般来说, 如果想达到足以媲美 PostgreSQL 的数据安全性, 你应该同时使用两种持久化功能。
如果你非常关心你的数据, 但仍然可以承受数分钟以内的数据丢失, 那么你可以只使用 RDB 持久化。
有很多用户都只使用 AOF 持久化, 但我们并不推荐这种方式: 因为定时生成 RDB 快照(snapshot)非常便于进行数据库备份, 并且 ==RDB 恢复数据集的速度也要比 AOF 恢复的速度要快, 除此之外, 使用 RDB 还可以避免之前提到的 AOF 程序的 bug== 。
Note: 因为以上提到的种种原因, 未来我们可能会将 AOF 和 RDB 整合成单个持久化模型。 (这是一个长期计划。) 接下来的几个小节将介绍 RDB 和 AOF 的更多细节。
根据官网的计划,RDB和AOF混合持久化方式在4.0版本横空出世
混合持久化方式
可以发现RDB和AOF给有各自的优点和缺点,在数据恢复方面RDB的速度更快,而AOF的数据更加全面,有没有办法既可以有RDB的快速又有AOF恢复数据的全面的持久化方式呢?这就是RDB-AOF混合持久化方式。也就是说AOF文件的前半段是RDB格式的全量数据后半段是redis命令格式的增量数据。
在Redis的配置文件中,有如下配置
# When rewriting the AOF file, Redis is able to use an RDB preamble in the
# AOF file for faster rewrites and recoveries. When this option is turned
# on the rewritten AOF file is composed of two different stanzas:
#
# [RDB file][AOF tail]
#
# When loading Redis recognizes that the AOF file starts with the "REDIS"
# string and loads the prefixed RDB file, and continues loading the AOF
# tail.
#
# This is currently turned off by default in order to avoid the surprise
# of a format change, but will at some point be used as the default.
aof-use-rdb-preamble no
在进行aof文件重写时,可以使用RDB前缀存储的aof文件中以便于快速重写和恢复。当该选项打开后,重写的AOF文件就会由两部分组成,[RDB file][AOF tail],当redis加载aof文件时发现其以“REDIS”作为开头字段,会先加载头部的RDB文件(快速),再继续加载AOF的尾部。
首先来看RDB文件和AOF文件使用`cat`指令查看的样子
[root@localhost redis]# cat data/dump.rdb
REDIS0008 redis-ver4.0.1 ## 这是RDB持久化方式的文件
redis-bits㨭eÌJused-me½
preamblezrepl-id(ea2f7bceb6b9f298d78af0a39e615371d8cfd161
䯬-offset~㴳erRyuJungÿ|·ª¹Į [root@localhost redis]# cat data/appendonly.aof
*2 ## 这是AOF持久化方式的文件
$6
SELECT
$1
0
*3
$3
set
$4
user
$7
RyuJung
将`aof-use-rdb-preamble no`配置项的值改为yes,并手动使用`BGREWRITEAOF`进行一次aof文件重写的操作。
127.0.0.1:6379> config set aof-use-rdb-preamble yes ## 修改开启aof的混合持久化方式(单次有效,如果要永久有效需要修改配置文件中的该配置项)。
OK
127.0.0.1:6379> BGREWRITEAOF ## 手动进行aof文件重写
Background append only file rewriting started
127.0.0.1:6379> set foo bar ## 再添加一条数据(没有触发aof重写条件)
OK
再次查看AOF文件
[root@localhost redis]# cat data/appendonly.aof
REDIS0008 redis-ver4.0.1 ## 前面为重写后产生全量RDB数据
redis-bits㨭e%¨used-memÈ
preambleチ䯬-id(5d5db9ff75cda53eb005ea70d8f1bae44da2c31c
䯬-offset~! edicfghbjlistfedcbaÿuserRyuJungÿvyĽp*2
$6 ## 在重写后在进行写操作的增量数据记录
SELECT
$1
0
*3
$3
set
$3
foo
$3
bar
Redis缓存与数据库同步(Kafka作为异步队列)
Redis实现布隆过滤器
安装Redis的bloomfilter插件
- 在github官网上找到其对应的发行版本,下载对应的.gz压缩文件。
wget https://github.com/RedisLabsModules/rebloom/archive/v1.1.1.tar.gz
- 解压完成后,使用make指令编译源文件,得到`rebloom.so`文件
[root@redis]# tar -zxvf v1.1.1.tar.gz
[root@redis]# cd redisbloom-1.1.1/
[root@redisbloom-1.1.1]# make
[root@redisbloom-1.1.1]# ls
contrib Dockerfile docs LICENSE Makefile mkdocs.yml ramp.yml README.md rebloom.so src tests
- 在redis.conf中加入该模块即可,并重启redis-server
- 通过`redis-cli`客户端就可以使用布隆过滤器了
使用
127.0.0.1:6379> bf.add user user2 ## 向user中添加元素
(integer) 1
127.0.0.1:6379> bf.exists user user2 ##查找元素是否存在
(integer) 1 ##存在
127.0.0.1:6379> bf.exists user user3
(integer) 0 ##不存在
127.0.0.1:6379> bf.madd user user3 user4 user5 user6 user7 ## madd 批量添加
1) (integer) 1
2) (integer) 1
3) (integer) 1
4) (integer) 1
5) (integer) 1
127.0.0.1:6379> bf.mexists user user1 user2 user3 ## 批量判断
1) (integer) 0
2) (integer) 1
3) (integer) 1
127.0.0.1:6379> bf.reserve books 0.001 20 ## 创建一个名为books的误判率为0.1%,容量为20的k-v
OK
`bf.reserve key error_rate initial_size` 创建语法
Java实现的BloomFilter
创建一个maven项目,并导入操作bloomfilter的依赖包
io.github.nitesh7
jrebloom
1.1.2
编写测试类。
import java.math.BigDecimal;
import java.math.RoundingMode;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import io.rebloom.client.Client;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.Protocol;
import redis.clients.jedis.util.Pool;
public class BloomFilterTest {
public static final String HOST = "192.168.0.108";
public static final String PASSWORD = "ryujung";
Pool jedisPool;
Jedis jedis;
Client client;
public static final String BOOKS = "books";
@Before
public void init() {
JedisPoolConfig config = new JedisPoolConfig();
jedisPool = new JedisPool(config,
HOST,
Protocol.DEFAULT_PORT,
Protocol.DEFAULT_TIMEOUT,
PASSWORD);
jedis = jedisPool.getResource();
//测试jedis连接是否成功
System.out.println(jedis.ping());
client = new Client(jedisPool);
}
@After
public void destroy() {
if (jedis != null) {
jedis.close();
}
if (jedisPool != null) {
jedisPool.close();
}
}
/**
* 默认的Filter的initial_size=100,error_rate=0.1;
*/
@Test
public void testBloomFilterAdd() {
jedis.del("books".getBytes());
int count = 0;
for (int i = 0; i < 10000; i++) {
String book = "book:" + i;
boolean add = client.add(BOOKS, book);
if (!add) {
System.out.println("添加失败:" + book);
count++;
}
}
Double errorRate = (double) (count / 100);
System.out.println("通过布隆过滤器添加完成!10000个数据中误判" + count);
System.out.println("误判率为:" + errorRate + "%");
}
/**
* 默认的Filter的initial_size=100,error_rate=0.1;
* 这次手动声明filter的初始大小1_0000和错误率0.005
*/
@Test
public void testBloomFilterAdd2() {
client.delete("books");
client.createFilter("books", 1_0000, 0.005);
int count = 0;
for (int i = 0; i < 1_0000; i++) {
String book = "book:" + i;
boolean add = client.add(BOOKS, book);
if (!add) {
System.out.println("添加失败:" + book);
count++;
}
}
/**
* public BigDecimal divide(BigDecimal divisor, int scale, int roundingMode)
* 第一参数表示除数, 第二个参数表示小数点后保留位数,
*/
BigDecimal errorRate = BigDecimal.valueOf(count).divide(new BigDecimal(100),
4, //保留4位小数
RoundingMode.HALF_UP);//四舍五入
System.out.println("通过布隆过滤器添加完成!10000个数据中误判" + count);//3
System.out.println("误判率为:" + errorRate + "%");// 0.0300%
}
/**
* 以下数据在均不存在于filter中,测试布隆过滤器的过滤能力;
*/
@Test
public void testFilterCheck() {
int count =0;
for (int i = 1_0000; i < 1_5000; i++) {
String book = "book:" + i;
if (client.exists(BOOKS, book)) {
System.out.println("元素存在集合中:" + book);
count++;
}
}
System.out.println("误判存在的元素数量为:"+count);// 53
}
@Test
public void testJedisAdd() {
jedis.del("books".getBytes());
int count = 0;
for (int i = 0; i < 10000; i++) {
String book = "book:" + i;
Long add = jedis.sadd(BOOKS, book);
if (add != 1L) {
System.out.println("添加失败:" + book);
count++;
}
}
double errorRate = count / 100;
System.out.println("通过set集合添加完成!10000个数据中误判" + count);
System.out.println("误判率为:" + errorRate + "%");
}
}
7. Redis知识点总结
缓存穿透 (缓存中不存在)
查询一个缓存中一定没有的数据, 由于缓存没有全部会去数据库查询,数据库瞬间大量流量压进来,容易导致系统宕机
缓存穿透的解决办法:
- 无论数据是否存在都存放进redis中,没有就存null
- 为防止数据被null屏蔽, 给所有的过期时间的数据设置过期时间,某个时间后去数据库查询新数据
- 布隆过滤器
缓存雪崩 (大面积失效)
缓存的大面积数据同时全体过期,所有的查询穿透缓存压进数据库,容易导致系统宕机
解决方案:
- key的过期时间设置为随机
缓存击穿 (热点key)
热点key值的过期,导致高并发访问击穿缓存,压进数据库,导致负载过高
高并发访问中,无论是大面积key还是热点key,一旦redis中没有就会同时去访问数据库,导致数据库负载过高而出现问题,解决的方法:锁机制
Redis锁机制
Docker进入redis的方法
docker exec –it redis redis-cli
docker exec –it redis redis-benchmark
在分布式条件下,所有的Java锁机制都无法正常使用, 因为不再同一个jvm环境下,不同的jvm加锁之间没有关系, 无法加同一把锁, 这些锁被称为进程内锁.
PRC-->用于解决进程间通信问题
进程间(这里指跨机器)-->使用进程间锁-->分布式锁
分布式锁的核心:
- 加锁, 判断并将锁位设置值,表示已经加锁(判断和设置必须是原子性操作,否则将有线程安全问题),使用redis中的setnx操作
- 获取锁后判断setnx的返回值是否为0, 如果不为0(设置锁成功)则执行业务逻辑并释放锁,否则就重试等待锁(自旋)
- 以上逻辑存在问题,如果特殊情况导致锁没有被释放(断电,灾害,异常-可解决),则所有线程将无期限等待,所以要给锁设置过期时间expire
- 如果加锁,如果设置锁和设置过期时间不是原子性的,则所无法被释放的情况还是有可能发生
综上所述,使用如下命令解决以上问题
set 锁名 锁值 EX 10 NX ##EX为过期时间,单位秒,NX表示执行的条件为not exist,不存在值才设置,否则不进行设置
## 设置成功返回1,设置失败返回0
java代码为:
//加锁
String token = UUID.randomUUID().toString();
String lock = jedis.set(key, token, "NX", "EX",20);
setnx命令:set if not exist ,原子性操作,判断并保存,多线程并发条件下是安全的,如果不存则设置值并返回1,否则设置失败并返回0
如果业务逻辑执行超时,导致锁过期被自动删除,在业务逻辑执行完后又一次删锁操作有可能会导致其他线程刚设置的锁被删除,从而导致线程安全问题
解决思路: 每次加锁时,将锁的值设置为自己特有的值, 在删除锁之前进行判断,当前redis中的锁 ,如果是自己设置的特有值才删除锁,否则不删除
在删除锁之前的判断redis中的锁时,redis刚刚返回值时, 自己的锁过期被自动释放了,由其他线程获取锁, 而判断依然为自己的锁,又一次执行锁删除操作, 导致删除其他线程的锁, 造成线程安全问题
问题原因: 判断是否为自己的特有所和删除操作非原子性,导致线程安全问题
解决方案: 使用lua脚本执行判断和删锁操作, 由于redis为单线程,该操作就保证了原子性
//原子性解锁操作
String script =
"if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end";
jedis.eval(script, Collections.singletonList(key), Collections.singletonList(token));
总结Redis分布式锁的核心
- 加锁: 判断和加锁必须为原子操作,如果失败则自旋
- 锁必须设置过期时间
- 判断和解锁操作必须为原子操作,判断失败则不进行删锁操作
8. Redis高级配置
主从复制集群搭建
主从复制概述
在Redis客户端通过info replication可以查看与复制相关的状态,对于了解主从节点的当前状态,以及解决出现的问题都会有帮助。
主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master/leader),后者称为从节点(slave/follower);数据的复制是单向的,只能由主节点到从节点。
默认情况下,每台Redis服务器都是主节点;且一个主节点可以有多个从节点(或没有从节点),但一个从节点只能有一个主节点。
主从复制的作用主要包括:
- 数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
- 故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余。
- 负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis数据时应用连接主节点,读Redis数据时应用连接从节点),分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。
- 高可用基石:除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。
创建主从赋值集群
将redis.conf
文件赋值三份,分别起名为redis-master.conf
redis-slaver01.conf
redis-slaver02.conf
修改配置文件,主要的修改项目如下
- 指定端口
- 设置
pidfile
- 设置log文件位置
- 修改RDB文件名
- 修改AOF文件名
主 从1 从2
port 6380 6381 6382
pidfile /var/run/redis_6380.pid /var/run/redis_6381.pid /var/run/redis_6382.pid
logfile "/usr/local/redis/logs/master-6380.log"
logfile "/usr/local/redis/logs/slaver-01-6381.log"
logfile "/usr/local/redis/logs/slaver-02-6382.log"
dbfilename dump-master-6380.rdb
dbfilename dump-slaver-01-6381.rdb
dbfilename dump-slaver-02-6382.rdb
appendfilename "appendonly-master.aof"
appendfilename "appendonly-slaver-01.aof"
appendfilename "appendonly-slaver-02.aof"
- 添加配置从连接主的认证信息
slaveof localhost 6380
masterauth ryujung
启动三个redis服务,并向主服务中添加一个key
## 启动三个redis实例
[root@localhost redis]# ./bin/redis-server cluster/redis-master.conf
[root@localhost redis]# ./bin/redis-server cluster/redis-slaver01.conf
[root@localhost redis]# ./bin/redis-server cluster/redis-slaver02.conf
## 进入master中并执行写操作
[root@localhost redis]# ./bin/redis-cli -p 6380 -a ryujung
127.0.0.1:6380> keys *
(empty list or set)
127.0.0.1:6380> set master masterKey
OK
127.0.0.1:6380> quit
## 进入slaver-01中查看是否成功赋值master中的信息,并尝试写操作
[root@localhost redis]# ./bin/redis-cli -p 6381 -a ryujung
127.0.0.1:6381> keys *
1) "master"
127.0.0.1:6381> set slaver01 slaver01
(error) READONLY You can't write against a read only slave. ## 写操作失败
127.0.0.1:6381> quit
## 进入slaver-01中查看是否成功赋值master中的信息,并尝试写操作
[root@localhost redis]# ./bin/redis-cli -p 6382 -a ryujung
127.0.0.1:6382> keys *
1) "master"
127.0.0.1:6382> set slaver02 slaver02
(error) READONLY You can't write against a read only slave. ## 写操作失败
查看每个节点的主从复制相关的状态信息
127.0.0.1:6382> info replication ## 在不同节点使用该命令查看或者直接使用info指令
# Replication
role:slave
master_host:localhost
master_port:6380
master_link_status:up
master_last_io_seconds_ago:6
master_sync_in_progress:0
slave_repl_offset:1463
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:cdba41d50a84ca0b22f6f9da4189de800ac513e2
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:1463
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:15
repl_backlog_histlen:1449
主从同步的机制
Redis主从复制可以根据是否是全量分为全量同步和增量同步。
Redis主从同步策略
主从刚刚连接的时候,进行全量同步;全同步结束后,进行增量同步。当然,如果有需要,slave 在任何时候都可以发起全量同步。redis 策略是,无论如何,首先会尝试进行增量同步,如不成功,要求从机进行全量同步。
全量同步
Redis全量复制一般发生在Slave初始化阶段,这时Slave需要将Master上的所有数据都复制一份。具体步骤如下:
- 从服务器连接主服务器,发送SYNC命令;
- 主服务器接收到SYNC命名后,开始执行BGSAVE命令生成RDB文件并使用缓冲区记录此后执行的所有写命令;
- 主服务器BGSAVE执行完后,向所有从服务器发送快照文件,并在发送期间继续记录被执行的写命令;
- 从服务器收到快照文件后丢弃所有旧数据,载入收到的快照;
- 主服务器快照发送完毕后开始向从服务器发送缓冲区中的写命令;
- 从服务器完成对快照的载入,开始接收命令请求,并执行来自主服务器缓冲区的写命令;
增量同步
Redis增量复制是指Slave初始化后开始正常工作时主服务器发生的写操作同步到从服务器的过程。
增量复制的过程主要是主服务器每执行一个写命令就会向从服务器发送相同的写命令,从服务器接收并执行收到的写命令。
==来自官网的解释==
每一个 Redis master 都有一个 replication ID :这是一个较大的伪随机字符串,标记了一个给定的数据集。每个 master 也持有一个==偏移量==,master 将自己产生的复制流发送给 slave 时,发送多少个字节的数据,自身的偏移量就会增加多少,目的是当有新的操作修改自己的数据集时,它可以以此更新 slave 的状态。复制偏移量即使在没有一个 slave 连接到 master 时,也会自增,所以基本上每一对给定的
Replication ID, offset
都会标识一个 master 数据集的确切版本。
当 slave 连接到 master 时,它们使用 PSYNC 命令来发送它们记录的旧的 master replication ID 和它们至今为止处理的偏移量。通过这种方式, master 能够仅发送 slave 所需的增量部分。但是如果 master 的缓冲区中没有足够的命令积压缓冲记录,或者如果 slave 引用了不再知道的历史记录(replication ID),则会转而进行一个全量重同步:在这种情况下, slave 会得到一个完整的数据集副本,从头开始。
下面是一个全量同步的工作细节:
master 开启一个后台保存进程,以便于生产一个 RDB 文件。同时它开始缓冲所有从客户端接收到的新的写入命令。当后台保存完成时, master 将数据集文件传输给 slave, slave将之保存在磁盘上,然后加载文件到内存。再然后 master 会发送所有缓冲的命令发给 slave。这个过程以指令流的形式完成并且和 Redis 协议本身的格式相同。
你可以用 telnet 自己进行尝试。在服务器正在做一些工作的同时连接到 Redis 端口并发出 SYNC 命令。你将会看到一个批量传输,并且之后每一个 master 接收到的命令都将在 telnet 回话中被重新发出。事实上 SYNC 是一个旧协议,在新的 Redis 实例中已经不再被使用,但是其仍然向后兼容:但它不允许部分重同步,所以现在 PSYNC 被用来替代 SYNC。
之前说过,当主从之间的连接因为一些原因崩溃之后, slave 能够自动重连。如果 master 收到了多个 slave 要求同步的请求,它会执行一个单独的后台保存,以便于为多个 slave 服务。
集群命令
SLAVEOF host port
该命令可以将当前服务器转变为指定服务器的从属服务器(slave server)。
如果当前服务器已经是某个主服务器(master server)的从属服务器,那么执行 SLAVEOF host port 将使当前服务器停止对旧主服务器的同步,丢弃旧数据集,转而开始对新主服务器进行同步。
从属服务器执行命令 SLAVEOF NO ONE
将使得这个从属服务器关闭复制功能,并从从属服务器转变回主服务器,原来同步所得的数据集不会被丢弃。(重启失效)
哨兵模式
介绍
主从切换技术的方法是:当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用。这不是一种推荐的方式,更多时候,我们优先考虑哨兵模式。
该系统执行以下三个任务:
- 监控(Monitoring): Sentinel 会不断地检查你的主服务器和从服务器是否运作正常。
- 提醒(Notification): 当被监控的某个 Redis 服务器出现问题时, Sentinel 可以通过 API 向管理员或者其他应用程序发送通知。
- 自动故障迁移(Automatic failover): 当一个主服务器不能正常工作时, Sentinel 会开始一次自动故障迁移操作, 它会将失效主服务器的其中一个从服务器升级为新的主服务器, 并让失效主服务器的其他从服务器改为复制新的主服务器; 当客户端试图连接失效的主服务器时, 集群也会向客户端返回新主服务器的地址, 使得集群可以使用新主服务器代替失效服务器。
然而一个哨兵进程对Redis服务器进行监控,可能会出现问题,为此,我们可以使用多个哨兵进行监控。各个哨兵之间还会进行监控,这样就形成了多哨兵模式。
用文字描述一下故障切换(failover)的过程。假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行failover过程,仅仅是哨兵1主观的认为主服务器不可用,这个现象成为主观下线。当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,那么哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行failover操作。切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为客观下线。这样对于客户端而言,一切都是透明的。
哨兵模式搭建
首先,需要找到redis安装目录下的sentinel.conf
配置文件,将其复制到/usr/local/reids/
文件夹下,然后对其进行修改
[root@localhost redis]# find / -name sentinel.conf
/root/redis-4.0.1/sentinel.conf
[root@localhost redis]# cp /root/redis-4.0.1/sentinel.conf ./sentinel.conf
[root@localhost redis]# vim sentinel.conf
配置sentinel.conf
文件,修改端口号、开启后台启动、连接master的认证信息等
哨兵80 哨兵81 哨兵82
port 26380 26381 26382
## 其他配置都相同
daemonize yes
## 哨兵监视master的信息,mymaster是名称,可以自己定义,然后是host,port,最后的数字代表,至少要多少哨兵认为master主观下线才能进行故障迁移操作。
sentinel monitor mymaster 127.0.0.1 6380 2
sentinel auth-pass mymaster ryujung
测试哨兵是否可以正常工作并进行故障迁移
## 首先启动服务集群
[root@localhost redis]# ./bin/redis-server cluster/redis-master.conf
[root@localhost redis]# ./bin/redis-server cluster/redis-slaver01.conf
[root@localhost redis]# ./bin/redis-server cluster/redis-slaver02.conf
## 再启动三个哨兵,这里暂时注释了后台启动,为了查看日志信息。
[root@localhost redis]# ./bin/redis-sentinel cluster/sentinel-80.conf
5175:X 05 Apr 04:48:57.986 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
5175:X 05 Apr 04:48:57.986 # Redis version=4.0.1, bits=64, commit=00000000, modified=0, pid=5175, just started
5175:X 05 Apr 04:48:57.986 # Configuration loaded
5175:X 05 Apr 04:48:57.987 * Increased maximum number of open files to 10032 (it was originally set to 1024).
_._
_.-``__ ''-._
_.-`` `. `_. ''-._ Redis 4.0.1 (00000000/0) 64 bit
.-`` .-```. ```\/ _.,_ ''-._
( ' , .-` | `, ) Running in sentinel mode
|`-._`-...-` __...-.``-._|'` _.-'| Port: 26380
| `-._ `._ / _.-' | PID: 5175
`-._ `-._ `-./ _.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' | http://redis.io
`-._ `-._`-.__.-'_.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' |
`-._ `-._`-.__.-'_.-' _.-'
`-._ `-.__.-' _.-'
`-._ _.-'
`-.__.-'
5175:X 05 Apr 04:48:57.988 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128.
5175:X 05 Apr 04:48:57.988 # Sentinel ID is 592910408e0a58fe14a763e727c7926e2a3e1c63
5175:X 05 Apr 04:48:57.988 # +monitor master mymaster 127.0.0.1 6380 quorum 2
5175:X 05 Apr 04:48:57.990 * +slave slave 127.0.0.1:6381 127.0.0.1 6381 @ mymaster 127.0.0.1 6380
5175:X 05 Apr 04:48:57.994 * +slave slave 127.0.0.1:6382 127.0.0.1 6382 @ mymaster 127.0.0.1 6380
这时将主节点shutdown,观察哨兵日志:
8486:X 05 Apr 05:54:43.357 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128.
8486:X 05 Apr 05:54:43.357 # Sentinel ID is 7f4b89cd0ad9cf11094f826fca638ff290a858a7
8486:X 05 Apr 05:54:43.357 # +monitor master mymaster 127.0.0.1 6380 quorum 2
8486:X 05 Apr 05:54:43.359 * +slave slave 127.0.0.1:6381 127.0.0.1 6381 @ mymaster 127.0.0.1 6380
8486:X 05 Apr 05:54:43.362 * +slave slave 127.0.0.1:6382 127.0.0.1 6382 @ mymaster 127.0.0.1 6380
8486:X 05 Apr 05:54:52.504 * +sentinel sentinel 5d49f583b4411b1020a409902641908f855b0811 127.0.0.1 26381 @ mymaster 127.0.0.1 6380
8486:X 05 Apr 05:55:03.948 * +sentinel sentinel 168a2a27b82a80877ee155ae15dca0838ff1b74b 127.0.0.1 26382 @ mymaster 127.0.0.1 6380
8486:X 05 Apr 05:56:00.755 # +sdown master mymaster 127.0.0.1 6380 ## 发现mater下线
8486:X 05 Apr 05:56:00.828 # +new-epoch 1 ## 新纪元由0设置为1
8486:X 05 Apr 05:56:00.831 # +vote-for-leader ## 选举新的master 5d49f583b4411b1020a409902641908f855b0811 1
8486:X 05 Apr 05:56:00.831 # +odown master mymaster 127.0.0.1 6380 #quorum 2/2
8486:X 05 Apr 05:56:00.831 # Next failover delay: I will not start a failover before Sun Apr 5 06:02:00 2020 ##在当前哨兵故障迁移超时范围内,不会再由其他哨兵重复故障迁移
8486:X 05 Apr 05:56:01.556 # +config-update-from sentinel 5d49f583b4411b1020a409902641908f855b0811 127.0.0.1 26381 @ mymaster 127.0.0.1 6380
8486:X 05 Apr 05:56:01.556 # +switch-master mymaster 127.0.0.1 6380 127.0.0.1 6381
### 故障迁移后 master由6380改为了6381
8486:X 05 Apr 05:56:01.556 * +slave slave 127.0.0.1:6382 127.0.0.1 6382 @ mymaster 127.0.0.1 6381 ## 发现了一个从节点 6382
8486:X 05 Apr 05:56:01.556 * +slave slave 127.0.0.1:6380 127.0.0.1 6380 @ mymaster 127.0.0.1 6381 ## 发现了一个从节点 6380,即原来的主节点已经变成当前master6381的从节点了
8486:X 05 Apr 05:56:31.567 # +sdown slave 127.0.0.1:6380 127.0.0.1 6380 @ mymaster 127.0.0.1 6381 ## 并且标记该从节点为下线状态.
可以看到,在master下线后,经过选举新的master并进行故障迁移failover,原来的master节点由6380变为了6381,这时再去查看6381的主从信息,发现就已经变成了master。并且取消了从服务的只读模式,可以进行写操作了。
127.0.0.1:6381> info replication
# Replication
role:master
connected_slaves:1
....
127.0.0.1:6381> set master 6381
OK
此时再次上线原先的master服务6380,查看哨兵日志:
8486:X 05 Apr 06:08:20.753 # -sdown slave 127.0.0.1:6380 127.0.0.1 6380 @ mymaster 127.0.0.1 6381 ## 取消从节点6380的下线标志,即发现6380从节点已上线,主节点依然是故障迁移后的6381
再进入6380服务中确认
[root@localhost redis]# ./bin/redis-cli -a ryujung -p 6380 ## 进入6380服务.
127.0.0.1:6380> info replication
# Replication
role:slave ## 当前服务的角色为slaver
master_host:127.0.0.1 ## master的host地址
master_port:6381 ## mater端口为6381
master_link_status:up ## master状态为上线
127.0.0.1:6380> set master 6380 ## 并且已经设置为只读模式,无法进行写操作.
(error) READONLY You can't write against a read only slave.
哨兵模式配置
简单版
port 26379
daemonize yes
pidfile /var/run/redis-sentinel.pid
dir /usr/local/redis/redis-5.0.4/data/
logfile "sentinel-26379.log"
# 2 的意思是当两个 sentinel monitor发现master有问题时则认定该master有问题
sentinel monitor mymaster 127.0.0.1 7000 2
#ping了30000毫秒还没ping通则认定master有问题
sentinel down-after-milliseconds mymaster 30000
#选择新的master后,slave要对新的master进行复制,1表示并发,每次复制一个,减轻master压力
sentinel parallel-syncs mymaster 1
#180000表示故障转移超时时间(毫秒)
sentinel failover-timeout mymaster 180000
sentinel deny-scripts-reconfig yes
全量版
# Example sentinel.conf
# 哨兵sentinel实例运行的端口 默认26379
port 26379
# 哨兵sentinel的工作目录
dir /tmp
# 哨兵sentinel监控的redis主节点的 ip port
# master-name 可以自己命名的主节点名字 只能由字母A-z、数字0-9 、这三个字符".-_"组成。
# quorum 当这些quorum个数sentinel哨兵认为master主节点失联 那么这时 客观上认为主节点失联了
# sentinel monitor <master-name> <ip> <redis-port> <quorum>
sentinel monitor mymaster 127.0.0.1 6379 2
# 当在Redis实例中开启了requirepass foobared 授权密码 这样所有连接Redis实例的客户端都要提供密码
# 设置哨兵sentinel 连接主从的密码 注意必须为主从设置一样的验证密码
# sentinel auth-pass <master-name> <password>
sentinel auth-pass mymaster MySUPER--secret-0123passw0rd
# 指定多少毫秒之后 主节点没有应答哨兵sentinel 此时 哨兵主观上认为主节点下线 默认30秒
# sentinel down-after-milliseconds <master-name> <milliseconds>
sentinel down-after-milliseconds mymaster 30000
# 这个配置项指定了在发生failover主备切换时最多可以有多少个slave同时对新的master进行 同步,
这个数字越小,完成failover所需的时间就越长,
但是如果这个数字越大,就意味着越 多的slave因为replication而不可用。
可以通过将这个值设为 1 来保证每次只有一个slave 处于不能处理命令请求的状态。
# sentinel parallel-syncs <master-name> <numslaves>
sentinel parallel-syncs mymaster 1
# 故障转移的超时时间 failover-timeout 可以用在以下这些方面:
#1. 同一个sentinel对同一个master两次failover之间的间隔时间。
#2. 当一个slave从一个错误的master那里同步数据开始计算时间。直到slave被纠正为向正确的master那里同步数据时。
#3.当想要取消一个正在进行的failover所需要的时间。
#4.当进行failover时,配置所有slaves指向新的master所需的最大时间。不过,即使过了这个超时,slaves依然会被正确配置为指向master,但是就不按parallel-syncs所配置的规则来了
# 默认三分钟
# sentinel failover-timeout <master-name> <milliseconds>
sentinel failover-timeout mymaster 180000
# SCRIPTS EXECUTION
#配置当某一事件发生时所需要执行的脚本,可以通过脚本来通知管理员,例如当系统运行不正常时发邮件通知相关人员。
#对于脚本的运行结果有以下规则:
#若脚本执行后返回1,那么该脚本稍后将会被再次执行,重复次数目前默认为10
#若脚本执行后返回2,或者比2更高的一个返回值,脚本将不会重复执行。
#如果脚本在执行过程中由于收到系统中断信号被终止了,则同返回值为1时的行为相同。
#一个脚本的最大执行时间为60s,如果超过这个时间,脚本将会被一个SIGKILL信号终止,之后重新执行。
#通知型脚本:当sentinel有任何警告级别的事件发生时(比如说redis实例的主观失效和客观失效等等),将会去调用这个脚本,
这时这个脚本应该通过邮件,SMS等方式去通知系统管理员关于系统不正常运行的信息。调用该脚本时,将传给脚本两个参数,
一个是事件的类型,
一个是事件的描述。
如果sentinel.conf配置文件中配置了这个脚本路径,那么必须保证这个脚本存在于这个路径,并且是可执行的,否则sentinel无法正常启动成功。
#通知脚本
# sentinel notification-script <master-name> <script-path>
sentinel notification-script mymaster /var/redis/notify.sh
# 客户端重新配置主节点参数脚本
# 当一个master由于failover而发生改变时,这个脚本将会被调用,通知相关的客户端关于master地址已经发生改变的信息。
# 以下参数将会在调用脚本时传给脚本:
# <master-name> <role> <state> <from-ip> <from-port> <to-ip> <to-port>
# 目前<state>总是“failover”,
# <role>是“leader”或者“observer”中的一个。
# 参数 from-ip, from-port, to-ip, to-port是用来和旧的master和新的master(即旧的slave)通信的
# 这个脚本应该是通用的,能被多次调用,不是针对性的。
# sentinel client-reconfig-script <master-name> <script-path>
sentinel client-reconfig-script mymaster /var/redis/reconfig.sh
哨兵模式优缺点
优点:
- 哨兵集群,基于主从复制模式。
- 主从可以切换,故障可以自动转移,系统的可用性很高
- 哨兵模式就是主从模式的升级,由手动到自动, 更加健壮。
缺点:
- Redis在线扩容比较麻烦,集群一旦达到上线,在线扩容很复杂,很麻烦。
- 实现哨兵模式的配置其实很麻烦的,配置项目很多。