学习资料:
【狂神说Java】Redis最新超详细版教程通俗易懂_哔哩哔哩_bilibili
(72条消息) Redis(基于狂神说Java-Redis)_你给我把被子盖好了,别再踢了的博客-CSDN博客_狂神说redis
Redis 安装 | 菜鸟教程 (runoob.com)
学习目的
Redis等关系型数据库作缓存去缓存使用频率高的数据,减轻Mysql等关系型数据库的压力
NoSQL翻译为Not Only SQL,译为不仅仅是SQL,意指非关系型数据库
web2.0的诞生,传统的关系型数据库已经很难对付web2.0时代!特别是指大规模高并发社区!会出现很多问题,NoSQL在大数据时代发展的十分迅速,尤其是Redis
很多的数据类型用户的个人信息,社交网络,地理位置,这些数据类型的存储不需要一个固定的格式,不需要太多操作就可以实现横向拓展,就比如Redis,它是使用类似于Java的Map
1、方便扩展(数据之间没有联系可以快速拓展)
2、大数据量高性能,Redis可以支持8w的并发量(写操作),11w访问量(读操作),NoSQL的缓存记录级,是一种细粒度的缓存,性能比较高
3、数据类型多样性(不需要事先设计数据库,随取随用,数据量过大就无法设计)
4、传统的关系数据库管理系统(Relational Database Management System:RDBMS)和NoSQL的区别
关系型数据库与非关系型数据库对比
传统的RDBMS(关系型数据库)
NoSQL(非关系型数据库)
5、大数据时代的3V + 3高
大数据时代的3V
高并发
实际 NoSql+关系型数据库 一起使用
# 商品的基本信息
名称、价格、商家信息
MySQL / Oracle 去IOE化(IOE:IBM、Oracle、EMC存储设备)
# 商品描述
评论,文本信息多
文档型数据库,MongoDB
# 图片
分布式文件系统 FastDFS
淘宝:TFS
Google:GFS
Hadoop:HDFS
阿里云:OSS
# 商品关键字(搜索)
搜索引擎 solr elasticsearch
淘宝:ISearch,ISearch作者,阿里的多隆
# 商品热门波段信息
内存数据库
Redis Tair Memcached...
# 商品交易,外部支付接口
第三方应用
1、KV键值对
2、文档型数据库(Bson,Binary Json,二进制Json)
MongoDB,需要掌握,它是一种基于分布式文件存储的数据库,由C++编写,主要用来处理大量的文档
MongoDB 是一种介于关系型数据库和非关系型数据库之间的一种中间产品,功能丰富,而且MongoDB是NoSQL中最像关系型数据库的产品
ConthDB
3、列存储数据库
4、图形关系数据库
4种分类的对比
本次使用的Redis的版本采用的是5.0.10
什么是Redis?
Redis(Remote Directory Server),中文译为远程字典服务,免费开源,由C语言编写,支持网络,可基于内存也可持久化的日志型,KV键值对数据库,并且提供多种语言的API,是当下NoSQL中最热门的技术之一!被人们称之为结构化数据库!
Redis能干嘛?
…
特性
数据类型多样
持久化
Redis集群
事务
…
官网:https://redis.io/
Redis中文文档:http://www.redis.cn/documentation.html
下载地址:进入官网下载即可(Windows版本需要在GitHub上下载,并且Redis版本已停更较长时间,不建议使用)
并且,Redis官方推荐在Linux服务器上进行搭建
D:\tool\Redis-x64-5.0.14
文件 | 作用 |
---|---|
redis-benchmark.exe | 测试性能 |
redis-check-aof.exe | 检查AOF持久化 (Redis支持两种持久化方式,RDB / AOF) |
redis-server.exe | 启动服务 |
redis-cli.exe | 客户端 |
1、运行服务
双击redis-server.exe 启动Redis服务 或 打开一个 cmd 窗口 使用 cd 命令切换目录运行:
redis-server.exe redis.windows.conf
这时候另启一个 cmd 窗口,原来的不要关闭,不然就无法访问服务端了。
2、连接Redis
使用 Redis客户端(redis-cli.exe)连接Redis服务
切换到 redis 目录下运行:
redis-cli.exe -h 127.0.0.1 -p 6379
或
redis-cli
3、连接成功
D:\tool\Redis-x64-5.0.14
λ redis-cli.exe
127.0.0.1:6379>
4、使用
127.0.0.1:6379> ping #测试连接
PONG
127.0.0.1:6379> set name zs
OK
127.0.0.1:6379> get name
"zs"
127.0.0.1:6379>
安装Redis的第一种,官网下载安装包
1、下载安装包,redis-5.0.10.tar.gz
2、下载到Windows之后,用Xftp工具上传至Linux
3、解压安装包并将其解压
tar -zxvf redis-5.0.4.tar.gz
并且解压之后可以看见Redis的配置文件redis.conf
4、同时还需要基本的环境搭建
# 保证Redis的正常运行,gcc的安装也是必要的
yum install gcc-c++
# 查看版本
g++ -v
# 安装Redis所需要的环境
make
# 此命令只是为了确认当前所有环境全部安装完毕,可以选择不执行
make install
Redis的安装,默认在/usr/local/bin
下
5、之后,需要将Redis的配置文件复制到bin目录下,可以提前准备好一个目录,然后在复制到新创建好的目录中
[root@192 opt]# cd /opt/redis-5.0.4/
[root@192 redis-5.0.4]# ls
00-RELEASENOTES CONTRIBUTING deps Makefile README.md runtest runtest-sentinel src utils
BUGS COPYING INSTALL MANIFESTO redis.conf runtest-cluster sentinel.conf tests
[root@192 redis-5.0.4]# cp redis.conf /usr/local/bin/myconfig/
[root@192 redis-5.0.4]#
6、然后修改复制之后的配置文件,修改一条信息,修改的信息就是图中划红线的位置,它的意思是指守护进程模式启动,即可以在后台运行Redis
vim 搜索 方法:/str
daemonize
7、随后就可以开始启动Redis服务(通过指定的配置文件启动服务)
[root@192 bin]# cd /usr/local/bin
[root@192 bin]# redis-server /usr/local/bin/myconfig/redis.conf
8、连接
/usr/local/bin/
[root@192 bin]# redis-cli -p 6379
127.0.0.1:6379> ping
PONG
127.0.0.1:6379> set age 23
OK
127.0.0.1:6379> get age
"23"
127.0.0.1:6379> keys *
1) "age"
9 、查看进程运行
[root@192 ~]# ps -ef|grep redis
root 2057 1967 0 16:21 pts/0 00:00:00 grep --color=auto redis
[root@192 bin]# redis-server /usr/local/bin/myconfig/redis.conf # 启动Redis服务
#...
[root@192 bin]# ps -ef|grep redis
root 2063 1 1 16:25 ? 00:00:00 redis-server 127.0.0.1:6379
root 2069 1967 0 16:25 pts/0 00:00:00 grep --color=auto redis
[root@192 bin]#
10、关闭服务
127.0.0.1:6379> shutdown
127.0.0.1:6379> shutdown
not connected> exit
[root@192 bin]# ps -ef|grep redis
root 2074 1967 0 16:29 pts/0 00:00:00 grep --color=auto redis
有时候会有中文乱码。
要在 redis-cli 后面加上 --raw
redis-cli --raw
就可以避免中文乱码了
[root@192 bin]# redis-cli --raw
127.0.0.1:6379> set name 张三
OK
127.0.0.1:6379> get name
张三
redis-benchmark性能测试工具
# 当前命令表示,性能测试,在本机,端口号6379,并发连接数100,每个连接10w个请求数量
redis-benchmark -h localhost -p 6379 -c 100 -n 100000
[root@192 bin]# redis-benchmark -h localhost -p 6379 -c 100 -n 100000
# 测试结果如下,以Redis的INLINE命令为例
====== PING_INLINE ======
100000 requests completed in 5.24 seconds # 十万个请求在5.24秒之内被处理
100 parallel clients # 每次请求都有100个客户端在执行
3 bytes payload # 一次处理3个字节的数据
keep alive: 1 # 每次都保持一个服务器的连接,只用一台服务器处理这些请求
0.37% <= 1 milliseconds
13.17% <= 2 milliseconds
# ...
100.00% <= 104 milliseconds # 所有的请求在104秒之内完成
19076.69 requests per second # 平均每秒处理19076.69个请求
====== PING_BULK ====== # set、get、ping等每个命令都会测试
100000 requests completed in 4.45 seconds
100 parallel clients
3 bytes payload
keep alive: 1
0.31% <= 1 milliseconds
# ...
备注:在Redis中,关键字语法不区分大小写!
Redis有16个数据库支持,为啥嘞,可以查看redis.conf配置文件
并且初始数据库默认使用0号数据库(16个数据库对应索引0到15)
127.0.0.1:6379> select 12
OK
127.0.0.1:6379[12]> select 0
OK
127.0.0.1:6379> dbsize # 查看当前库的key数量
(integer) 0
127.0.0.1:6379> keys *
1) "myset:__rand_int__"
2) "mylist"
3) "key:__rand_int__"
4) "name"
5) "counter:__rand_int__"
127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> keys *
(empty list or set)
127.0.0.1:6379>
# 还有一个清空的命令,叫做flushall,它的意思是清空16个数据库中的全部信息。
# 不管在那种数据库中,清空库一直都是需要慎重操作的
题外话:为什么Redis选用6379作为默认端口号?
6379在是手机按键上MERZ对应的号码,而MERZ取自意大利歌女Alessia Merz的名字。MERZ长期以来被Redis作者antirez及其朋友当作愚蠢的代名词。后来Redis作者在开发Redis时就选用了这个端口。(摘自知乎)
Redis是单线程的(从Redis6.0.1开始支持多线程)
Redis的读写速度很快,官方表示,Redis基于内存操作,CPU不是Redis的性能瓶颈,Redis的性能瓶颈是根据机器的内存和带宽
Redis是C语言编写,官方提供的数据为10万+的QPS(Queries-Per-Second,每秒内的查询次数)
Redis单线程为什么速度还是这么快?
对于Redis,有两个误区:
1、高性能的服务器一定是多线程的?
2、多线程一定比单线程效率高?
Redis将所有的数据全部放在内存中,使用单线程去操作效率比较高,对于多线程,CPU有一种东西叫做上下文切换,这种操作耗时,对于内存系统来说,没有上下文切换,效率一定是最高的。
Redis使用单进程的模式来处理客户端的请求,对大部分事件的响应都是通过epoll函数的加强封装,Redis的实际处理速度依靠主进程的执行效率,epoll可以显著提高程序在大量并发连接中系统的CPU利用率
Redis中文网翻译:
Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。 它支持多种类型的数据结构,如 字符串(strings), 散列(hashes), 列表(lists), 集合(sets), 有序集合(sorted sets) 与范围查询, bitmaps, hyperloglogs 和 地理空间(geospatial) 索引半径查询。 Redis 内置了 复制(replication),LUA脚本(Lua scripting), LRU驱动事件(LRU eviction),事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis哨兵(Sentinel)和自动 分区(Cluster)提供高可用性(high availability)。
基础语法(命令小写) | 作用 |
---|---|
set key value | 设置一个key |
get key | 获取一个key对应value |
exists key | 查询key是否存在 |
move key n(n是数字) | 将当前key移动到指定的几号数据库中 |
keys * | 查询当前数据库中全部的key |
expire key time | 设置当前key的过期时间 |
ttl key | 查询当前key的存活时间 |
type key | 查看key的数据类型 |
flushdb | 清空当前数据库信息(慎用) |
flushall | 空16个数据库中的全部信息(慎用) |
select n | 选择数据库 |
基础语法(命令大写):
SET key value 设置一个key
GET key 获取一个key对应value
EXISTS key 查询key是否存在
MOVE key n(数字) 将当前key移动到指定的几号数据库中
KEYS * 查询当前数据库中全部的key
EXPIRE key time 设置当前key的过期时间
TTL key 查询当前key的存活时间
TYPE key 查看key的数据类型
1、set key value
127.0.0.1:6379> set name 张三 # 设置key-value
OK
127.0.0.1:6379> get name # 查询key指定的value
张三
2、exists key
127.0.0.1:6379> exists name # 查看当前key是否存在
1
127.0.0.1:6379> exists name1
0
3、move
127.0.0.1:6379> move name 1
1
127.0.0.1:6379> KEYS *
127.0.0.1:6379> select 1
OK
127.0.0.1:6379[1]> keys *
name
4、expire key time
127.0.0.1:6379[1]> expire name 5 # 设置当前key的过期时间,单位是秒
1
127.0.0.1:6379[1]> ttl name # 查看指定key的存活时间,
-2 # 返回-2表示当前key已经过期,如果为-1,表示永不过期
127.0.0.1:6379> KEYS * # name 过期被清空
(empty list or set)
5、type key
127.0.0.1:6379[1]> set name 张三
OK
127.0.0.1:6379[1]> type name
string
以下命令记忆小写(命令提示大写),一是好记,二是java中与redis相关的方法与小写命令类似
语法 | 作用 |
---|---|
append key Value | 对指定key实现字符串拼接,如果key不存在,等同于set |
strlen key | 查看指定key的长度 |
incr key | 对指定key进行自增,类似于Java中的i++ |
decr key | 自减,类似于Java的i– |
incrby key n | 对指定key按照指定的步长值进行自增 |
decrby key n | 按照指定的步长值自减 |
setrange key index value | 从指定key的索引开始,插入指定的value值。 |
getrange key index value | 将指定key按照索引的开始和结束范围进行截取,成为一个新的key |
setex key time value | 设置一个有存活时间的key |
setex key value | 如果这个key不存在,即创建 |
mset key value … | 设置多个key value |
mget key value | 获取多个key指定的value |
getset key value | 先获取指定的key,然后再设置指定的value |
# 语法:
APPEND key appendValue # 对指定key实现字符串拼接,如果key不存在,等同于set
STRLEN key # 查看指定key的长度
INCR key # 对指定key进行自增,类似于Java中的i++
DECR key # 自减,类似于Java的i--
INCRBY key n # 对指定key按照指定的步长值进行自增
DECRBY key n # 按照指定的步长值自减
SETRANGE key index value # 从指定key的索引开始,插入指定的value值。如果key不存 在且索引>1,那么当前的索引之前的数据,会用\x00代替并占用一个索引位置,相当于ASCII码中的null
GETRANGE key startIndex endInde #将指定key按照索引的开始和结束范围进行截取,成为一个新的key
SETEX key time value # 设置一个有存活时间的key
SETNX key value # 如果这个key不存在,即创建
MSET key value ... #设置多个key value
MGET key ... #获取多个key指定的value
GETSET key value # 先获取指定的key,然后再设置指定的value
1、简单使用
127.0.0.1:6379[1]> set name ab
OK
127.0.0.1:6379[1]> append ab cd
2
127.0.0.1:6379[1]> get name
ab
127.0.0.1:6379[1]> strlen name
2
2、实现自增自减效果
127.0.0.1:6379[1]> incr age
26
127.0.0.1:6379[1]> decr age
25
127.0.0.1:6379[1]> incrby age 10
35
127.0.0.1:6379[1]> decrby age 24
11
3、实现字符串截取效果
127.0.0.1:6379> set k1 hello,xiaohuang
OK
127.0.0.1:6379> get k1
"hello,xiaohuang"
127.0.0.1:6379> GETRANGE k1 0 3 # 实现字符串截取,有起始索引和结束索引,相当于Java中的subString()
"hell"
# 如果结束索引为-1,则表示当前截取的字符串为全部
127.0.0.1:6379> GETRANGE k1 0 -1
"hello,xiaohuang"
4、实现字符串的替换效果
127.0.0.1:6379> set key2 abcdefg
OK
127.0.0.1:6379> get key2
"abcdefg"
127.0.0.1:6379> SETRANGE key2 2 hello # 实现字符串的替换效果,命令中的数字“2”表示从索引2的位置开始将其替换为指定字符串
(integer) 7
127.0.0.1:6379> get key2
"abhello"
5、设置过期时间
# setex(set with expire) # 设置过期时间
# setnx(set with not exist) # 如果key不存在,创建(分布式锁中常用)
127.0.0.1:6379[1]> setex name 10 张三
OK
127.0.0.1:6379[1]> keys *
name
127.0.0.1:6379[1]> get name
127.0.0.1:6379[1]> keys *
127.0.0.1:6379> setnx lan redis # 如果key不存在,即创建
(integer) 1
127.0.0.1:6379> setnx lan mongodb
(integer) 0 # 0表示没有设置成功,也可理解为“有0行受到影响”
127.0.0.1:6379> get lan
"redis"
6、一次性设置(获取)多个键值对
127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3 # 同时设置多个值
OK
127.0.0.1:6379> KEYS *
1) "k2"
2) "k1"
3) "k3"
127.0.0.1:6379> mget k1 k2 k3 # 同时获取多个值
1) "v1"
2) "v2"
3) "v3"
# 也可以在这边的语法前面加上一个m,代表设置多个
127.0.0.1:6379> msetnx k1 vv1 k4 v4
(integer) 0
# 但是这边同时设置多个值,如果有一个key已经存在,那么这一条设置语句便不会执行成功,
# 因为Redis单条语句是原子操作,要么同时成功,要么同时失败
127.0.0.1:6379> keys *
1) "k2"
2) "k1"
3) "k3"
# 在Redis中,还推荐了一个比较有意思的东西
# 这是Redis中关于key的命名,可以用“:”来代表层次结构,可以对指定的key进行分类存储
127.0.0.1:6379> mset user:1:name xiaohuang user:1:age 21
OK
127.0.0.1:6379> mget user:1:name user:1:age
1) "xiaohuang"
2) "21"
127.0.0.1:6379> getset sqlan redis # 先获取当前key指定的value,如果不存在,会返回nil(null),然后设置新值
(nil)
127.0.0.1:6379> get sqlan
"redis"
127.0.0.1:6379> getset sqlan hbase
"redis"
127.0.0.1:6379> get sqlan
"hbase"
类似于Redis中String这样的使用场景,value值可以是字符串,也可以是其他类型
String的存储的字符串长度最大可以达到512M
主要用途
Redis中的List列表可以做很多事情,可以将其看成数据结构中的栈,也可以是队列,或者阻塞队列
命令 | 作用 | 关键字 |
---|---|---|
LPUSH key value1 value2 … | 设置一个key,从头部插入数据(头插法) | lpush |
RPUSH key value1 value2 … | 设置一个key,从尾部插入数据(尾插法) | rpush |
LRANGE key startIndex endIndex | 返回列表中从开始索引到结束索引位置的value值 | lrange |
LPOP key | 从key头部弹出一个value | lpop |
RPOP key | 从尾部弹出一个value | rpop |
LINDEX index | 返回key中指定索引的value值 | lindex |
LREM key n value | 删除key中的指定的value值,n代表删除几个 | lrem |
LLEN key | 返回key的长度 | llen |
LTRIM key startIndex endIndex | 截取key,截取的范围从开始索引到结束索引 | ltrim |
LSET key index value | 从当前key的索引开始插入指定的value值 | lset |
RPOPLPUSH key1 key2 | 从key1的尾部弹出一个元素,将此元素从key2的头部插入 | (转移) |
LINSERT key BEFORE | 从指定key中已存在的value的前面插入一个指定的value | |
AFTER oldValue newValue | 从指定key后面插入一个指定的value |
1、插入
127.0.0.1:6379> lpush mylist a b c # 头插法
3
127.0.0.1:6379> lrange mylist 0 2 # 查看
c
b
a
127.0.0.1:6379> RPUSH list2 a b c #尾插法
3
127.0.0.1:6379> lrange list2 0 2
a
b
c
127.0.0.1:6379> llen list2 # 返回长度
2
2、弹出
127.0.0.1:6379> lpop mylist # 头出
c
127.0.0.1:6379> lrange mylist 0 2
b
a
127.0.0.1:6379> rpop list2 #尾出
c
127.0.0.1:6379> lrange list2 0 2
a
b
127.0.0.1:6379> lindex list2 1 # 定位
b
127.0.0.1:6379> lpush list4 a b c d
4
127.0.0.1:6379> LREM list4 2 b # 删除
1
127.0.0.1:6379> lrange list4 0 8
d
c
a
3、截取
127.0.0.1:6379> lpush list3 a b c d
4
127.0.0.1:6379> ltrim list3 1 2 # 截取
OK
127.0.0.1:6379> lrange list3 0 8
c
b
127.0.0.1:6379> lpush list4 a b c d
4
127.0.0.1:6379> RPOPLPUSH list3 list4 # 转移
sss
127.0.0.1:6379> lrange list4 0 8
sss
d
c
b
a
4、指定value插入
127.0.0.1:6379> lpush list5 a b c d
4
127.0.0.1:6379> lrange list5 0 -1
d
c
b
a
127.0.0.1:6379> lrange list5 0 8
d
c
b
a
127.0.0.1:6379> linsert list5 before b ssr # 从指定 value值的前面 插入
5
127.0.0.1:6379> lrange list5 0 8
d
c
ssr
b
a
127.0.0.1:6379> linsert list5 after b ddt # 从指定 value值的后面 插入
7
127.0.0.1:6379> lrange list5 0 8
d
c
ssr
b
ddt
a
set集合无序不重复
命令 | 作用 | 关键词 |
---|---|---|
SADD key value1 value2 … | 设置一个key | sadd |
SMEMBERS key | 查看当前key | smembers |
SISMEMBER key value | 查看key中指定的value是否存在 | sismember |
SCARD key | 查看key的长度 | scard |
SREM key value | 删除key中的指定value | srem |
SPOP key | 随机删除key中的一个value | spop |
SRANDMEMBER key [n] | 随机查看当前key中的一个或者多个value | srandmember |
SMOVE key1 key2 key1Value | 将key1中的value移动到key2中 | smove |
SDIFF key1 key2 | 两个key相交,求第一个key的补集 | sdiff |
SINTER key1 key2 | 两个key相交,求交集 | sinter |
SUNION key1 key2 | 两个key相交,求并集 | sunion |
1、设置set
127.0.0.1:6379> sadd set1 a b c # 向集合添加元素
3
127.0.0.1:6379> smembers set1 # 查看所有
c
b
a
127.0.0.1:6379> sadd set1 a b c # 集合无序不重复
0
127.0.0.1:6379> sadd set1 d
1
127.0.0.1:6379> smembers set1
d
c
b
a
127.0.0.1:6379> scard set1 # 查看key的长度
4
2、删除
127.0.0.1:6379> sismember set1 a # 查看key中指定的value是否存在
1 # 1 表示存在
127.0.0.1:6379> sismember set1 s
0 # 1 表示不存在
127.0.0.1:6379> SMEMBERS set1 # 查看所有
d
c
b
a
127.0.0.1:6379> srem set1 d # 指定的value
1
127.0.0.1:6379> spop set1 # 随机删除
c
127.0.0.1:6379> SMEMBERS set1
b
a
127.0.0.1:6379> sadd set2 f g
2
127.0.0.1:6379> smove set2 set1 f # 移动
1
127.0.0.1:6379> SMEMBERS set1
f
b
a
127.0.0.1:6379> SRANDMEMBER set1 # 随机抽取
f
2、集合
生活中的一个小现象,就比如说微信公众号,会有共同关注,还有QQ的共同好友
数学集合关系中的:交、并、补。微信公众号中的共同关注,以及QQ的共同好友,就是关系中的交!
127.0.0.1:6379> sadd set3 a b c
3
127.0.0.1:6379> sadd set4 a c d
3
127.0.0.1:6379> sdiff set3 set4 # 求第一个key的补集
b
127.0.0.1:6379> sinter set3 set4 # 求交集
c
a
127.0.0.1:6379> sunion set3 set4 # 求交集
c
b
a
d
命令都可以在英语单词中找到一些规律
把SDIFF、SINTER还有SUNION这三个单词首字母去掉,可以得到
DIFF:different,它代表不同的,用一句Redis官网的翻译来描述:返回的集合元素是第一个key的集合与后面所有key的集合的差集
INTER:intersection,翻译过来为交叉,同样的,意指数学关系中的交集
UNION:union,翻译为联合,与数学关系中的并集也是可以沾边的
Redis中的哈希,本质上KV相同但是KV中的V,它也是一个键值对,本质和操作字符串区别不大
命令 | 作用 | 关键字 |
---|---|---|
HSET key field value | 设置单个hash | hset |
HGET key field | 获取单个 | hget |
HMSET key field1 v1 field2 v2 | 设置多个 | hmset |
HMGET key field | 获取多个 | hmget |
HGETALL key | 获取hash中全部的field-value | hgetall |
HLEN key | 获取hash长度 | hlen |
HEXISTS key field | 查询hash中指定的field是否存在 | hexists |
HKEYS key | 只获取hash中的field | hkeys |
HVALS key | 只获取hash中value | hvals |
HINCRBY key field n | 对hash中指定的field设置自增自减 | hincrby |
1、创建hash
127.0.0.1:6379> hset hash1 name zs # 设置单个 hash
1
127.0.0.1:6379> hget hash1 name # 获取单个 hash
zs
127.0.0.1:6379> hmset hash2 name joy age 23 # 设置多个
OK
127.0.0.1:6379> hmget hash2 name age # 获取多个
joy
23
2、获取
127.0.0.1:6379> hgetall hash1 # 获取hash中全部的 键值对
name
zs
127.0.0.1:6379> hlen hash1 # 获取长度
1
127.0.0.1:6379> hkeys hash2 # 获取全部的键
name
age
127.0.0.1:6379> hvals hash2 # 获取全部的值
joy
23
127.0.0.1:6379> hset hash3 age 23
1
127.0.0.1:6379> hincrby hash3 age 25 # 自增23
48
3、使用“:”
可以使用hash做一些临时变更的数据,可以是用户信息,或者是经常变动的信息,上面的String也提到了使用“:”进行层次分割,不过hash更适合对象存储,String适合于文本的存储
127.0.0.1:6379> HMSET user:1 name xiaohuang age 21 sex boy
OK
127.0.0.1:6379> HGETALL user:1
1) "name"
2) "xiaohuang"
3) "age"
4) "21"
5) "sex"
6) "boy"
在set的基础上增加了一个score的值,相当于zset k1 score v1,使用score来对当前key中元素进行排序
命令 | 作用 | 关键字 |
---|---|---|
ZADD key score1 value1 score2 value2 … | zset中添加一个或多个元素 | zadd |
ZRANGE key startIndex endIndex | 查询从开始到结束索引的zset集合 | zrange |
ZRANGEBYSCORE key min max [WITHSCORES] | 对hash中按照指定数值进行升序排列 | |
ZREVRANGE key startIndex endIndex | 对指定开始和结束索引进行降序排列 | |
ZREM key field | 删除hash中指定的field | zrem |
ZCARD key | 查询hash长度 | zcard |
ZCOUNT key [min max] | 查询hash数量,还可以增加最大值和最小值的范围 | zcount |
1、
127.0.0.1:6379> zadd set1 1 one # 添加一个元素
1
127.0.0.1:6379> zadd set1 2 tew 3 three # 添加多个元素
2
127.0.0.1:6379> zrange set1 0 -1 # 查询从开始到结束索引的zset集合
one
tew
three
127.0.0.1:6379>
2、实现元素的排序
# 根据zset中score的值来实现元素的排序
127.0.0.1:6379> ZADD salary 3500 xiaohong 6500 xiaohuang 3900 zhangsan
(integer) 3
# 当前命令,inf在Unix系统中代表的意思是无穷,所以当前命令是指,将当前zset,以从小到大的形式进行排列
127.0.0.1:6379> ZRANGEBYSCORE salary -inf +inf
1) "xiaohong"
2) "zhangsan"
3) "xiaohuang"
127.0.0.1:6379> ZRANGEBYSCORE salary -inf +inf withscores # 在排列的同时,将score和指定的元素全部展示
1) "xiaohong"
2) "3500"
3) "zhangsan"
4) "3900"
5) "xiaohuang"
6) "6500"
127.0.0.1:6379> ZREVRANGE salary 0 -1 withscores # 将数据从大到小进行排列
1) "xiaohuang"
2) "6500"
3) "zhangsan"
4) "3900"
5) "xiaohong"
6) "3500"
127.0.0.1:6379> ZRANGEBYSCORE salary -inf 4000 withscores # 展示的同时还可以指示score的查询最大值,指定查询范围
1) "xiaohong"
2) "3500"
3) "zhangsan"
4) "3900"
127.0.0.1:6379> ZREM salary zhangsan # 删除zset中的一个元素
(integer) 1
127.0.0.1:6379> ZRANGE salary 0 -1
1) "xiaohong"
2) "xiaohuang"
127.0.0.1:6379> ZCARD salary
(integer) 2
3、
127.0.0.1:6379> ZADD myset 1 hello 2 world 3 xiaohuang 4 xiaohei 5 xiaolan
(integer) 5
# 语法:ZCOUNT key min max ,min和max包左也包右,它是一个闭区间
127.0.0.1:6379> ZCOUNT myset 2 5 # 获取指定区间的成员数量
(integer) 4
127.0.0.1:6379>
其他
其他的API,如果说在工作中出现了,可以查看Redis的官方文档:http://www.redis.cn/commands.html
案例:zset是Redis的数据类型,可以排序,生活中也有案例,班级成绩,员工工资
设置权重,1、普通消息;2、重要消息;添加权重进行消息判断其重要性
来一个更接地气的案例,可以打开B站,排行榜,B站会根据视频的浏览量和弹幕量进行综合评分,进行排名
微信朋友圈中的朋友的位置,或者是QQ中也有的附近的人,饿了么中外卖小哥的位置距离
这个在Redis中被定为特殊的数据类型可叫做Geo,它是Redis3.2正式推出的一个特性,可以推导出两个地方的地理位置,两地之间的距离,方圆几千米之内的人。
对于这个关于地理的数据类型,它有6个命令
关键词 | 命令 | 作用 |
---|---|---|
geoadd | GEOADD key 经度 纬度 城市名称 | 添加地理位置 |
geodist | GEODIST key member1 member2 [unit] | 指定两个位置的距离 |
geohash | GEOHASH key member | 返回一个或多个元素的GeoHash表示,该命令返回11个字符组成的GeoHash字符串 |
geopos | GEOPOS key member1 member2 … | 获取一个或多个地理信息 |
georadius | GEORADIUS key 经度 纬度 半径 [单位] [WITHCOORD(搜寻到的目标的经纬度)] [WITHDIST(直线距离)] [count] | 自己所在的地址为圆心,半径查找 |
georadiusbymember | GEORADIUSBYMEMBER key member 长度 [unit]单位 | 找出指定元素周围的其他元素,就是以城市为中心,一定长度为半径搜索 |
因为这个特殊的数据类型和地理相关,需要用到地理的经纬度,可以推荐一个网站查看指定城市的经纬度:http://www.jsons.cn/lngcode/
1、geoadd 添加地理位置
语法:GEOADD key 经度 纬度 城市名称 …
注意:南北极无法直接添加。用添加城市数据来说,一般都会使用Java的Jedis来操作,而这些城市数据都是被下载下来通过JavaAPI调用
有效经度从-180到180度
有效纬度从-85.05112878 到 85.05112878 度。超过范围会出现(error) ERR invalid longitude,latitude pair
127.0.0.1:6379> GEOADD china:city 116.40 39.90 beijing 121.47 31.23 shanghai # 添加
2
127.0.0.1:6379> GEOHASH china:city beijing # 返回GeoHash字符串
wx4fbxxfke0
127.0.0.1:6379> ZRANGE china:city 0 -1 # 使用zset命令查看geospatial
shanghai
beijing
127.0.0.1:6379> ZREM china:city beijing # 使用zset的基本命令即可删除
1
127.0.0.1:6379> ZRANGE china:city 0 -1
shanghai
2、geodist
单位:m表示单位米、km表示千米、mi表示英里、ft表示英尺
语法:GEODIST key member1 member2 [unit]
# 查看beijing和shanghai两个位置的直线距离
127.0.0.1:6379> GEOADD china:city 116.40 39.90 beijing 121.47 31.23 shanghai
1
127.0.0.1:6379> GEODIST china:city beijing shanghai # 单位米
1067378.7564
127.0.0.1:6379> GEODIST china:city beijing shanghai km # 单位千米
1067.3788
3、georadius
自己所在的地址为圆心,半径查找
对于社交软件来说,附近的人,就相当于,你现在所在的地址,再加上一定的半径来进行查找
GEORADIUS key 经度 纬度 半径 [单位] [WITHCOORD(搜寻到的目标的经纬度)] [WITHDIST(直线距离)] [count]
# 以111经度31纬度为中心,1000km为半径搜寻在器范围之内的城市
127.0.0.1:6379> GEORADIUS china:city 111 31 1000 km
1) "shenzhen"
2) "guangzhou"
3) "fuzhou"
4) "shanghai"
# 追加参数,目标经纬度,直线距离
127.0.0.1:6379> GEORADIUS china:city 111 31 1000 km WITHCOORD WITHDIST
# ...
# 还可以限制查询的结果条数,只显示两条
127.0.0.1:6379> GEORADIUS china:city 111 31 1000 km WITHCOORD WITHDIST count 2
1) 1) "guangzhou"
2) "905.0108"
3) 1) "113.27999979257583618"
2) "23.1199990030198208"
# ...
4、GEORADIUSBYMEMBER
找出指定元素周围的其他元素,就是以城市为中心,一定长度为半径搜索
# 找出以shanghai为中心,1000km为半径搜索
127.0.0.1:6379> GEORADIUSBYMEMBER china:city shanghai 1000 km
1) "xiamen"
2) "fuzhou"
3) "shanghai"
在讲Hyperloglog之前,什么是基数?
集合中包含的不重复元素即为基数,就比如一个A数据集,A{1,3.7,9,11},它的基数为5,可以接受误差
Hyperloglog是Redis2.8.9更新的,它是一种数据结构,主要是针对于基数统计的算法
优点,占用的内存很小,只需要使用12KB的内存即可统计2^64的数据
在实际业务中,网页的UV(Unique Visitor,独立访客),一个人访问一个网站多次,只能算作是一个,用传统的方式,set集合保存用户的id,然后统计set中元素个数作为标准来判断。使用这种方式来进行数据统计的话,大量的内存用来浪费给保存用户id了,目的是为了计数,而不是为了保存用户id
Hyperloglog计数的错误率在0.81%,用来执行UV任务,可以忽略不计
关键词 | 命令 | 作用 |
---|---|---|
pfadd | PFADD key value1 value2… | 创建一组数据集,如果数据集中有相同的元素就会有去重效果 |
pfcount | PFCOUNT key | 查看元素的长度 |
pfmerge | PFMERGE key3 key1 key2 | 将两组元素合并成一个新数组,并带有去重效果,相当于数学中的并集 |
127.0.0.1:6379> pfadd pf1 a b c d e f g h i j # 创建
1
127.0.0.1:6379> pfcount pf1 # 查看元素的长度
10
127.0.0.1:6379> pfadd pf2 1 2 3 4
1
127.0.0.1:6379> PFMERGE pf1 pf2 # 合并数组
OK
127.0.0.1:6379> pfcount pf1
14
如果在项目中允许容错,可以使用Hyperloglog
如果不行,就可以直接使用set或者Java的HashMap来实现
Bitmaps是一种位存储的数据类型,在Redis2.2.0被推出,
生活中可以实现的场景,统计活跃用户,在线状态,用户签到,这些都是表示两种状态之间的,可以使用Bitmaps
Bitmaps,译为位图,也是一种数据结构,操作二进制位进行记录,只有0和1两种状态。Bitmaps通过最小的单位bit来进行存储,表示元素对应的状态
关键词 | 命令 | 作用 |
---|---|---|
setbit | SETBIT key offset value | 设置一个key,在指定的offset位置上设置一个value,这个value只能是0或者1 |
getbit | GETBIT key offset | 获取指定key上的offset位的value值 |
bitcount | BITCOUNT key [start] [end] | 在指定key中计算被设置为 1 的比特位的数量。 |
bitop | BITOP operation destKey key1 key2 … | 对一个或者多个key进行二进制的逻辑运算 |
bitpos | BiTPOS key bit [start] [end] | 指定key中返回value中第一个出现0或1的offset |
1、
127.0.0.1:6379> setbit bit1 3 0
0
127.0.0.1:6379> getbit bit1 3
0
127.0.0.1:6379> setbit bit1 2 1
0
127.0.0.1:6379> bitcount bit1
1
# 在week中返回第一个出现1的value值
127.0.0.1:6379> BITPOS week 1 0 -1
4
2、BITOP逻辑运算
一共有4种逻辑运算,AND、OR、NOT、XOR,分别代表 并、或、非、异或
127.0.0.1:6379> SETBIT bit-1 0 1
(integer) 0
127.0.0.1:6379> BITOP AND and-bit bit1 bit2 # 对bit1和bit2进行并操作 得到 and-bit
# ...
127.0.0.1:6379> BITOP OR or-bit bit1 bit2 # 或 操作
# 对bit1进行 非 操作,注意:非操作只针对一个key
127.0.0.1:6379> BITOP NOT not-bit bit1
(integer) 1
# 对bit-1和bit-2进行 异或 操作
127.0.0.1:6379> BITOP XOR xor-bit bit-1 bit-2
备注:BITOP执行命令较慢,因为其时间复杂度为O(n)。
在进行计数时,如果数据量过大,建议直接将其指派到master-slave中的slave节点进行处理,避免阻塞
Redis单条命令保持原子性,但是Redis事务不保证原子性
原子性: 要么都成功,要么都失败
Redis事务本质:一组命令的集合(如我要先set 再 get,set,get这组命令就是事务)
Redis事务本质,可以将Redis的事务看成是一个队列,将一组命令进行“入队”,然后一起执行
一个事务中的所有命令都会被序列化,在事务执行的过程中,会按照顺序执行
Redis事务的三个特性:一致性,顺序性,排他性
关键字 | 命令 | 作用 |
---|---|---|
multi | MULTI | 开启事务 |
exec | EXEC | 执行事务 |
discard | DISCARD | 关闭事务(放弃事务) |
1、正常流程
127.0.0.1:6379> multi # 开启事务
OK
# === 事务入队中 ===
127.0.0.1:6379> set name zs
QUEUED # 表示命令入队,等待客户端执行事务
127.0.0.1:6379> append name joy
QUEUED # 命令入队
127.0.0.1:6379> get name
QUEUED # 命令入队
# === 事务入队结束 ===
127.0.0.1:6379> exec # 执行事务
OK
# === 执行结果 ===
5
zsjoy
2、放弃事务
127.0.0.1:6379> multi # 开启事务
OK
127.0.0.1:6379> set age 23
QUEUED
127.0.0.1:6379> append age 15
QUEUED
127.0.0.1:6379> get age
QUEUED
127.0.0.1:6379> discard # 放弃事务
OK
# 一旦放弃事务,之前入队的全部命令都不会执行
(nil)
127.0.0.1:6379> get age # 无结果
(nil)
3、编译型异常
编译型异常(又叫入队错误)的特点:事务中有错误的命令,会导致默认放弃事务,所有的命令都不会执行
127.0.0.1:6379> multi # 开启事务
OK
127.0.0.1:6379> set name zs
QUEUED
127.0.0.1:6379> append name2 # 错误的命令
ERR wrong number of arguments for 'append' command
127.0.0.1:6379> get name
QUEUED
127.0.0.1:6379> EXEC # 执行事务,出现编译型异常
EXECABORT Transaction discarded because of previous errors.
4、运行时异常
运行时异常(又叫执行错误):在事务执行的过程中语法没有出现任何问题,但是它对不同类型的key执行了错误的操作,
Redis只会将返回的报错信息包含在执行事务的结果中,并不会影响Redis事务的一致性
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set name zs
QUEUED
127.0.0.1:6379> incr name # 语法正确,但是对一个String类型的k1执行了错误的操作
QUEUED
127.0.0.1:6379> get name
QUEUED
127.0.0.1:6379> exec # 执行事务,出现运行时异常
OK # 执行ok
ERR value is not an integer or out of range # 命令报错,但是不影响事务整体的运行
zs # 依旧执行
监控
悲观锁:
乐观锁
1、首先先模拟正常状态
语法:watch key … : 对指定key进行监控,监控这个key在事务执行之前是否被修改
# 模拟客户转账
127.0.0.1:6379> watch money # 监控money
OK
127.0.0.1:6379> multi # 如果没有被修改,那么这个事务是可以正常执行成功的
OK
127.0.0.1:6379> decrby money 200 # 转账200
QUEUED
127.0.0.1:6379> incrby out 20 # + 20
QUEUED
127.0.0.1:6379> exec
-200
20
127.0.0.1:6379>
2、如果被监控的key在事务之外被修改了
127.0.0.1:6379> WATCH money
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> DECRBY money 30
QUEUED
127.0.0.1:6379> INCRBY out 30
QUEUED
# === 表示另一个客户端 ===
127.0.0.1:6379> get money
"80"
127.0.0.1:6379> INCRBY money 200 # 修改被监控的数据
(integer) 280
127.0.0.1:6379> get money
"280"
# 再次执行事务,会直接返回nil,代表执行失败
127.0.0.1:6379> EXEC
(nil)
127.0.0.1:6379> get money # 再次查看,当前监控的key已经被修改
"280"
实际上关于WATCH,还有一个命令,UNWATCH,意思是解除所有监控,但是官网的原话是,一旦你执行了DISCARD或者EXEC,就没必要在执行UNWATCH
Jedis是Redis官方推荐的Java连接Redis的连接开发工具!使用Java操作Redis的中间件
创建Moven项目,导入依赖
<dependency>
<groupId>redis.clientsgroupId>
<artifactId>jedisartifactId>
<version>3.2.0version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>1.2.70version>
dependency>
src/test/TestPing
import redis.clients.jedis.Jedis;
public class TestPing {
public static void main(String[] args) {
// new一个Jedis对象
Jedis jedis = new Jedis("127.0.0.1", 6379);
// Jedis中的API就是之前学习的命令
System.out.println(jedis.ping());
//关闭连接
jedis.close();
}
}
返回PONG
不过这边有一个小问题,如果你的Redis是远程连接的话,会出现连接超时或者是拒绝访问的问题,在这边需要做两件事情,当然,防火墙的关闭也是必不可少的
打开redis.conf配置文件
随后进行API测试
import redis.clients.jedis.Jedis;
public class JedisType {
public static void main(String[] args) {
Jedis jedis = new Jedis("127.0.0.1", 6379);
System.out.println("=================== String =========================");
System.out.println(jedis.set("name", "zs"));
System.out.println(jedis.get("name"));
System.out.println(jedis.append("name", "+value"));
System.out.println(jedis.get("name"));
System.out.println(jedis.strlen("name"));
System.out.println("=================== List =========================");
System.out.println(jedis.lpush("listKey", "l1", "l2", "l3"));
System.out.println(jedis.lrange("listKey", 0, -1)); // [l3, l2, l1]
System.out.println(jedis.llen("listKey"));
System.out.println("=================== Hash =========================");
System.out.println(jedis.hset("hashKey", "k1", "v1"));
System.out.println(jedis.hset("hashKey", "k2", "v2"));
System.out.println(jedis.hset("hashKey", "k3", "v3"));
System.out.println(jedis.hmget("hashKey", "k1", "k2", "k3")); // [v1, v2, v3]
System.out.println(jedis.hgetAll("hashKey")); // {k3=v3, k2=v2, k1=v1}
System.out.println("=================== Set =========================");
System.out.println(jedis.sadd("setKey", "s1", "s2", "s3", "s4"));
System.out.println(jedis.smembers("setKey")); // [s2, s1, s4, s3]
System.out.println(jedis.scard("setKey"));
System.out.println("=================== Zset =========================");
System.out.println(jedis.zadd("ZKey", 90, "z1"));
System.out.println(jedis.zadd("ZKey", 80, "z2"));
System.out.println(jedis.zadd("ZKey", 85, "z3"));
System.out.println(jedis.zrange("ZKey", 0, -1)); // [z2, z3, z1]
}
}
=================== String =========================
OK
zs
8
zs+value
8
=================== List =========================
3
[l3, l2, l1]
3
=================== Hash =========================
1
1
1
[v1, v2, v3]
{k3=v3, k1=v1, k2=v2}
=================== Set =========================
4
[s4, s1, s3, s2]
4
=================== Zset =========================
1
1
1
[z2, z3, z1]
进程已结束,退出代码为 0
import com.alibaba.fastjson.JSONObject;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;
public class TestTX {
public static void main(String[] args) {
Jedis jedis = new Jedis("127.0.0.1", 6379);
jedis.flushDB();
JSONObject jsonObject = new JSONObject();
jsonObject.put("name", "张三");
jsonObject.put("age", "21");
jsonObject.put("sex", "boy");
Transaction multi = jedis.multi(); // 开启事务
String user = jsonObject.toJSONString();
try {
multi.set("user1", user);
multi.set("user2", user);
multi.exec();
} catch (Exception e) {
multi.discard(); // 出现问题,放弃事务
e.printStackTrace();
} finally {
System.out.println(jedis.mget("user1", "user2"));
jedis.close(); // 关闭连接
}
}
}
[{"sex":"boy","name":"张三","age":"21"}, {"sex":"boy","name":"张三","age":"21"}]
1、通用API
import java.util.Set;
import redis.clients.jedis.Jedis;
public class TestKey {
public TestKey() {
}
public static void main(String[] args) {
Jedis jedis = new Jedis("127.0.0.1", 6379);
System.out.println("清空数据:" + jedis.flushDB());
System.out.println("判断某个键是否存在:" + jedis.exists("username"));
System.out.println("新增<'username','kuangshen'>的键值对:" + jedis.set("username", "kuangshen"));
System.out.println("新增<'password','password'>的键值对:" + jedis.set("password", "password"));
System.out.print("系统中所有的键如下:");
Set<String> keys = jedis.keys("*");
System.out.println(keys);
System.out.println("删除键password:" + jedis.del("password"));
System.out.println("判断键password是否存在:" + jedis.exists("password"));
System.out.println("查看键username所存储的值的类型:" + jedis.type("username"));
System.out.println("随机返回key空间的一个:" + jedis.randomKey());
System.out.println("重命名key:" + jedis.rename("username", "name"));
System.out.println("取出改后的name:" + jedis.get("name"));
System.out.println("按索引查询:" + jedis.select(0));
System.out.println("删除当前选择数据库中的所有key:" + jedis.flushDB());
System.out.println("返回当前数据库中key的数目:" + jedis.dbSize());
System.out.println("删除所有数据库中的所有key:" + jedis.flushAll());
jedis.connect();
jedis.disconnect();
jedis.flushAll();
}
}
2、String(字符串)
import java.util.concurrent.TimeUnit;
import redis.clients.jedis.Jedis;
public class TestString {
public TestString() {
}
public static void main(String[] args) {
Jedis jedis = new Jedis("127.0.0.1", 6379);
jedis.flushDB();
System.out.println("===========增加数据===========");
System.out.println(jedis.set("key1", "value1"));
System.out.println(jedis.set("key2", "value2"));
System.out.println(jedis.set("key3", "value3"));
System.out.println("删除键key2:" + jedis.del("key2"));
System.out.println("获取键key2:" + jedis.get("key2"));
System.out.println("修改key1:" + jedis.set("key1", "value1Changed"));
System.out.println("获取key1的值:" + jedis.get("key1"));
System.out.println("在key3后面加入值:" + jedis.append("key3", "End"));
System.out.println("key3的值:" + jedis.get("key3"));
System.out.println("增加多个键值对:" + jedis.mset(new String[]{"key01", "value01", "key02", "value02", "key03", "value03"}));
System.out.println("获取多个键值对:" + jedis.mget(new String[]{"key01", "key02", "key03"}));
System.out.println("获取多个键值对:" + jedis.mget(new String[]{"key01", "key02", "key03", "key04"}));
System.out.println("删除多个键值对:" + jedis.del(new String[]{"key01", "key02"}));
System.out.println("获取多个键值对:" + jedis.mget(new String[]{"key01", "key02", "key03"}));
jedis.flushDB();
System.out.println("===========新增键值对防止覆盖原先值==============");
System.out.println(jedis.setnx("key1", "value1"));
System.out.println(jedis.setnx("key2", "value2"));
System.out.println(jedis.setnx("key2", "value2-new"));
System.out.println(jedis.get("key1"));
System.out.println(jedis.get("key2"));
System.out.println("===========新增键值对并设置有效时间=============");
System.out.println(jedis.setex("key3", 2, "value3"));
System.out.println(jedis.get("key3"));
try {
TimeUnit.SECONDS.sleep(3L);
} catch (InterruptedException var3) {
var3.printStackTrace();
}
System.out.println(jedis.get("key3"));
System.out.println("===========获取原值,更新为新值==========");
System.out.println(jedis.getSet("key2", "key2GetSet"));
System.out.println(jedis.get("key2"));
System.out.println("获得key2的值的字串:" + jedis.getrange("key2", 2L, 4L));
}
}
3、List(列表)
import redis.clients.jedis.Jedis;
public class TestList {
public TestList() {
}
public static void main(String[] args) {
Jedis jedis = new Jedis("127.0.0.1", 6379);
jedis.flushDB();
System.out.println("===========添加一个list===========");
jedis.lpush("collections", new String[]{"ArrayList", "Vector", "Stack", "HashMap", "WeakHashMap", "LinkedHashMap"});
jedis.lpush("collections", new String[]{"HashSet"});
jedis.lpush("collections", new String[]{"TreeSet"});
jedis.lpush("collections", new String[]{"TreeMap"});
System.out.println("collections的内容:" + jedis.lrange("collections", 0L, -1L));
System.out.println("collections区间0-3的元素:" + jedis.lrange("collections", 0L, 3L));
System.out.println("===============================");
System.out.println("删除指定元素个数:" + jedis.lrem("collections", 2L, "HashMap"));
System.out.println("collections的内容:" + jedis.lrange("collections", 0L, -1L));
System.out.println("删除下表0-3区间之外的元素:" + jedis.ltrim("collections", 0L, 3L));
System.out.println("collections的内容:" + jedis.lrange("collections", 0L, -1L));
System.out.println("collections列表出栈(左端):" + jedis.lpop("collections"));
System.out.println("collections的内容:" + jedis.lrange("collections", 0L, -1L));
System.out.println("collections添加元素,从列表右端,与lpush相对应:" + jedis.rpush("collections", new String[]{"EnumMap"}));
System.out.println("collections的内容:" + jedis.lrange("collections", 0L, -1L));
System.out.println("collections列表出栈(右端):" + jedis.rpop("collections"));
System.out.println("collections的内容:" + jedis.lrange("collections", 0L, -1L));
System.out.println("修改collections指定下标1的内容:" + jedis.lset("collections", 1L, "LinkedArrayList"));
System.out.println("collections的内容:" + jedis.lrange("collections", 0L, -1L));
System.out.println("===============================");
System.out.println("collections的长度:" + jedis.llen("collections"));
System.out.println("获取collections下标为2的元素:" + jedis.lindex("collections", 2L));
System.out.println("===============================");
jedis.lpush("sortedList", new String[]{"3", "6", "2", "0", "7", "4"});
System.out.println("sortedList排序前:" + jedis.lrange("sortedList", 0L, -1L));
System.out.println(jedis.sort("sortedList"));
System.out.println("sortedList排序后:" + jedis.lrange("sortedList", 0L, -1L));
}
}
4、set(集合)
import redis.clients.jedis.Jedis;
public class TestSet {
public TestSet() {
}
public static void main(String[] args) {
Jedis jedis = new Jedis("127.0.0.1", 6379);
jedis.flushDB();
System.out.println("============向集合中添加元素(不重复)============");
System.out.println(jedis.sadd("eleSet", new String[]{"e1", "e2", "e4", "e3", "e0", "e8", "e7", "e5"}));
System.out.println(jedis.sadd("eleSet", new String[]{"e6"}));
System.out.println(jedis.sadd("eleSet", new String[]{"e6"}));
System.out.println("eleSet的所有元素为:" + jedis.smembers("eleSet"));
System.out.println("删除一个元素e0:" + jedis.srem("eleSet", new String[]{"e0"}));
System.out.println("eleSet的所有元素为:" + jedis.smembers("eleSet"));
System.out.println("删除两个元素e7和e6:" + jedis.srem("eleSet", new String[]{"e7", "e6"}));
System.out.println("eleSet的所有元素为:" + jedis.smembers("eleSet"));
System.out.println("随机的移除集合中的一个元素:" + jedis.spop("eleSet"));
System.out.println("随机的移除集合中的一个元素:" + jedis.spop("eleSet"));
System.out.println("eleSet的所有元素为:" + jedis.smembers("eleSet"));
System.out.println("eleSet中包含元素的个数:" + jedis.scard("eleSet"));
System.out.println("e3是否在eleSet中:" + jedis.sismember("eleSet", "e3"));
System.out.println("e1是否在eleSet中:" + jedis.sismember("eleSet", "e1"));
System.out.println("e1是否在eleSet中:" + jedis.sismember("eleSet", "e5"));
System.out.println("=================================");
System.out.println(jedis.sadd("eleSet1", new String[]{"e1", "e2", "e4", "e3", "e0", "e8", "e7", "e5"}));
System.out.println(jedis.sadd("eleSet2", new String[]{"e1", "e2", "e4", "e3", "e0", "e8"}));
System.out.println("将eleSet1中删除e1并存入eleSet3中:" + jedis.smove("eleSet1", "eleSet3", "e1"));
System.out.println("将eleSet1中删除e2并存入eleSet3中:" + jedis.smove("eleSet1", "eleSet3", "e2"));
System.out.println("eleSet1中的元素:" + jedis.smembers("eleSet1"));
System.out.println("eleSet3中的元素:" + jedis.smembers("eleSet3"));
System.out.println("============集合运算=================");
System.out.println("eleSet1中的元素:" + jedis.smembers("eleSet1"));
System.out.println("eleSet2中的元素:" + jedis.smembers("eleSet2"));
System.out.println("eleSet1和eleSet2的交集:" + jedis.sinter(new String[]{"eleSet1", "eleSet2"}));
System.out.println("eleSet1和eleSet2的并集:" + jedis.sunion(new String[]{"eleSet1", "eleSet2"}));
System.out.println("eleSet1和eleSet2的差集:" + jedis.sdiff(new String[]{"eleSet1", "eleSet2"}));
jedis.sinterstore("eleSet4", new String[]{"eleSet1", "eleSet2"});
System.out.println("eleSet4中的元素:" + jedis.smembers("eleSet4"));
}
}
5、Hash(哈希)
import java.util.HashMap;
import java.util.Map;
import redis.clients.jedis.Jedis;
public class TestHash {
public TestHash() {
}
public static void main(String[] args) {
Jedis jedis = new Jedis("127.0.0.1", 6379);
jedis.flushDB();
Map<String, String> map = new HashMap();
map.put("key1", "value1");
map.put("key2", "value2");
map.put("key3", "value3");
map.put("key4", "value4");
jedis.hmset("hash", map);
jedis.hset("hash", "key5", "value5");
System.out.println("散列hash的所有键值对为:" + jedis.hgetAll("hash"));
System.out.println("散列hash的所有键为:" + jedis.hkeys("hash"));
System.out.println("散列hash的所有值为:" + jedis.hvals("hash"));
System.out.println("将key6保存的值加上一个整数,如果key6不存在则添加key6:" + jedis.hincrBy("hash", "key6", 6L));
System.out.println("散列hash的所有键值对为:" + jedis.hgetAll("hash"));
System.out.println("将key6保存的值加上一个整数,如果key6不存在则添加key6:" + jedis.hincrBy("hash", "key6", 3L));
System.out.println("散列hash的所有键值对为:" + jedis.hgetAll("hash"));
System.out.println("删除一个或者多个键值对:" + jedis.hdel("hash", new String[]{"key2"}));
System.out.println("散列hash的所有键值对为:" + jedis.hgetAll("hash"));
System.out.println("散列hash中键值对的个数:" + jedis.hlen("hash"));
System.out.println("判断hash中是否存在key2:" + jedis.hexists("hash", "key2"));
System.out.println("判断hash中是否存在key3:" + jedis.hexists("hash", "key3"));
System.out.println("获取hash中的值:" + jedis.hmget("hash", new String[]{"key3"}));
System.out.println("获取hash中的值:" + jedis.hmget("hash", new String[]{"key3", "key4"}));
}
}
备注:从SpringBoot2.x之后,原先使用的Jedis被lettuce替代
Jedis:采用直连,模拟多个线程操作会出现安全问题。为避免此问题,需要使用Jedis Pool连接池!类似于BIO模式
lettuce:采用netty网络框架,对象可以在多个线程中被共享,完美避免线程安全问题,减少线程数据,类似于NIO模式
1、创建SpringBoot项目
2、分析源码
首先先查看RedisAutoConfiguration中的源码
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
// @Import注解导入了两个配置类,有Lettuce和Jedis,
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {
@Bean
@ConditionalOnMissingBean(name = "redisTemplate") //如果redisTemplate存在,则方法不生效
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
throws UnknownHostException {
// 默认的RedisTemplate直接使用此类内部默认设置操作数据,但是Redis对象需要序列化
// 泛型都是Object,后面使用的话,大都是RedisTemplate
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
@Bean
@ConditionalOnMissingBean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory)
throws UnknownHostException {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
}
上面的@Import注解导入了两个配置类,有Lettuce和Jedis,可以点开这两个类查看
对比一下可以发现,Jedis配置类中有两个类是默认不存在的,不存在就无法使用
3、配置
# 配置Redis
spring.redis.host=127.0.0.1
spring.redis.port=6379
4、常用方式
三种常用方式
//操作String
redisTemplate.opsForValue().set("myKey", "myValue");
//操作Hash
redisTemplate.opsForHash().hasKey("name","张三");
// ...
redisTemplate.exec();
//redisTemplate.keys()
RedisConnection conn = redisTemplate.getConnectionFactory().getConnection();
//通过 conn 清库
conn.flushAll();
5、测试
package com.example.springboot;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisTemplate;
@SpringBootTest
class ApplicationTests {
@Autowired
private RedisTemplate<String,String> redisTemplate;
@Test
void contextLoads() {
redisTemplate.opsForValue().set("myKey","myValue");
System.out.println(redisTemplate.opsForValue().get("myKey"));
}
}
myValue
开发中一般用json传数据
1、新建 实体类
@Data
@Component
@NoArgsConstructor
@AllArgsConstructor
public class User {
private String name;
private int age;
}
2、测试
@SpringBootTest
class ApplicationTests {
@Autowired
private RedisTemplate<String,String> redisTemplate;
@Test
void test(){
try {
User user = new User("张三",25);
//object -> json 开发中一般用json传数据
String json = new ObjectMapper().writeValueAsString(user);
redisTemplate.opsForValue().set("user",json);
System.out.println(redisTemplate.opsForValue().get("user"));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
}
{"name":"张三","age":25}
3、这样也行
User user = new User("张三", 25);
redisTemplate.opsForValue().set("user", String.valueOf(user));
System.out.println(redisTemplate.opsForValue().get("user"));
User(name=张三, age=25)
4、 推荐实体类序列化
public class User implements Serializable {
private String name;
private int age;
}
@SpringBootTest
class ApplicationTests {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Test
void contextLoads() {
redisTemplate.opsForValue().set("myKey", "张三");
System.out.println(redisTemplate.opsForValue().get("myKey"));
}
}
127.0.0.1:6379> get myKey
寮犱笁
客户端中文会取得乱码,这与RedisTemplate默认序列化有关
先展示RedisTemplate的部分源码
// 这些是RedisTemplate的序列化配置
private @Nullable RedisSerializer keySerializer = null;
private @Nullable RedisSerializer valueSerializer = null;
private @Nullable RedisSerializer hashKeySerializer = null;
private @Nullable RedisSerializer hashValueSerializer = null;
@Override
public void afterPropertiesSet() {
super.afterPropertiesSet();
boolean defaultUsed = false;
if (defaultSerializer == null) {
// 这边默认使用JDK的序列化方式,我们可以自定义一个配置类,采用其他的序列化方式
defaultSerializer = new JdkSerializationRedisSerializer(
classLoader != null ? classLoader : this.getClass().getClassLoader());
}
}
}
自定义RedisConfig
package com.example.springboot.config;
//...
@Configuration
public class RedisConfig {
@Bean
@SuppressWarnings("all")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
// RedisTemplate对象,使用更方便
RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
template.setConnectionFactory(factory);
//Json的序列化配置
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// String的序列化配置
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash的value序列化方式采用jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
RedisUtil工具类
在SpringBoot中,如要操作Redis,就需要一直调用RedisTemplate.opsxxx的方法,一般在工作中不会去这样使用,公司里都会内部将这些操作数据类型的API进行一个封装,就像在学JDBC还有Mybatis等框架的时候,都会有一个XxxUtil的Java工具类,使用起来比较简单
这边推荐一个GitHub:https://github.com/iyayu/RedisUtil.git
package com.example.springboot.utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@Component
public final class RedisUtil {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// =============================common============================
/**
* 指定缓存失效时间
* @param key 键
* @param time 时间(秒)
*/
public boolean expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据key 获取过期时间
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
public long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 判断key是否存在
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除缓存
* @param key 可以传一个值 或多个
*/
@SuppressWarnings("unchecked")
public void del(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete(CollectionUtils.arrayToList(key));
}
}
}
// ============================String=============================
/**
* 普通缓存获取
* @param key 键
* @return 值
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存放入
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 普通缓存放入并设置时间
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public boolean set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 递增
* @param key 键
* @param delta 要增加几(大于0)
*/
public long incr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递增因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 递减
* @param key 键
* @param delta 要减少几(小于0)
*/
public long decr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递减因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, -delta);
}
// ================================Map=================================
/**
* HashGet
* @param key 键 不能为null
* @param item 项 不能为null
*/
public Object hget(String key, String item) {
return redisTemplate.opsForHash().get(key, item);
}
/**
* 获取hashKey对应的所有键值
* @param key 键
* @return 对应的多个键值
*/
public Map<Object, Object> hmget(String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* HashSet
* @param key 键
* @param map 对应多个键值
*/
public boolean hmset(String key, Map<String, Object> map) {
try {
redisTemplate.opsForHash().putAll(key, map);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* HashSet 并设置时间
* @param key 键
* @param map 对应多个键值
* @param time 时间(秒)
* @return true成功 false失败
*/
public boolean hmset(String key, Map<String, Object> map, long time) {
try {
redisTemplate.opsForHash().putAll(key, map);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value) {
try {
redisTemplate.opsForHash().put(key, item, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value, long time) {
try {
redisTemplate.opsForHash().put(key, item, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除hash表中的值
*
* @param key 键 不能为null
* @param item 项 可以使多个 不能为null
*/
public void hdel(String key, Object... item) {
redisTemplate.opsForHash().delete(key, item);
}
/**
* 判断hash表中是否有该项的值
*
* @param key 键 不能为null
* @param item 项 不能为null
* @return true 存在 false不存在
*/
public boolean hHasKey(String key, String item) {
return redisTemplate.opsForHash().hasKey(key, item);
}
/**
* hash递增 如果不存在,就会创建一个 并把新增后的值返回
*
* @param key 键
* @param item 项
* @param by 要增加几(大于0)
*/
public double hincr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, by);
}
/**
* hash递减
*
* @param key 键
* @param item 项
* @param by 要减少记(小于0)
*/
public double hdecr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, -by);
}
// ============================set=============================
/**
* 根据key获取Set中的所有值
* @param key 键
*/
public Set<Object> sGet(String key) {
try {
return redisTemplate.opsForSet().members(key);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 根据value从一个set中查询,是否存在
*
* @param key 键
* @param value 值
* @return true 存在 false不存在
*/
public boolean sHasKey(String key, Object value) {
try {
return redisTemplate.opsForSet().isMember(key, value);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将数据放入set缓存
*
* @param key 键
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSet(String key, Object... values) {
try {
return redisTemplate.opsForSet().add(key, values);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 将set数据放入缓存
*
* @param key 键
* @param time 时间(秒)
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSetAndTime(String key, long time, Object... values) {
try {
Long count = redisTemplate.opsForSet().add(key, values);
if (time > 0)
expire(key, time);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 获取set缓存的长度
*
* @param key 键
*/
public long sGetSetSize(String key) {
try {
return redisTemplate.opsForSet().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 移除值为value的
*
* @param key 键
* @param values 值 可以是多个
* @return 移除的个数
*/
public long setRemove(String key, Object... values) {
try {
Long count = redisTemplate.opsForSet().remove(key, values);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
// ===============================list=================================
/**
* 获取list缓存的内容
*
* @param key 键
* @param start 开始
* @param end 结束 0 到 -1代表所有值
*/
public List<Object> lGet(String key, long start, long end) {
try {
return redisTemplate.opsForList().range(key, start, end);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 获取list缓存的长度
*
* @param key 键
*/
public long lGetListSize(String key) {
try {
return redisTemplate.opsForList().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 通过索引 获取list中的值
*
* @param key 键
* @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
*/
public Object lGetIndex(String key, long index) {
try {
return redisTemplate.opsForList().index(key, index);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
*/
public boolean lSet(String key, Object value) {
try {
redisTemplate.opsForList().rightPush(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
* @param key 键
* @param value 值
* @param time 时间(秒)
*/
public boolean lSet(String key, Object value, long time) {
try {
redisTemplate.opsForList().rightPush(key, value);
if (time > 0)
expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, List<Object> value) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, List<Object> value, long time) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
if (time > 0)
expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据索引修改list中的某条数据
*
* @param key 键
* @param index 索引
* @param value 值
* @return
*/
public boolean lUpdateIndex(String key, long index, Object value) {
try {
redisTemplate.opsForList().set(key, index, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 移除N个值为value
*
* @param key 键
* @param count 移除多少个
* @param value 值
* @return 移除的个数
*/
public long lRemove(String key, long count, Object value) {
try {
Long remove = redisTemplate.opsForList().remove(key, count, value);
return remove;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
}
@Autowired
private RedisUtil redisUtil;
@Test
void test() {
redisUtil.set("name","张三");
System.out.println(redisUtil.get("name"));
}
张三
Redis在启动的时候是通过配置文件进行启动的
对 Redis.conf 配置文件 进行分析
1、Units
单位,Redis配置文件中的单位对大小写不敏感
# Redis configuration file example
# Note on units: when memory size is needed, it is possible to specify
# it in the usual form of 1k 5GB 4M and so forth:
#
# 1k => 1000 bytes
# 1kb => 1024 bytes
# 1m => 1000000 bytes
# 1mb => 1024*1024 bytes
# 1g => 1000000000 bytes
# 1gb => 1024*1024*1024 bytes
#
# [翻译]单位不区分大小写,所以1GB 1Gb 1gB都是一样的。
2、includes
包含,可以在Redis启动的时候再加载一些除了Redis.conf之外的其他的配置文件,和Spring的import,jsp的include类似
################################## INCLUDES ###################################
# ...
# [作用]
# include .\path\to\local.conf
# include c:\path\to\other.conf
3、NETWORK
网络,表示Redis启动时开放的端口默认与本机绑定
################################## NETWORK #####################################
# By default, ...
#
# Examples:
#
# bind 192.168.1.100 10.0.0.1
# bind 127.0.0.1 ::1
#
# ~~~ WARNING ~~~ ...
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# [作用]网络,表示Redis启动时开放的端口默认与本机绑定
bind 127.0.0.1
# Protected mode is a layer of security protection, in order to avoid that
# ...
# [作用]是否开启保护模式,Redis默认开启,如果没有设置bind的IP地址和Redis密码,那么服务就会默认只能在本机运行
protected-mode yes
# [翻译]接受指定端口上的连接,默认为6379 (IANA #815344)。
# [翻译]如果端口0被指定,Redis将不会监听TCP套接字。
# [作用]Redis指定监听端口,默认为6379
port 6379
# ...
# [作用]表示服务器闲置多长时间(秒)后被关闭,如果这个这个数值为0,表示这个功能不起作用
timeout 0
4、GENERAL
################################# GENERAL #####################################
# By default ...
#
# 创建一个pid文件是最好的努力:如果Redis不能创建它
# 没有坏的事情发生,服务器将正常启动和运行。
# 不支持 /var/run/redis.pid
# [作用]指定服务器详细级别。
# 这可以是:
# debug(很多信息,对开发/测试很有用)
# verbose(有很多很少有用的信息,但不像调试级别那样混乱)
# notice(略微冗长,可能是您在生产中想要的内容)
# warning(只记录非常重要/关键的消息)
loglevel notice
# Specify the log file name. Also 'stdout' can be used to force
# Redis to log on the standard output.
# [作用]打印的日志文件名称,如果为空,表示标准输出,在配置守护进程的模式下会将输出信息保存到/dev/null
logfile ""
# ...
# 数据库支持数量,16个
databases 16
# [作用]是否以守护进程的方式运行,即后台运行,一般默认为no,需要手动改为yes
always-show-logo yes
配置日志等级,日志等级的可选项如下(翻译自配置文件,有改动):
5、SNAPSHOTTING
中文翻译为快照,如果在规定的时间内,数据发生了几次更新,那么就会将数据同步备份到一个文件中
Redis的持久化有两种方式,一种是RDB,一种是AOF。SNAPSHOTTING主要针对的是Redis持久化中的RDB
Redis是一个内存数据库,如果不采用持久化对数据进行保存,那么就会出现断电即失的尴尬场面
################################ SNAPSHOTTING ################################
# ...
# 在900秒内,至少有一个key被修改(添加),就会进行持久化操作
save 900 1
# 在300秒内,至少有10个key被修改,就会进行持久化操作
save 300 10
# 在60秒内,至少有1万个key被修改,就会进行持久化操作
save 60 10000
# 如果Redis在进行持久化的时候出现错误,是否停止写入,默认为是
top-writes-on-bgsave-error yes
#是否在进行数据备份时压缩持久化文件,默认为是,这个操作会耗费CPU资源,可以设置为no
rdbcompression yes
# 在保存持久化文件的同时,对文件内容进行数据校验
rdbchecksum yes
# 持久化文件保存的目录,默认保存在当前目录下
dir ./
6、REPLICATION
复制主机上的数据,当前配置所指定的IP和端口号即为主机
################################# REPLICATION #################################
# Redis在配置文件中将此配置注释,默认不使用,下同
# replicaof
# 如果配置的主机有密码,需要配置此密码以通过master的验证
# masterauth
7、SECRULITY
安全,可以在配置文件中设置Redis的登录密码
8、CLIENT
Redis允许存在的客户端的最大数量,默认有一万个
################################### CLIENTS ####################################
# Redis允许存在的客户端的最大数量,默认有一万个
# maxclients 10000
############################## MEMORY MANAGEMENT ################################
# Redis配置最大的内存容量
# maxmemory
# 内存达到上限之后默认的处理策略
# maxmemory-policy noeviction
处理策略有以下几种
9、APPEND ONLY MODE
这是Redis持久化的另一种方式,AOF,AOF模式默认不开启,Redis默认开启的是持久化模式是RDB,在大部分情况下,RDB的模式完全够用
appendonly no
AOF持久化的文件名称
appendfilename "appendonly.aof"
每秒执行一次同步,但是可能会丢失这一秒的数据
# 对于 appendfsync 它有以下几个属性
# appendfsync always 表示每次修改都会进行数据同步,速度较慢,消耗性能
# appendfsync no 不执行同步,不消耗性能
appendfsync everysec # 数据不同步,每秒记录一次
为什么Redis要实现持久化
Redis的数据存在内存中,断电即失,所以要实现持久化,保存数据
RDB,全称Redis DataBase。什么是RDB,在指定的时间间隔内将数据集快照写入到磁盘中,在恢复数据的时候将这些快照文件读取到内存中,对应的配置文件中的SNAPSHOTTING,可以查看在上面提到的Redis的配置文件
Redis会单独创建出一个子进程(fork)来进行持久化,会将数据写入到一个临时文件中,待持久化操作结束之后,临时文件会将已经持久化完成的文件替换掉,在这个过程中,主进程不进行任何IO操作,这也就确保RDB极高的性能,相比于RDB和AOF,RDB的模式会比AOF更加的高效。如果在进行大数据恢复,并且对于数据的精度要求不高,那么就可以使用RDB,Redis的持久化默认的也是RDB,在一般情况下(生产环境)不需要修改这个配置
什么是fork?fork就是复制一个和当前一模一样的进程作为原进程的子进程
RDB保存的文件就是dump.rdb文件
# 配置文件
# The filename where to dump the DB
dbfilename dump.rdb
RDB
1、满足配置文件的save规则的情况下,会自动触发RDB规则
2、执行FLUSHALL命令,也会触发RDB规则,但是没有意义,因为文件内容为空
3、退出Redis,也会产生dump.rdb文件(退出Redis默认执行save命令)
4、在客户端中使用save或者bgsave命令,也可以触发RDB规则但是这两种规则有所不同
如何进行数据恢复
1、首先使用一个Redis命令查看持久化文件保存的位置
# config get dir
# linux
127.0.0.1:6379> CONFIG GET dir
1) "dir"
2) "/usr/local/bin"
2、然后将dump.rdb文件放到Redis的启动目录下即可
优点:
缺点
Redis的另一种持久化方式,AOF,全名为Append Only File,它用日志的形式来记录每一个写操作,将Redis执行过的命令进行记录(读操作不记录),只追加文件,不改写文件。Redis在启动时会自动加载AOF持久化的文件重新构建数据,也就是Redis重启会把AOF持久化文件中的信息从前到后执行一次以完成数据的恢复
AOF持久化对应的配置文件的位置是APPEND ONLY MODE
############################## APPEND ONLY MODE ###############################
# ...
# 启动AOF,需要将no 改成yes
appendonly yes
# The name of the append only file (default: "appendonly.aof")
appendfilename "appendonly.aof"
修改完配置之后,只需重新启动就可
这里有一个小细节需要注意:如果AOF和RDB模式在配置文件中都有开启的话,为了保证数据的安全性,在Redis启动时会优先使用AOF
Redis的AOF持久化保存的文件名称就叫做appendonly.aof
如果说appendonly.aof文件的内容发生了一些错误,那么在Redis进行启动时,会出现问题
[root@bogon bin]# redis-server myconfig/redis.conf
1574:C 16 Nov 2020 03:47:18.350 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
1574:C 16 Nov 2020 03:47:18.350 # Redis version=5.0.10, bits=64, commit=00000000, modified=0, pid=1574, just started
1574:C 16 Nov 2020 03:47:18.350 # Configuration loaded
[root@bogon bin]# redis-cli -p 6379
# 下面的这一个返回结果,表示连接被拒绝
Could not connect to Redis at 127.0.0.1:6379: Connection refused
not connected> exit
如果appendonly.aof内部发生错误,咋办?Redis中提供了一个可以修复aof文件的修复工具叫做redis-check-aof
怎么使用呢,下面有一条命令
redis-check-aof --fix appendonly.aof
[root@bogon bin]# redis-check-aof --fix appendonly.aof
0x 167: Expected \r\n, got: 6769
AOF analyzed: size=383, ok_up_to=351, diff=32
This will shrink the AOF from 383 bytes, with 32 bytes, to 351 bytes
Continue? [y/N]: y
Successfully truncated AOF
Redis优缺点
优点:
它支持 同步记录 和 异步记录 ,对应的配置文件的属性分别是下面两个
appendfsync always # 同步记录,客户端中一有写操作,即刻记录,数据的完整性好,但是性能较差
appendfsync everysec # 异步记录,每秒记录一次,但是服务器如果在这一秒之内宕机,这一秒的数据就会丢失
appendfsync no # 不记录
缺点:
从恢复数据的角度来说,AOF所恢复的数据量一定是比RDB来得大的,从恢复数据的时间的角度来说,AOF的时间也是大于RDB的
AOF持久化本质就是采用日志的形式对文件内容进行追加,为了防止追加之后这个文件变得越大,所以Redis推出了一种针对于AOF文件的重写机制,如果AOF文件的大小超过配置文件中所设定的阈值时,会自动触发重写机制对文件内容进行压缩,只对可以恢复数据的命令进行保留,针对于这种重写机制,也可以在客户端中对这种重写机制进行手动触发,只需要一个命令
127.0.0.1:6379> bgrewriteaof
原理
当AOF文件持久追加并且越来越大时,Redis会fork出一条新进程来对文件进行重写,和RDB一样,AOF也是先写临时文件再将其替换掉。Redis会对新进程中的数据进行遍历,每次都会遍历set和set有关的命令。重写并没有读取原来的appendonly.aof文件,而是使用命令将内存中的数据库内容进行重写得到一个新的文件
Redis会将上一次重写的AOF文件大小进行记录,如果当前文件的大小超过源文件的一倍并且大小大于64M时就会触发重写操作
可以在配置文件中查看重写的信息
auto-aof
# 自动重写只追加文件。
# Redis能够自动重写日志文件隐式调用
# BGREWRITEAOF当AOF日志大小以指定的百分比增长时。
# 这是它的工作原理:Redis会记住AOF文件的大小
# 在启动时使用AOF)。
#
#基本大小与当前大小相比较。如果当前大小为大于指定百分比时,重写被触发。也
#你需要指定AOF文件被重写的最小大小
#对于避免重写AOF文件非常有用,即使百分比增加了
#已经达到,但仍然很小。
#
#指定一个百分比为零,以禁用自动AOF
#重写特性。
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
总结
Redis的发布订阅(publish/subscribe)是一种消息通信模式,发送者(publish)发送消息,订阅者(subscribe)接收消息
Redis客户端可以订阅任意数量的频道
如图,有三个客户端订阅了一个Channel1
当Channel1的后台发送了一个数据到Channel1的频道中,这三个订阅了Channel1的客户端就会同时收到这个数据
命令:
这些都是用来实现数据通信的命令,现实中的场景可以是网络聊天室,广播等
订阅端
# [SUBSCRIBE] 订阅一个频道叫chanword
127.0.0.1:6379> subscribe chanword
subscribe
chanword # 频道名
1 # 1表示订阅成功(1表示命令执行成功,o表示命令执行失败)
# === 一旦开始订阅,会立即占用当前进程去监听自己所订阅的那个Channel ===
message # 标识
chanword # 频道名
Hello World # 接受的数据
message
chanword
my number is 1548 # 接受的数据
发送端
# [PUBLISH] 往频道中发布一条消息
127.0.0.1:6379> publish nochan "Hello World"
0 # 发送失败,没有nochan这个频道
127.0.0.1:6379> publish chanword "Hello World"
1 # 发送成功,有chanword这个频道
127.0.0.1:6379> publish chanword "my number is 1548"
1 # 发送成功
原理:
Redis是C语言编写,在实现消息的发布和订阅的时候,Redis将其封装在一个后缀名为点c的文件中,pubsub.c
通过subscribe和publish命令实现消息的发布和订阅,当用户订阅了一个频道之后,redis-server里面维护了一个字典,字典里有很多个Channel(频道),字典的值就是一个链表,链表中保存的是订阅了这个频道的用户,使用publish命令往频道发送数据之后,redis-server使用此频道作为key,去遍历这个指定的value链表,将信息依次发送过去
发布订阅的实现场景
1、实时沟通消息系统
2、微信公众号(点击关注,后台发送一篇博客,订阅的用户就可以监听到)
…
还有一些比较复杂的场景,可以使用消息中间件来做,RabbitMQ,RocketMQ,kafka…
主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(Master/Leader),后者称为从节点(Slave/Follower), 数据的复制是单向的!只能由主节点复制到从节点(主节点以写为主、从节点以读为主)。
默认情况下,每台Redis服务器都是主节点,一个主节点可以有0个或者多个从节点,但每个从节点只能由一个主节点。
作用
为什么使用集群
我们在讲解配置文件的时候,注意到有一个replication
模块 (见Redis.conf中第8条)
info replication
127.0.0.1:6379> info replication
# Replication
role:master # 角色 master:主节点
connected_slaves:0 # 从机数量:0
master_replid:425bbc84c7141a0738b5d0d6cd6920a756e334e4
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0
127.0.0.1:6379>
既然需要启动多个服务,就需要配置多个配置文件。每个配置文件对应修改以下信息:
端口号、pid文件名、日志文件名、rdb文件名
[root@192 bin]# cd myconfig/
[root@192 myconfig]# ls
redis.conf
[root@192 myconfig]# cp redis.conf redis_master79.conf
[root@192 myconfig]# cp redis.conf redis_sub81.conf
[root@192 myconfig]# cp redis.conf redis_sub80.conf
[root@192 myconfig]# vim redis_master79.conf
[root@192 myconfig]# vim redis_sub80.conf
[root@192 myconfig]# vim redis_sub81.conf
[root@192 myconfig]# ls
redis.conf redis_master79.conf redis_sub80.conf redis_sub81.conf
1、配置redis配置文件
# redis.conf 【备份不使用,不修改】
# 端口号
port 6379
# 日志文件名
logfile ""
# pid文件名
pidfile /var/run/redis_6379.pid
# rdb文件名
dbfilename dump.rdb
# redis_master79.conf 【做主节点】
port 6379
logfile "6379.log"
pidfile /var/run/redis_6379.pid
dbfilename dump6379.rdb
# redis_sub80.conf 【做从节点1】
port 6380
logfile "6380.log"
pidfile /var/run/redis_6380.pid
dbfilename dump6380.rdb
# redis_sub81.conf 【做从节点2】
port 6381
logfile "6381.log"
pidfile /var/run/redis_6381.pid
dbfilename dump6381.rdb
# 启动主节点
[root@192 bin]# redis-server /usr/local/bin/myconfig/redis_master79.conf
# 启动从节点1
[root@192 bin]# redis-server /usr/local/bin/myconfig/redis_sub80.conf
# 启动从节点2
[root@192 bin]# redis-server /usr/local/bin/myconfig/redis_sub81.conf
# 查看是否启动成功
[root@192 myconfig]# ps -ef | grep redis
root 2031 1 1 09:35 ? 00:00:00 redis-server 127.0.0.1:6379
root 2037 1 0 09:35 ? 00:00:00 redis-server 127.0.0.1:6380
root 2042 1 1 09:35 ? 00:00:00 redis-server 127.0.0.1:6381
root 2047 1977 0 09:35 pts/0 00:00:00 grep --color=auto redis
默认情况下,每台Redis服务器都是主节点;我们一般情况下只用配置从机就好了!
认老大(认主)!一主(79)二从(80,81)
使用SLAVEOF host port
就可以为从机配置主机了。
# 6380
# SLAVEOF host port
# 二个从机 都 认 127.0.0.1 6379 为老大
127.0.0.1:6380> SLAVEOF 127.0.0.1 6379
OK
# Replication
role:slave # 角色:奴隶(从属)
master_host:127.0.0.1 # 老大的ip
master_port:6379 # 老大的端口
# 。。。
# 6381
127.0.0.1:6381> SLAVEOF 127.0.0.1 6379
OK
127.0.0.1:6381> info replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6379
# 。。。
# 6378 [主机]
# 然后主机上也能看到从机的状态:
127.0.0.1:6379> info replication
# Replication
role:master # 角色:主机
connected_slaves:2 # 从机数量
slave0:ip=127.0.0.1,port=6381,state=online,offset=322,lag=1 # 从机2信息
slave1:ip=127.0.0.1,port=6380,state=online,offset=322,lag=0 # 从机1信息
# ...
我们这里是使用命令搭建,是暂时的(重启失效),真实开发中应该在从机的配置文件中进行配置,这样的话是永久的。
################################# REPLICATION #################################
#
# +------------------+ +---------------+
# | Master | ---> | Replica |
# | (receive writes) | | (exact copy) |
# +------------------+ +---------------+
#
# 配置主机信息
# replicaof
replicaof 127.0.0.1 6379
# 如果 Redis 有密码需要在配置文件设置主机密码 masterauth ******
# masterauth
1、从机自动保存主机的所以数据
# === 主机【6379】 ===
127.0.0.1:6379> set name 张三
OK
127.0.0.1:6379> set age 25
OK
127.0.0.1:6379> keys *
age
name
127.0.0.1:6379>
# === 主机【6380】 ===
127.0.0.1:6380> keys *
name
age
# === 主机【6381】 ===
127.0.0.1:6381> keys *
name
age
2、从机只能读,不能写,主机可读可写但是多用于写。
127.0.0.1:6380> set name 张三 # 从机【6380】写入失败
READONLY You can't write against a read only replica.
127.0.0.1:6381> set name 张三 # 从机【6381】写入失败
READONLY You can't write against a read only replica.
127.0.0.1:6379> set name 张三 # 主机【6379】写入成功
OK
127.0.0.1:6379> get name # 主机【6379】读取成功
张三
127.0.0.1:6381> get name # 从机【6380】读取成功
张三
127.0.0.1:6381> get name # 从机【6381】读取成功
张三
3、当主机断电宕机后,默认情况下从机的角色不会发生变化 ,集群中只是失去了写操作,当主机恢复以后,又会连接上从机恢复原状。
4、当从机断电宕机后,若不是使用配置文件配置的从机,再次启动后作为主机是无法获取之前主机的数据的,若此时重新配置称为从机,又可以获取到主机的所有数据。这里就要提到一个同步原理。
5、第二条中提到,默认情况下,主机故障后,不会出现新的主机,有两种方式可以产生新的主机:
slaveof no one
,这样执行以后从机会独立出来成为一个主机如果没有老大了,这个时候能不能选择出来一个老大呢?手动!
谋朝篡位
如果主机断开了连接,我们可以使用SLAVEOF no one
让自己变成主机!其他的节点就可以手动连接到最新的主节点(手动)!如果这个时候老大修复了,也不能让失去的从节点回归!
复制原理
Slave(从机) 启动成功连接到 master 后会发送一个sync同步命令
Master 接到命令,启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令,在后台进程执行 完毕之后,master将传送整个数据文件到slave,并完成一次完全同步。
全量复制:而slave服务在接收到数据库文件数据后,将其存盘并加载到内存中。
增量复制:Master 继续将新的所有收集到的修改命令依次传给slave,完成同步
但是只要是重新连接master,一次完全同步(全量复制)将被自动执行! 我们的数据一定可以在从机中看到!
层层链路
上一个M链接下一个 S! 这时候也可以完成我们的主从复制!
# 6380 依旧是slave(从机)
+------------------+ +---------------+ +------------------+
| 6379 | ---> | 6380 | ---> | 6381 |
| (master 主机) | | (master+slave)| | ( slave ) |
+------------------+ +---------------+ +------------------+
127.0.0.1:6381> SLAVEOF 127.0.0.1 6380
OK
127.0.0.1:6380> info replication
role:slave # 依旧是slave(从机)
master_host:127.0.0.1
master_port:6379
master_link_status:up
master_last_io_seconds_ago:2
master_sync_in_progress:0
slave_repl_offset:3150
slave_priority:100
slave_read_only:1 # 从机的数量
connected_slaves:1 # 连接从机的数量
slave0:ip=127.0.0.1,port=6381,state=online,offset=3150,lag=0 # 6381 从机的信息
哨兵:字面上是侦测、巡视,哨兵模式即侦测并自动把一台从服务器切换为主服务器
参考博客:Redis哨兵(Sentinel)模式 - 简书 (jianshu.com)
主从切换技术的方法是:当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工 干预,费事费力,还会造成一段时间内服务不可用。这不是一种推荐的方式,更多时候,我们优先考虑哨兵模式。Redis从2.8开始正式提供了Sentinel(哨兵) 架构来解决这个问题。 谋朝篡位的自动版,能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库。 哨兵模式是一种特殊的模式,首先Redis提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例。
哨兵模式是一种特殊的模式,首先Redis提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例。
单哨兵模式
哨兵的作用:
然而一个哨兵进程对Redis服务器进行监控,可能会出现问题(比如哨兵死了),为此,我们可以使用多个哨兵进行监控。 各个哨兵之间还会进行监控,这样就形成了多哨兵模式。
多哨兵模式
假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行failover过程,仅仅是哨兵1主观的认为主服务器不可用,这个现象成为主观下线。当后面的哨兵也检测到主服务器不可用,并且数量达到一 定值时,那么哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行failover[故障转移]操作。 切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为 客观下线。
1、配置哨兵配置文件 sentinel.conf
# sentinel monitor 被监控的名称 host port 1
sentinel monitor myredis 127.0.0.1 6379 1
后面的这个数字1,代表投票机制。即主机挂了,slave投票看让谁接替成为主机,票数最多的,就会成为主机!
2、启动哨兵!
[root@192 bin]# redis-sentinel myconfig/sentinel.conf
成功启动哨兵模式
(此时哨兵监视着我们的主机6379,当我们断开主机后):
[root@192 bin]# redis-sentinel myconfig/sentinel.conf
2177:X 27 Jan 2022 16:23:28.599 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
2177:X 27 Jan 2022 16:23:28.599 # Redis version=5.0.4, bits=64, commit=00000000, modified=0, pid=2177, just started
2177:X 27 Jan 2022 16:23:28.599 # Configuration loaded
2177:X 27 Jan 2022 16:23:28.600 * Increased maximum number of open files to 10032 (it was originally set to 1024).
_._
_.-``__ ''-._
_.-`` `. `_. ''-._ Redis 5.0.4 (00000000/0) 64 bit
.-`` .-```. ```\/ _.,_ ''-._
( ' , .-` | `, ) Running in sentinel mode
|`-._`-...-` __...-.``-._|'` _.-'| Port: 26379
| `-._ `._ / _.-' | PID: 2177
`-._ `-._ `-./ _.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' | http://redis.io
`-._ `-._`-.__.-'_.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' |
`-._ `-._`-.__.-'_.-' _.-'
`-._ `-.__.-' _.-'
`-._ _.-'
`-.__.-'
2177:X 27 Jan 2022 16:23:28.601 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128.
2177:X 27 Jan 2022 16:23:28.601 # Sentinel ID is 1633eb8d209a87db8ade04dbd021fcbad073782f
2177:X 27 Jan 2022 16:23:28.601 # +monitor master myredis 127.0.0.1 6379 quorum 1
2177:X 27 Jan 2022 16:28:46.365 # +sdown master myredis 127.0.0.1 6379
#【Tiper】 此时哨兵监视着我们的主机6379,当我们断开主机后,哨兵认为主机断开
2177:X 27 Jan 2022 16:28:46.365 # +odown master myredis 127.0.0.1 6379 #quorum 1/1
2177:X 27 Jan 2022 16:28:46.365 # +new-epoch 2
2177:X 27 Jan 2022 16:28:46.365 # +try-failover master myredis 127.0.0.1 6379
2177:X 27 Jan 2022 16:28:46.367 # +vote-for-leader 1633eb8d209a87db8ade04dbd021fcbad073782f 2
2177:X 27 Jan 2022 16:28:46.367 # +elected-leader master myredis 127.0.0.1 6379
2177:X 27 Jan 2022 16:28:46.367 # +failover-state-select-slave master myredis 127.0.0.1 6379
2177:X 27 Jan 2022 16:28:46.433 # +selected-slave slave 127.0.0.1:6380 127.0.0.1 6380 @ myredis 127.0.0.1 6379
2177:X 27 Jan 2022 16:28:46.434 * +failover-state-send-slaveof-noone slave 127.0.0.1:6380 127.0.0.1 6380 @ myredis 127.0.0.1 6379
2177:X 27 Jan 2022 16:28:46.508 * +failover-state-wait-promotion slave 127.0.0.1:6380 127.0.0.1 6380 @ myredis 127.0.0.1 6379
2177:X 27 Jan 2022 16:28:46.977 # +promoted-slave slave 127.0.0.1:6380 127.0.0.1 6380 @ myredis 127.0.0.1 6379
2177:X 27 Jan 2022 16:28:46.977 # +failover-state-reconf-slaves master myredis 127.0.0.1 6379
2177:X 27 Jan 2022 16:28:47.061 * +slave-reconf-sent slave 127.0.0.1:6381 127.0.0.1 6381 @ myredis 127.0.0.1 6379
2177:X 27 Jan 2022 16:28:47.978 * +slave-reconf-inprog slave 127.0.0.1:6381 127.0.0.1 6381 @ myredis 127.0.0.1 6379
2177:X 27 Jan 2022 16:28:47.978 * +slave-reconf-done slave 127.0.0.1:6381 127.0.0.1 6381 @ myredis 127.0.0.1 6379
2177:X 27 Jan 2022 16:28:48.042 # +failover-end master myredis 127.0.0.1 6379
#【Tiper】 选取随机新的主机
2177:X 27 Jan 2022 16:28:48.042 # +switch-master myredis 127.0.0.1 6379 127.0.0.1 6380
2177:X 27 Jan 2022 16:28:48.042 * +slave slave 127.0.0.1:6381 127.0.0.1 6381 @ myredis 127.0.0.1 6380
2177:X 27 Jan 2022 16:28:48.042 * +slave slave 127.0.0.1:6379 127.0.0.1 6379 @ myredis 127.0.0.1 6380
2177:X 27 Jan 2022 16:29:18.052 # +sdown slave 127.0.0.1:6379 127.0.0.1 6379 @ myredis 127.0.0.1 6380
2177:X 27 Jan 2022 16:35:51.699 # -sdown slave 127.0.0.1:6379 127.0.0.1 6379 @ myredis 127.0.0.1 6380
# 【Tiper】 如果主机此时回来了,只能归并到新的主机下,当做从机
2177:X 27 Jan 2022 16:36:01.641 * +convert-to-slave slave 127.0.0.1:6379 127.0.0.1 6379 @ myredis 127.0.0.1 6380
如果主机此时回来了,只能归并到新的主机下,当做从机,这就是哨兵模式的规则!
哨兵模式优缺点
优点:
缺点:
哨兵模式的全部配置
完整的哨兵模式配置文件 sentinel.conf
# 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
sentinel monitor mymaster 127.0.0.1 6379 1
# 当在Redis实例中开启了requirepass foobared 授权密码 这样所有连接Redis实例的客户端都要提供密码
# 设置哨兵sentinel 连接主从的密码 注意必须为主从设置一样的验证密码
# sentinel auth-pass
sentinel auth-pass mymaster MySUPER--secret-0123passw0rd
# 指定多少毫秒之后 主节点没有应答哨兵sentinel 此时 哨兵主观上认为主节点下线 默认30秒
# sentinel down-after-milliseconds
sentinel down-after-milliseconds mymaster 30000
# 这个配置项指定了在发生failover主备切换时最多可以有多少个slave同时对新的master进行 同步,
这个数字越小,完成failover所需的时间就越长,
但是如果这个数字越大,就意味着越 多的slave因为replication而不可用。
可以通过将这个值设为 1 来保证每次只有一个slave 处于不能处理命令请求的状态。
# sentinel parallel-syncs
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
sentinel failover-timeout mymaster 180000
# SCRIPTS EXECUTION
#配置当某一事件发生时所需要执行的脚本,可以通过脚本来通知管理员,例如当系统运行不正常时发邮件通知相关人员。
#对于脚本的运行结果有以下规则:
#若脚本执行后返回1,那么该脚本稍后将会被再次执行,重复次数目前默认为10
#若脚本执行后返回2,或者比2更高的一个返回值,脚本将不会重复执行。
#如果脚本在执行过程中由于收到系统中断信号被终止了,则同返回值为1时的行为相同。
#一个脚本的最大执行时间为60s,如果超过这个时间,脚本将会被一个SIGKILL信号终止,之后重新执行。
#通知型脚本:当sentinel有任何警告级别的事件发生时(比如说redis实例的主观失效和客观失效等等),将会去调用这个脚本,
#这时这个脚本应该通过邮件,SMS等方式去通知系统管理员关于系统不正常运行的信息。调用该脚本时,将传给脚本两个参数,
#一个是事件的类型,
#一个是事件的描述。
#如果sentinel.conf配置文件中配置了这个脚本路径,那么必须保证这个脚本存在于这个路径,并且是可执行的,否则sentinel无法正常启动成功。
#通知脚本
# sentinel notification-script
sentinel notification-script mymaster /var/redis/notify.sh
# 客户端重新配置主节点参数脚本
# 当一个master由于failover而发生改变时,这个脚本将会被调用,通知相关的客户端关于master地址已经发生改变的信息。
# 以下参数将会在调用脚本时传给脚本:
#
# 目前总是“failover”,
# 是“leader”或者“observer”中的一个。
# 参数 from-ip, from-port, to-ip, to-port是用来和旧的master和新的master(即旧的slave)通信的
# 这个脚本应该是通用的,能被多次调用,不是针对性的。
# sentinel client-reconfig-script
sentinel client-reconfig-script mymaster /var/redis/reconfig.sh
服务的高可用问题!
Redis缓存的使用,极大的提升了应用程序的性能和效率,特别是数据查询方面。但同时,它也带来了一 些问题。其中,最要害的问题,就是数据的一致性问题,从严格意义上讲,这个问题无解。如果对数据的一致性要求很高,那么就不能使用缓存。 另外的一些典型问题就是,缓存穿透、缓存雪崩和缓存击穿。目前,业界也都有比较流行的解决方案。
概念
通俗来讲就是,在缓存中没查到(缓存未命中),还要数据库中进行查找,
在默认情况下,用户请求数据时,会先在缓存(Redis)中查找,若没找到即缓存未命中,再在数据库中进行查找,数量少可能问题不大,可是一旦大量的请求数据(例如秒杀场景,秒杀前0访问,秒杀时高访问)缓存都没有命中的话,就会全部转移到数据库上,造成数据库极大的压力,就有可能导致数据库崩溃。网络安全中也有人恶意使用这种手段进行攻击被称为洪水攻击。
解决方案
方案1:布隆过滤器
对所有可能查询的参数以Hash的形式存储,以便快速确定是否存在这个值,在控制层先进行拦截校验,校验不通过直接打回,减轻了存储系统的压力。
方案2:缓存空对象
一次请求若在缓存和数据库中都没找到,就在缓存中方一个空对象用于处理后续这个请求。
这样做有一个缺陷:存储空对象也需要空间,大量的空对象会耗费一定的空间,存储效率并不高。解决这个缺陷的方式就是设置较短过期时间
即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响。
概念
访问量太大,缓存过期
相较于缓存穿透,缓存击穿的目的性更强,一个存在的key,在缓存过期的一刻,同时有大量的请求,这些请求都会击穿到DB,造成瞬时DB请求量大、压力骤增。这就是缓存被击穿,只是针对其中某个key的缓存不可用而导致击穿,但是其他的key依然可以使用缓存响应。
比如热搜排行上,一个热点新闻被同时大量访问就可能导致缓存击穿。
解决方案
设置热点数据永不过期
这样就不会出现热点数据过期的情况,但是当Redis内存空间满的时候也会清理部分数据,而且此种方案会占用空间,一旦热点数据多了起来,就会占用部分空间。
加互斥锁(分布式锁)
在访问key之前,采用SETNX(set if not exists)来设置另一个短期key来锁住当前key的访问,访问结束再删除该短期key。保证同时刻只有一个线程访问。这样对锁的要求就十分高。
概念
大量的key设置了相同的过期时间,导致大量缓存在同一时刻全部失效,造成瞬时DB请求量大、压力骤增,引起雪崩。
解决方案
redis高可用
这个思想的含义是,既然redis有可能挂掉,那我多增设几台redis,这样一台挂掉之后其他的还可以继续工作,其实就是搭建的集群
限流降级
这个解决方案的思想是,在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
数据预热
数据加热的含义就是在正式部署之前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。
(服务降级:比如双11,停掉退款业务,全力保证购买业务)