Redis最全教程

1. Redis安装

1.1 从官网下载安装包。

这里我们以Redis5.x为例。下载好后使用xftp上传到你的服务器上或者你的虚拟机中。目录可以任意,但建议把自己安装的软件放在/opt文件夹下。

使用命令tar -zxvf redis-5.0.8.tar.gz把下载的软件包解压,后面的要写你自己下载的包。

1.2安装Redis

进入到刚刚解压的Redis的文件夹。因为Redis是用c写的,所以要保证已经安装gcc了。(注:最新版的6.x在使用make命令可能会报错,需要升级下gcc就好。)

在Redis的文件夹中使用命令进行安装。

make 
make PREFIX=/usr/local/redis install 

第二行 命令是指定把软件安装的位置,如果不指定默认是安装在/usr/local/bin目录中。

安装好之后我们的Redis就安装在/usr/local/redis这个文件夹下了。安装好后,会发现没有redis.conf这个Redis的配置文件,这个配置文件在解压的时候的那个目录,把它拷贝到安装的目录中。这样每次启动的时候给他指定配置文件就好,而自带的配置文件不去动它。如果后期配置错了可以把这个删除然后重新拷贝过来。你不做这步复制,redis也能正常启动,只是的会用一套默认配置。

Redis安装之后的bin目录主要有一下的几个功能:

img

1.3启动测试Redis

首先修改一下拷贝过来的redis.conf文件。把后台启动打开,默认是前台启动。

image-20200919143554152

启动的时候,指定使用配置文件:# ./bin/redis-server ./redis.conf

出现如下的就说明启动成功,否则会报错。image-20200919144940574

我们可以使用Redis自带的redis-cli去连接Redis。使用命令redis-cli -p 6379注意需要指定端口。

然后如ping,他会回一个pong,说明搭建是成功的。

image-20200919145512785

1.4 关闭redis

暴力的方式是查询redis的进程号,然后使用kill命令。

正确的方式是,如果进入redis-cli可以如下操作:

image-20200919145806875

如果没有进入,则直接使用命令关闭./bin/redis-cli shutdown

1.5 redis的常用设置

redis的设置主要就是通过他的配置文件进行设置,目前主要可以设置如下几项:

  • 后台启动

    # 上面已经说了这里,把这个改成yes就可以了。
    daemonize yes
  • 设置用户名密码

    ## 在默认的情况下,redis是没有密码的,如果在测试的时候是没问题的,但是如果项目要共享出去或者是真实的项目,那么
    ## 就会有安全隐患,所以这种情况下需要设置密码。
    ## 设置的方式还是在配置文件中 redis.conf,找到requirepass 标签,把它前面的 `#`去掉,然后后面添加自己的密码即可
    requirepass yourpassword
    ## 设置好之后重启redis即可,重启后如果不进行下面两种方式认证,会发现你没有权限在redis中进行任何操作
    ## 使用redis-cli 登录的时候可以添加参数 -a 后面添加密码(这种是明文,但是会给警告,不管他)
    ## 第二种是按照原来的 redis-cli -p 6379 命令 登录,登录后 使用命令 `auth yourpassword` 进行验证(也是明文)
    
    
  • 设置允许远程连接

    ## bind字段默认为: bind 127.0.0.1 这样只能本机访问redis
    ## 若允许远程主机访问,可注释掉bind行   或者    将bind 127.0.0.1改为: bind 0.0.0.0

2.Redis的五大基本数据类型

基本操作

127.0.0.1:12138> keys * # 查看所有的key
(empty list or set)
127.0.0.1:12138> set user hello # 设置值
OK
127.0.0.1:12138> set age 18
OK
127.0.0.1:12138> EXISTS age  # 判断键存不存在
(integer) 1
127.0.0.1:12138> move age 1  # 把键移动到指定的数据库 ,redis有16个库,默认使用第一个也就是0库
(integer) 1
127.0.0.1:12138> keys *
1) "user"
127.0.0.1:12138> get user  # 根据key获取value
"hello"
127.0.0.1:12138> type user # 判断当前key对应的value的类型
string
127.0.0.1:12138> EXPIRE user 10 # 设置key的过期时间,单位是秒。-1表示没有过期时间
(integer) 1
127.0.0.1:12138> TTL user # 查看key的剩余时间
(integer) 6
127.0.0.1:12138> TTL user
(integer) 4
127.0.0.1:12138> TTL user # 时间为-2表示已过期,key已经不存在
(integer) -2
127.0.0.1:12138> keys *
(empty list or set)
127.0.0.1:12138> 
127.0.0.1:12138> select 1 # 切换到指定的数据库
OK
127.0.0.1:12138[1]> keys *
1) "age"
127.0.0.1:12138[1]> DBSIZE # 查看当前数据库的大小
(integer) 1

2.1 String(字符串)

#################################################################################################################
基本操作
127.0.0.1:12138> set k1 v1 # 设置值
OK
127.0.0.1:12138> get k1  # 获取值
"v1"
127.0.0.1:12138> keys * # 获取所有的key
1) "k1"
127.0.0.1:12138> EXISTS k1 # 判断某个key是否存在
(integer) 1
127.0.0.1:12138> APPEND k1 "append" # 追加字符串,如果当前key不存在,就相当于set key
(integer) 8
127.0.0.1:12138> get k1 
"v1append"
127.0.0.1:12138> APPEND k2 "not exist" 
(integer) 9
127.0.0.1:12138> keys *
1) "k1"
2) "k2"
127.0.0.1:12138> STRLEN k1 # 获取key对应的value的长度
(integer) 8
#################################################################################################################
# i++ i-- 自增,自减  以及指定步长
127.0.0.1:12138> set views 0  # 初始浏览量为0
OK
127.0.0.1:12138> incr views   # 自增一
(integer) 1
127.0.0.1:12138> incr views
(integer) 2
127.0.0.1:12138> get views
"2"
127.0.0.1:12138> decr views # 自减一
(integer) 1
127.0.0.1:12138> decr views 
(integer) 0
127.0.0.1:12138> decr views 
(integer) -1
127.0.0.1:12138> get views
"-1"
127.0.0.1:12138> INCRBY views 10 # 设置步长为10
(integer) 9
127.0.0.1:12138> DECRBY views 15 # 设置步长为15
(integer) -6
127.0.0.1:12138> get views
"-6"
#################################################################################################################
# 字符串范围 range
127.0.0.1:12138> set key1 "you are good"
OK
127.0.0.1:12138> get key1
"you are good"
127.0.0.1:12138> GETRANGE key1 0 3 # 截取字符串 [0,3]
"you "
127.0.0.1:12138> 
127.0.0.1:12138> GETRANGE key1 0 -1 # 获取全部的字符串 和 get key是一样的
"you are good"
# 替换!
127.0.0.1:12138> set key2 abcdefg
OK
127.0.0.1:12138> get key2
"abcdefg"
127.0.0.1:12138> SETRANGE key2 1 123 # 替换指定位置开始的字符串!
(integer) 7
127.0.0.1:12138> get key2
"a123efg"
#################################################################################################################
# setex (set with expire) # 设置过期时间
# setnx (set if not exist) # 不存在在设置 (在分布式锁中会常常使用!)
127.0.0.1:12138> setex key3 10 "hello" # 设置key3 的值为 hello,10秒后过期
OK
127.0.0.1:12138> ttl key3
(integer) 4
127.0.0.1:12138> ttl key3
(integer) 2
127.0.0.1:12138> ttl key3
(integer) -2
127.0.0.1:12138> get key3
(nil)
127.0.0.1:12138> setnx mykey "redis" # 如果mykey 不存在,创建mykey
(integer) 1
127.0.0.1:12138> setnx mykey "redisss" # 如果mykey存在,创建失败!不会修改之前的value
(integer) 0
127.0.0.1:12138> get mykey
"redis"
#################################################################################################################
# mset mget 批量操作
127.0.0.1:12138> mset k1 v1 k2 v2 k3 v3 # 同时设置多个值
OK
127.0.0.1:12138> keys *
1) "k1"
2) "k2"
3) "k3"
127.0.0.1:12138> mget k1 k2 k3 # 同时获取多个值
1) "v1"
2) "v2"
3) "v3"
127.0.0.1:12138> mget k1 k2 k4 # 如果某个键不存在就返回nil空
1) "v1"
2) "v2"
3) (nil)
127.0.0.1:12138> msetnx k1 v1 k4 v4 # msetnx 是一个原子性的操作,要么一起成功,要么一起失败!
(integer) 0
127.0.0.1:12138> get k4 
(nil)
127.0.0.1:12138> 
#################################################################################################################
getset # 先get然后在set
127.0.0.1:12138> getset db redis # 如果不存在值,则返回 nil,set是会执行的
(nil)
127.0.0.1:12138> get db
"redis"
127.0.0.1:12138> getset db kafka # 如果存在值,获取原来的值,并设置新的值
"redis"
127.0.0.1:12138> get db
"kafka"
#################################################################################################################

2.2 List(列表)

在redis里面,我们可以把list玩成 ,栈、队列、阻塞队列! 消息队列 (Lpush Rpop), 栈( Lpush Lpop)!
  • 他实际上是一个链表,before Node after , left,right 都可以插入值
  • 如果key 不存在,创建新的链表
  • 如果key存在,新增内容
  • 如果移除了所有值,空链表,也代表不存在!
  • 在两边插入或者改动值,效率最高! 中间元素,相对来说效率会低一点~
#################################################################################################################
127.0.0.1:12138> LPUSH list one two  # 将一个值或者多个值,插入到列表头部 (左)
(integer) 2
127.0.0.1:12138> LPUSH list three
(integer) 3
127.0.0.1:12138> LRANGE list 0 -1 # 获取list中值!
1) "three"
2) "two"
3) "one"
127.0.0.1:12138> LRANGE list 0 1 # 通过区间获取具体的值!
1) "three"
2) "two"
127.0.0.1:12138> RPUSH list right
(integer) 4
127.0.0.1:12138> LRANGE list 0 -1 # 将一个值或者多个值,插入到列表位部 (右)
1) "three"
2) "two"
3) "one"
4) "right"
#################################################################################################################
LPOP
RPOP
127.0.0.1:12138> LRANGE list 0 -1
1) "three"
2) "two"
3) "one"
4) "right"
127.0.0.1:12138> LPOP list # 移除list的第一个元素
"three"
127.0.0.1:12138> RPOP list # 移除list的最后一个元素
"right"
127.0.0.1:12138> LRANGE list 0 -1
1) "two"
2) "one"
#################################################################################################################
Lindex
127.0.0.1:12138> LRANGE list 0 -1
1) "two"
2) "one"
127.0.0.1:12138> LINDEX list 1 # 通过下标获得 list 中的某一个值!
"one"
127.0.0.1:12138> LINDEX list 0
"two"
#################################################################################################################
LLEN 
127.0.0.1:12138> LLEN list # 返回列表的长度
(integer) 2
#################################################################################################################
移除指定的值!
Lrem
127.0.0.1:6379> LRANGE list 0 -1
1) "three"
2) "three"
3) "two"
4) "one"
127.0.0.1:6379> lrem list 1 one # 移除list集合中指定个数的value,精确匹配
(integer) 1
127.0.0.1:6379> LRANGE list 0 -1
1) "three"
2) "three"
3) "two"
127.0.0.1:6379> lrem list 1 three
(integer) 1
127.0.0.1:6379> LRANGE list 0 -1
1) "three"
2) "two"
127.0.0.1:6379> Lpush list three
(integer) 3
127.0.0.1:6379> lrem list 2 three
(integer) 2
127.0.0.1:6379> LRANGE list 0 -1
1) "two"
## list里面 没有根据index去删除的,只有根据值删除的,如果想要删除指定index,有这两种方法
### 方法一 先把想删的设置成自己的值,然后删除自己的值
lset mylist index "del"
lrem mylist 1 "del"
### 方法二 也可以用事务管道合并成一次请求
multi
lset mylist index "del"
lrem mylist 1 "del"
exec
#################################################################################################################
trim 修剪。; list 截断!
127.0.0.1:6379> keys *
(empty list or set)
127.0.0.1:6379> Rpush mylist "hello"
(integer) 1
127.0.0.1:6379> Rpush mylist "hello1"
(integer) 2
127.0.0.1:6379> Rpush mylist "hello2"
(integer) 3
127.0.0.1:6379> Rpush mylist "hello3"
(integer) 4
127.0.0.1:6379> ltrim mylist 1 2 # 通过下标截取指定的长度,这个list已经被改变了,截断了只剩下截取的元素!
OK
127.0.0.1:6379> LRANGE mylist 0 -1
1) "hello1"
2) "hello2"
#################################################################################################################
rpoplpush # 移除列表的最后一个元素,将他移动到新的列表中!
127.0.0.1:6379> rpush mylist "hello"
(integer) 1
127.0.0.1:6379> rpush mylist "hello1"
(integer) 2
127.0.0.1:6379> rpush mylist "hello2"
(integer) 3
127.0.0.1:6379> rpoplpush mylist myotherlist # 移除列表的最后一个元素,将他移动到新的列表中!
"hello2"
127.0.0.1:6379> lrange mylist 0 -1 # 查看原来的列表
1) "hello"
2) "hello1"
127.0.0.1:6379> lrange myotherlist 0 -1 # 查看目标列表中,确实存在改值!
1) "hello2"
#################################################################################################################
lset 将列表中指定下标的值替换为另外一个值,更新操作
127.0.0.1:6379> EXISTS list # 判断这个列表是否存在
(integer) 0
127.0.0.1:6379> lset list 0 item # 如果不存在列表我们去更新就会报错
(error) ERR no such key
127.0.0.1:6379> lpush list value1
(integer) 1
127.0.0.1:6379> LRANGE list 0 0
1) "value1"
127.0.0.1:6379> lset list 0 item # 如果存在,更新当前下标的值
OK
127.0.0.1:6379> LRANGE list 0 0
1) "item"
127.0.0.1:6379> lset list 1 other # 如果不存在,则会报错!
(error) ERR index out of range
#################################################################################################################
linsert # 将某个具体的value插入到列把你中某个元素的前面或者后面!
127.0.0.1:6379> Rpush mylist "hello"
(integer) 1
127.0.0.1:6379> Rpush mylist "world"
(integer) 2
127.0.0.1:6379> LINSERT mylist before "world" "other"
(integer) 3
127.0.0.1:6379> LRANGE mylist 0 -1
1) "hello"
2) "other"
3) "world"
127.0.0.1:6379> LINSERT mylist after world new
(integer) 4
127.0.0.1:6379> LRANGE mylist 0 -1
1) "hello"
2) "other"
3) "world"
4) "new"
#################################################################################################################

2.3 Set(集合)

set中的值是不能重复的!

应用场景:

微博,A用户将所有关注的人放在一个set集合中!将它的粉丝也放在一个集合中!
共同关注,共同爱好,二度好友,推荐好友!(六度分割理论)

#################################################################################################################
127.0.0.1:12138> sadd myset are # set集合中添加value
(integer) 1
127.0.0.1:12138> sadd myset you
(integer) 1
127.0.0.1:12138> sadd myset ok
(integer) 1
127.0.0.1:12138> SMEMBERS myset # 查看指定set的所有值
1) "you"
2) "are"
3) "ok"
127.0.0.1:12138> SISMEMBER myset leijun  # 判断某一个值是不是在set集合中!
(integer) 0
127.0.0.1:12138> SISMEMBER myset are
(integer) 1
#################################################################################################################
127.0.0.1:12138> SCARD myset # 获取set集合中的内容元素个数!
(integer) 3

#################################################################################################################
删除元素 srem
127.0.0.1:6379> srem myset you # 移除set集合中的指定元素
(integer) 1
127.0.0.1:6379> scard myset
(integer) 3
127.0.0.1:6379> SMEMBERS myset
1) "are"
2) "ok"
#################################################################################################################
set 无序不重复集合。抽随机!
127.0.0.1:12138> sadd myset you
(integer) 1
127.0.0.1:12138> SMEMBERS myset
1) "you"
2) "are"
3) "ok"
127.0.0.1:12138> SRANDMEMBER myset # 随机抽选出一个元素
"ok"
127.0.0.1:12138> SRANDMEMBER myset
"ok"
127.0.0.1:12138> SRANDMEMBER myset
"ok"
127.0.0.1:12138> SRANDMEMBER myset
"are"
127.0.0.1:12138> SRANDMEMBER myset
"ok"
127.0.0.1:12138> SRANDMEMBER myset 2 # 随机抽选出指定个数的元素
1) "you"
2) "ok"
#################################################################################################################
随机删除key!
127.0.0.1:12138> SMEMBERS myset
1) "you"
2) "are"
3) "ok"
127.0.0.1:12138>  spop myset # 随机删除一个key
"you"
127.0.0.1:12138>  spop myset
"are"
127.0.0.1:12138> SMEMBERS myset 
1) "ok"
127.0.0.1:12138>  spop myset 2 # 随机删除指定个数的key,如果集合中的个数不够就会有多少删除多少
1) "ok"
127.0.0.1:12138> SMEMBERS myset
(empty list or set)
#################################################################################################################
将一个指定的值,移动到另外一个set集合!
127.0.0.1:12138> sadd myset are
(integer) 1
127.0.0.1:12138> sadd myset you
(integer) 1
127.0.0.1:12138> sadd myset ok
(integer) 1
127.0.0.1:12138> sadd myset leijun
(integer) 1
127.0.0.1:12138> smove myset newmyset leijun # 将一个指定的值,移动到另外一个set集合!
(integer) 1
127.0.0.1:12138> SMEMBERS myset
1) "you"
2) "are"
3) "ok"
127.0.0.1:12138> SMEMBERS newmyset
1) "leijun"
#################################################################################################################
微博,B站,共同关注!(并集)
数字集合类:
- 差集 SDIFF
- 交集
- 并集
127.0.0.1:12138> sadd k1 a b c d f e g 
(integer) 7
127.0.0.1:12138> sadd k2 a b c d h i j k  
(integer) 8
127.0.0.1:12138> SDIFF k1 k2 # 差集 相对于k1, k2不存在的值
1) "f"
2) "e"
3) "g"
127.0.0.1:12138> SINTER k1 k2 # 交集
1) "c"
2) "b"
3) "d"
4) "a"
127.0.0.1:12138> SUNION k1 k2  # 并集
 1) "j"
 2) "c"
 3) "g"
 4) "h"
 5) "e"
 6) "i"
 7) "f"
 8) "d"
 9) "b"
10) "k"
11) "a"
#################################################################################################################

 2.4 Hash(哈希)

Map集合,key-map! 时候这个值是一个map集合! 本质和String类型没有太大区别,还是一个简单的
key-vlaue!
set myhash myfield myvalue

hash 更适合于对象的存储,String更加适合字符串存储!

#################################################################################################################
127.0.0.1:12138> hset myhash field1 hello # set一个具体 key-vlaue
(integer) 1
127.0.0.1:12138> hget myhash field1 # 获取一个字段值
"hello"
127.0.0.1:12138> hset hash f1 v1 f2 v2 # 目前测试的hset也可以同时设置多个值,和hmset的唯一区别就是,他返回的是影响的条数,如果原来有则覆盖,没有则新增,而后者只会返回一个字符串OK
(integer) 2
127.0.0.1:12138> hmset hash f3 v3 f4 v4 # set多个 key-vlaue
OK
127.0.0.1:12138> HGETALL hash # 获取全部的数据
1) "f1"
2) "v1"
3) "f2"
4) "v2"
5) "f3"
6) "v3"
7) "f4"
8) "v4"
127.0.0.1:12138> hdel hash f1 # 删除hash指定key字段!对应的value值也就消失了!
(integer) 1
127.0.0.1:12138> hdel hash f2 f3 # 可以一次删除多个字段
(integer) 2
127.0.0.1:12138> hdel hash f2  # 删除不存在的会返回影响的条数
(integer) 0 
127.0.0.1:12138> HGETALL hash
1) "f4"
2) "v4"
#################################################################################################################
hlen # 获取hash的字段数量
127.0.0.1:12138> HGETALL hash
1) "f4"
2) "v4"
127.0.0.1:12138> hset hash f1 v1 f2 v2
(integer) 2
127.0.0.1:12138> HGETALL hash
1) "f4"
2) "v4"
3) "f1"
4) "v1"
5) "f2"
6) "v2"
127.0.0.1:12138> hlen hash # 获取hash表的字段数量!
(integer) 3
#################################################################################################################
127.0.0.1:12138> HEXISTS hash f5 # 判断hash中指定字段是否存在!
(integer) 0
127.0.0.1:12138> HEXISTS hash f1
(integer) 1
#################################################################################################################
# 只获得所有field
# 只获得所有value
127.0.0.1:12138> hkeys hash # 只获得所有field
1) "f4"
2) "f1"
3) "f2"
127.0.0.1:12138> HVALS hash # 只获得所有value
1) "v4"
2) "v1"
3) "v2"
#################################################################################################################
# incr decr hash 中只有HINCRBY实现增加的api,当然增量可以使负的

127.0.0.1:12138> hset mytest age 5
(integer) 1
127.0.0.1:12138> HINCRBY myset age 1 #指定增量!
(integer) 1
127.0.0.1:12138> HINCRBY myset age 3
(integer) 4
127.0.0.1:12138> HINCRBY myset age -5 #指定增量!
(integer) -1
127.0.0.1:12138> hget myset age
"-1"
#################################################################################################################
# HSETNX 不为空的时候设置
127.0.0.1:12138> HSETNX myset f6 v6 # 如果不存在则可以设置
(integer) 1
127.0.0.1:12138> HSETNX myset f6 v5 # 如果存在则不能设置
(integer) 0
#################################################################################################################

2.5 Zset(有序集合)

在set的基础上,增加了一个值,set k1 v1 zset k1 score1 v1

应用场景:set 排序 存储班级成绩表,工资表排序!
普通消息,1, 重要消息 2,带权重进行判断!
排行榜应用实现,取Top N 测试!

#################################################################################################################
127.0.0.1:6379> zadd myset 1 one # 添加一个值
(integer) 1
127.0.0.1:6379> zadd myset 2 two 3 three # 添加多个值
(integer) 2
127.0.0.1:6379> ZRANGE myset 0 -1
1) "one"
2) "two"
3) "three"
#################################################################################################################
排序如何实现
# zset中也可以使用zrange 进行排序,和普通的range一样。(默认是从小到大,range能指定索引,而rangebyscore能指定分数)
127.0.0.1:6379> zadd salary 2500 xiaohong # 添加三个用户
(integer) 1
127.0.0.1:6379> zadd salary 5000 zhangsan
(integer) 1
127.0.0.1:6379> zadd salary 500 lisi
(integer) 1
# ZRANGEBYSCORE key min max
127.0.0.1:6379> ZRANGEBYSCORE salary -inf +inf # 显示全部的用户 从小到大!
1) "lisi"
2) "xiaohong"
3) "zhangsan"
127.0.0.1:6379> ZREVRANGE salary 0 -1 # 从大到进行排序!
1) "xiaohong"
2) "zhangsan"
3) "lisi"
127.0.0.1:6379> ZRANGEBYSCORE salary -inf +inf withscores # 显示全部的用户并且附带成
绩
1) "lisi"
2) "500"
3) "xiaohong"
4) "2500"
5) "zhangsan"
6) "5000"
127.0.0.1:6379> ZRANGEBYSCORE salary -inf 2500 withscores # 显示工资小于2500员工的升
序排序!
1) "lisi"
2) "500"
3) "xiaohong"
4) "2500"
#################################################################################################################
# 移除rem中的元素
127.0.0.1:6379> zrange salary 0 -1
1) "kaungshen"
2) "xiaohong"
3) "zhangsan"
127.0.0.1:6379> zrem salary xiaohong # 移除有序集合中的指定元素
(integer) 1
127.0.0.1:6379> zrange salary 0 -1
1) "kaungshen"
2) "zhangsan"
127.0.0.1:6379> zcard salary # 获取有序集合中的个数
(integer) 2
#################################################################################################################
127.0.0.1:6379> zadd myset 1 hello
(integer) 1
127.0.0.1:6379> zadd myset 2 world 3 kuangshen
(integer) 2
127.0.0.1:6379> zcount myset 1 3 # 获取指定区间的成员数量!
(integer) 3
127.0.0.1:6379> zcount myset 1 2
(integer) 2
#################################################################################################################

3.Redis的三种特殊数据类型

3.1Geospatial 地理位置

朋友的定位,附近的人,打车距离计算?
Redis 的 Geo 在Redis3.2 版本就推出了! 这个功能可以推算地理位置的信息,两地之间的距离,方圆
几里的人!

一共就6个命令

补充

# geo没有删除命令,我们可以使用zrem去删除,其实他底层使用的是 Zset!我们可以使用Zset命令来操作geo! 当然geo的最底层还是跳跃链表
127.0.0.1:6379> ZRANGE china:city 0 -1 # 查看地图中全部的元素
1) "chongqi"
2) "xian"
3) "shengzhen"
4) "hangzhou"
5) "shanghai"
6) "beijing"
127.0.0.1:6379> zrem china:city beijing # 移除指定元素! 这个常用
(integer) 1
127.0.0.1:6379> ZRANGE china:city 0 -1
1) "chongqi"
2) "xian"
3) "shengzhen"
4) "hangzhou"
5) "shanghai"
#################################################################################################################
# getadd 添加地理位置
# 命令:**GEOADD** key longitude latitude member [longitude latitude member ...]
# 命令描述:将指定的地理空间位置(经度、纬度、名称)添加到指定的key中。
# 规则:北极和南极无法直接添加,我们一般会下载城市数据,直接通过java程序一次性导入!
# 有效的经度从-180度到180度。
# 有效的纬度从-85.05112878度到85.05112878度。
# 当坐标位置超出上述指定范围时,该命令将会返回一个错误。
# 127.0.0.1:6379> geoadd china:city 39.90 116.40 beijin
(error) ERR invalid longitude,latitude pair 39.900000,116.400000
# 参数 key 值()
127.0.0.1:6379> geoadd china:city 116.40 39.90 beijing
(integer) 1
127.0.0.1:6379> geoadd china:city 121.47 31.23 shanghai
(integer) 1
127.0.0.1:6379> geoadd china:city 106.50 29.53 chongqi 114.05 22.52 shengzhen
(integer) 2
127.0.0.1:6379> geoadd china:city 120.16 30.24 hangzhou 108.96 34.26 xian
(integer) 2
#################################################################################################################
# getpos 获得当前定位:一定是一个坐标值!
27.0.0.1:6379> GEOPOS china:city beijing # 获取指定的城市的经度和纬度!
1) 1) "116.39999896287918091"
2) "39.90000009167092543"
127.0.0.1:6379> GEOPOS china:city beijing chongqi
1) 1) "116.39999896287918091"
2) "39.90000009167092543"
2) 1) "106.49999767541885376"
2) "29.52999957900659211"
#################################################################################################################
# GEODIST 两个位置之间的距离 单位:
m 表示单位为米。
km 表示单位为千米。
mi 表示单位为英里。
ft 表示单位为英尺
127.0.0.1:6379> GEODIST china:city beijing shanghai km # 查看上海到北京的直线距离
"1067.3788"
127.0.0.1:6379> GEODIST china:city beijing chongqi km # 查看重庆到北京的直线距离
"1464.0708"
#################################################################################################################
# georadius 以给定的经纬度为中心, 找出某一半径内的元素
127.0.0.1:6379> GEORADIUS china:city 110 30 1000 km # 以110,30 这个经纬度为中心,寻找方圆1000km内的城市
1) "chongqi"
2) "xian"
3) "shengzhen"
4) "hangzhou"
127.0.0.1:6379> GEORADIUS china:city 110 30 500 km
1) "chongqi"
2) "xian"
127.0.0.1:6379> GEORADIUS china:city 110 30 500 km withdist # 显示到中间距离的位置
1) 1) "chongqi"
2) "341.9374"
2) 1) "xian"
2) "483.8340"
127.0.0.1:6379> GEORADIUS china:city 110 30 500 km withcoord # 显示他人的定位信息
1) 1) "chongqi"
2) 1) "106.49999767541885376"
2) "29.52999957900659211"
2) 1) "xian"
2) 1) "108.96000176668167114"
2) "34.25999964418929977"
127.0.0.1:6379> GEORADIUS china:city 110 30 500 km withdist withcoord count 1 #筛选出指定的结果!
1) 1) "chongqi"
2) "341.9374"
3) 1) "106.49999767541885376"
2) "29.52999957900659211"
127.0.0.1:6379> GEORADIUS china:city 110 30 500 km withdist withcoord count 2
1) 1) "chongqi"
2) "341.9374"
3) 1) "106.49999767541885376"
2) "29.52999957900659211"
2) 1) "xian"
2) "483.8340"
3) 1) "108.96000176668167114"
2) "34.25999964418929977"
#################################################################################################################
# GEORADIUSBYMEMBER 找出位于指定元素周围的其他元素!
127.0.0.1:6379> GEORADIUSBYMEMBER china:city beijing 1000 km
1) "beijing"
2) "xian"
127.0.0.1:6379> GEORADIUSBYMEMBER china:city shanghai 400 km
1) "hangzhou"
2) "shanghai"
#################################################################################################################
# GEOHASH 命令 - 返回一个或多个位置元素的 Geohash 表示
该命令将返回11个字符的Geohash字符串!
127.0.0.1:6379> geohash china:city beijing chongqi
1) "wx4fbxxfke0"
2) "wm5xzrybty0"
#################################################################################################################

3.2 Hyperloglog

什么是基数?

A {1,3,5,7,8,7}
B{1,3,5,7,8}

基数(不重复的元素) = 5,可以接受误差!

Redis Hyperloglog 基数统计的算法!(这个也可以去学习学习)

优点:占用的内存是固定,2^64 不同的元素的技术,只需要废占用12KB内存!如果要从内存角度来比较的话 Hyperloglog 首选!

使用场景:网页的 UV (一个人访问一个网站多次,但是还是算作一个人!)
传统的方式, set 保存用户的id,然后就可以统计 set 中的元素数量作为标准判断 !
这个方式如果保存大量的用户id,就会比较麻烦!我们的目的是为了计数,而不是保存用户id;
0.81% 错误率! 统计UV任务,可以忽略不计的!

如果允许容错,那么一定可以使用 Hyperloglog !
如果不允许容错,就使用 set 或者自己的数据类型即可!

127.0.0.1:6379> PFadd mykey a b c d e f g h i j # 创建第一组元素 mykey
(integer) 1
127.0.0.1:6379> PFCOUNT mykey # 统计 mykey 元素的基数数量
(integer) 10
127.0.0.1:6379> PFadd mykey2 i j z x c v b n m # 创建第二组元素 mykey2
(integer) 1
127.0.0.1:6379> PFCOUNT mykey2
(integer) 9
127.0.0.1:6379> PFMERGE mykey3 mykey mykey2 # 合并两组 mykey mykey2 => mykey3 并集
OK
127.0.0.1:6379> PFCOUNT mykey3 # 看并集的数量!
(integer) 15

3.3 Bitmap 位存储

使用场景:

统计用户信息,活跃,不活跃! 登录 、 未登录! 打卡,365打卡! 两个状态的,都可以使用
Bitmaps!
Bitmap 位图,数据结构! 都是操作二进制位来进行记录,就只有0 和 1 两个状态!
365 天 = 365 bit 1字节 = 8bit 46 个字节左右!

# 使用bitmap 来记录 周一到周日的打卡!
# 周一:1 周二:0 周三:0 周四:1 ......
127.0.0.1:6379> setbit sign 0 1
(integer) 0
127.0.0.1:6379> setbit sign 1 0
(integer) 0
127.0.0.1:6379> setbit sign 2 0
(integer) 0
127.0.0.1:6379> setbit sign 3 1
(integer) 0
127.0.0.1:6379> setbit sign 4 1
(integer) 0
127.0.0.1:6379> setbit sign 5 0
(integer) 0
127.0.0.1:6379> setbit sign 6 0
(integer) 0
127.0.0.1:6379> getbit sign 3 # 查看某一天是否有打卡!
(integer) 1
127.0.0.1:6379> getbit sign 6
(integer) 0
127.0.0.1:6379> bitcount sign # 统计这周的打卡记录,就可以看到是否有全勤!
(integer) 3

4.Redis中的事务

事务简介

Redis 事务本质:一组命令的集合! 一个事务中的所有命令都会被序列化,在事务执行过程的中,会按照顺序执行!
一次性、顺序性、排他性!执行一些列的命令!
Redis事务没有没有隔离级别的概念!
所有的命令在事务中,并没有直接被执行!只有发起执行命令的时候才会执行!Exec
Redis单条命令式保存原子性的,但是事务不保证原子性!
redis的事务:
开启事务(multi)
命令入队(......)
执行事务(exec)

Redis的事务操作

#################################################################################################################
# 正常执行事务!
127.0.0.1:6379> multi # 开启事务
OK 
127.0.0.1:6379> set k1 v1 # 命令入队
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> get k2
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> exec # 执行事务
1) OK
2) OK
3) "v2"
4) OK
#################################################################################################################
# 放弃事务!
127.0.0.1:6379> multi # 开启事务
OK
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> set k4 v4
QUEUED
127.0.0.1:6379> DISCARD # 取消事务
OK
127.0.0.1:6379> get k4 # 事务队列中命令都不会被执行!
(nil)
#################################################################################################################
# 编译型异常(代码有问题! 命令有错!) ,事务中所有的命令都不会被执行!
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> getset k3 # 错误的命令
(error) ERR wrong number of arguments for 'getset' command
127.0.0.1:6379> set k4 v4
QUEUED
127.0.0.1:6379> set k5 v5
QUEUED
127.0.0.1:6379> exec # 执行事务报错!
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get k5 # 所有的命令都不会被执行!
(nil)
#################################################################################################################
# 运行时异常(1/0), 如果事务队列中存在语法性,那么执行命令的时候,其他命令是可以正常执行的,错误命令抛出异常!
127.0.0.1:6379> set k1 "v1"
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> incr k1 # 会执行的时候失败!
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> get k3
QUEUED
127.0.0.1:6379> exec
1) (error) ERR value is not an integer or out of range # 虽然第一条命令报错了,但是
依旧正常执行成功了!
2) OK
3) OK
4) "v3"
127.0.0.1:6379> get k2
"v2"
127.0.0.1:6379> get k3
"v3"
#################################################################################################################

事务的乐观锁,不是另一个线程,是另一个客户端,关于乐观锁可以参考下这个:https://my.oschina.net/itommy...

https://www.cnblogs.com/marti...

证明:

127.0.0.1:12138> set money 100
OK
127.0.0.1:12138> set mon 0
OK
127.0.0.1:12138> watch money
OK
127.0.0.1:12138> incrby money 55
(integer) 155
127.0.0.1:12138> multi
OK
127.0.0.1:12138> decr money
QUEUED
127.0.0.1:12138> incr mon
QUEUED
127.0.0.1:12138> exec
(nil)
127.0.0.1:12138> get money
"155"
127.0.0.1:12138> 

Redis 的监控机制(监控! Watch (面试常问!))

可以使用Redis的监控机制实现乐观锁或者分布式锁

测试:

#################################################################################################################
# 正常执行成功!
127.0.0.1:6379> set money 100
OK
127.0.0.1:6379> set out 0
OK
127.0.0.1:6379> watch money # 监视 money 对象
OK
127.0.0.1:6379> multi # 事务正常结束,数据期间没有发生变动,这个时候就正常执行成功!
OK
127.0.0.1:6379> DECRBY money 20
QUEUED
127.0.0.1:6379> INCRBY out 20
QUEUED
127.0.0.1:6379> exec
1) (integer) 80
2) (integer) 20
#################################################################################################################
# 测试多线程修改值 , 使用watch 可以当做redis的乐观锁操作!
127.0.0.1:6379> watch money # 监视 money
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> DECRBY money 10
QUEUED
127.0.0.1:6379> INCRBY out 10
QUEUED
127.0.0.1:6379> exec # 执行之前,再打开一个客户端,修改了我们的值,这个时候,就会导致事务执行失败!
(nil)
#################################################################################################################

如果修改失败,获取最新的值就好

image-20200926204958179

监控机制不会带来ABA问题

redis是单线程的,在使用watch进行监控的时候,一旦修改就会被watch,因为修改的时候就是那个线程,所以redis的watch不存在aba问题。 使用watch监视一个或者多个key,跟踪key的value修改情况,如果有key的value值在 事务exec执行之前被修改了,整个事务被取消。exec返回提示信息,表示事务已经失败。但是如果使用watch监视了一个带过期时间的键,那么即使这个键过期了,事务仍然可以正常执行。

5.Java中使用Redis

5.1通过jedis连接Redis

Java操作redis最基础的就是用jedis。Jedis 是 Redis 官方推荐的 java连接开发工具! 使用Java 操作Redis 中间件!如果你要使用
java操作redis,那么一定要对Jedis 十分的熟悉!

使用步骤:

  • 导入相关的依赖

    <!--导入jedis的包-->
    <dependencies>
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>3.2.0</version>
        </dependency>
    <!--fastjson-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.62</version>
        </dependency>
    </dependencies>
    • 编写测试代码
    public class JedisDemo {
        public static void main(String[] args) {
        // 1、 new Jedis 对象即可
            Jedis jedis = new Jedis("www.njitzyd.com",12138);
            // 如果设置了密码需要进行验证
            jedis.auth("zydredis");
         // jedis 所有的命令就是我们之前学习的所有指令!所以之前的指令学习很重要!
            System.out.println(jedis.ping());
        }
    }
    
    • 查看结果

      image-20200926215422152

可以看到连接成功!

Jedis中常用的API

#################################################################################################################
# 所有的api命令,就是我们对应的上面学习的指令,一个都没有变化!所有的都可以通过Jedis对象完成操作,和之前的命令行中的命令完全一致!
String
List
Set
Hash
Zset
#################################################################################################################
# 事务 也是和之前在命令行中的一致
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("hello","world");
jsonObject.put("name","leijun");
// 开启事务
Transaction multi = jedis.multi();
String result = jsonObject.toJSONString();
// jedis.watch(result)
try {
multi.set("user1",result);
multi.set("user2",result);
int i = 1/0 ; // 代码抛出异常事务,执行失败!
multi.exec(); // 执行事务!
} catch (Exception e) {
multi.discard(); // 放弃事务
e.printStackTrace();
} finally {
System.out.println(jedis.get("user1"));
System.out.println(jedis.get("user2"));
jedis.close(); // 关闭连接
}
}
}
#################################################################################################################
    

5.2 SpringBoot整合Redis

在SpringBoot 2.x 版本中,使用lettuce替代了jedis来操作Redis。

jedis : 采用的直连,多个线程操作的话,是不安全的,如果想要避免不安全的,使用 jedis pool 连接
池! 更像 BIO 模式
lettuce : 采用netty,实例可以再多个线程中进行共享,不存在线程不安全的情况!可以减少线程数据
了,更像 NIO 模式

源码解析
@Bean
@ConditionalOnMissingBean(name = "redisTemplate") // 我们可以自己定义一个redisTemplate来替换这个默认的!
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory
redisConnectionFactory)
throws UnknownHostException {
// 默认的 RedisTemplate 没有过多的设置,redis 对象都是需要序列化!
// 两个泛型都是 Object, Object 的类型,我们后使用需要强制转换 <String, Object>
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
@Bean
@ConditionalOnMissingBean // 由于 String 是redis中最常使用的类型,所以说单独提出来了一
个bean!
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory
redisConnectionFactory)
throws UnknownHostException {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
SpringBoot中使用案例

使用的步骤:

  • 添加依赖(是springboot项目)

    <!-- 操作redis -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
  • 配置连接

    在spring.properties中配置如下

    # 配置redis
    spring.redis.host=127.0.0.1
    spring.redis.port=6379
  • 测试

    @SpringBootTest
    class Redis02SpringbootApplicationTests {
    @Autowired
    private RedisTemplate redisTemplate;
    @Test
    void contextLoads() {
    // redisTemplate 操作不同的数据类型,api和我们的指令是一样的
    // opsForValue 操作字符串 类似String
    // opsForList 操作List 类似List
    // opsForSet
    // opsForHash
    // opsForZSet
    // opsForGeo
    // opsForHyperLogLog
    // 除了进本的操作,我们常用的方法都可以直接通过redisTemplate操作,比如事务,和基本的CRUD
    // 获取redis的连接对象
    // RedisConnection connection =redisTemplate.getConnectionFactory().getConnection();
    // connection.flushDb();
    // connection.flushAll();
    redisTemplate.opsForValue().set("mykey","myvalue");
    System.out.println(redisTemplate.opsForValue().get("mykey"));
    }
    }
自定义RedisTemplate

自带的RedisTemplate的问题:

  1. 默认的序列化方式是jdk自带的,当直接存入没有实现Serializable的类会直接报错序列化失败。

    image-20200926225941986

  2. 自带的两个泛型都是Object,而我们经常使用的是key为string类型。
  3. 我们无法指定序列化的方式,而实际开发中经常使用fastjson或者Jackson来实现序列化。

自定义如下,基本满足需求:

// 声明为一个配置类
@Configuration
public class RedisConfig {
// 自己定义了一个 RedisTemplate
@Bean
@SuppressWarnings("all")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory
factory) {
// 我们为了自己开发方便,一般直接使用 <String, Object>
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;
}
}

这样就可以实现自定义的RedisTemplate,当我们自定义时,系统自带的就不会初始化。(springboot的starter机制,在自带的RedisTemplate中有这个注解@ConditionalOnMissingBean(name = "redisTemplate")

自定义redis工具类

就是对上面自定义的RedisTemplate之后还可以再次封装,从而使得redis操作更方便。

// 在我们真实的分发中,或者你们在公司,一般都可以看到一个公司自己封装RedisUtil
@Component
public final class RedisUtil {

    // 这里要注意,注入的是我们刚刚自定义的RedisTemplate,而不是官方默认的!
    @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;
        }
    }
}

5.3 jedis和lu的对比

6.Redis的配置文件redis.conf详解

7.Redis持久化

Redis 是内存数据库,如果不将内存中的数据库状态保存到磁盘,那么一旦服务器进程退出,服务器中
的数据库状态也会消失。所以 Redis 提供了持久化功能!

7.1 RDB(Redis DataBase)

7.1.1简介

image-20200928205610092

在指定的时间间隔内将内存中的数据集快照写入磁盘,也就是行话讲的Snapshot快照,它恢复时是将快照文件直接读到内存里。
Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何IO操作的。这就确保了极高的性能。如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。RDB的缺点是最后一次持久化后的数据可能丢失。我们默认的就是RDB,一般情况下不需要修改这个配置!

rdb保存的文件是默认的dump.rdb 就是在我们的配置文件中快照中进行配置的!具体配置就是上面所描述的redis.conf文件。

7.1.2 触发机制

默认的save的规则:

image-20200928210407847

# 如果900s内,如果至少有一个1 key进行了修改,我们及进行持久化操作
save 900 1
# 如果300s内,如果至少10 key进行了修改,我们及进行持久化操作
save 300 10
# 如果60s内,如果至少10000 key进行了修改,我们及进行持久化操作
save 60 10000

当满足下面的条件时就会触发生成dump.rdb:

  1. save的规则满足的情况下,会自动触发rdb规则(上述的规则是针对bgsave命令的,是对bgsave命令生效的)
  2. 当执行save或者bgsave命令的时候,会触发rdb生成rdb文件(save和bgsave的区别)
  3. 执行 flushall 命令,也会触发我们的rdb规则!
  4. 退出redis(使用shutdown命令会触发,shutdown nosave 命令不会触发,使用kill命令强制退出也不会触发),也会产生 rdb 文件!

7.1.3 RDB文件保存过程

  • redis调用fork,现在有了子进程和父进程。
  • 父进程继续处理client请求,子进程负责将内存内容写入到临时文件。由于os的写时复制机制(copy on write)父子进程会共享相同的物理页面,当父进程处理写请求时os会为父进程要修改的页面创建副本,而不是写共享的页面。所以子进程的地址空间内的数据是fork时刻整个数据库的一个快照。
  • 当子进程将快照写入临时文件完毕后,用临时文件替换原来的快照文件,然后子进程退出。

7.1.4数据恢复

1、只需要将rdb文件放在我们redis启动目录就可以,redis启动的时候会自动检查dump.rdb 恢复其中的数据!(默认就是在这个位置)
2、查看需要存在的位置

127.0.0.1:6379> config get dir
1) "dir"
2) "/usr/local/bin" # 如果在这个目录下存在 dump.rdb 文件,启动就会自动恢复其中的数据

7.1.4 RDB的优缺点

优势

  • 一旦采用该方式,那么你的整个Redis数据库将只包含一个文件,这样非常方便进行备份。比如你可能打算没1天归档一些数据。
  • 方便备份,我们可以很容易的将一个一个RDB文件移动到其他的存储介质上
  • RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。
  • RDB 可以最大化 Redis 的性能:父进程在保存 RDB 文件时唯一要做的就是 fork 出一个子进程,然后这个子进程就会处理接下来的所有保存工作,父进程无须执行任何磁盘 I/O 操作。

劣势

  • 如果你需要尽量避免在服务器故障时丢失数据,那么 RDB 不适合你。 虽然 Redis 允许你设置不同的保存点(save point)来控制保存 RDB 文件的频率, 但是, 因为RDB 文件需要保存整个数据集的状态, 所以它并不是一个轻松的操作。 因此你可能会至少 5 分钟才保存一次 RDB 文件。 在这种情况下, 一旦发生故障停机, 你就可能会丢失好几分钟的数据。
  • 每次保存 RDB 的时候,Redis 都要 fork() 出一个子进程,并由子进程来进行实际的持久化工作。 在数据集比较庞大时, fork() 可能会非常耗时,造成服务器在某某毫秒内停止处理客户端; 如果数据集非常巨大,并且 CPU 时间非常紧张的话,那么这种停止时间甚至可能会长达整整一秒。 虽然 AOF 重写也需要进行 fork() ,但无论 AOF 重写的执行间隔有多长,数据的耐久性都不会有任何损失。

7.2 AOF(Append Only File)

将我们的所有命令都记录下来,history,恢复的时候就把这个文件全部在执行一遍!

image-20200928224944407

以日志的形式来记录每个写操作,将Redis执行过的所有指令记录下来(读操作不记录),只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作
Aof保存的是 appendonly.aof 文件

image-20200928225027558

默认是不开启的,我们需要手动进行配置!我们只需要将 appendonly 改为yes就开启了 aof!其他的默认就好,可以参考上面配置文件部分的讲解。重启,redis 就可以生效了!

如果如果aof文件被破坏,比如手动修改里面的内容,可以使用redis-check-aof来进行修复,具体命令是:redis-check-aof --fix appendonly.aof(可能会把错误的那条数据给删除,会造成丢失数据。但是这种手动修改数据的场景不多见)

7.2.1 AOF 简介

redis会将每一个收到的写命令都通过write函数追加到文件中(默认是 appendonly.aof)。

当redis重启时会通过重新执行文件中保存的写命令来在内存中重建整个数据库的内容。当然由于os会在内核中缓存 write做的修改,所以可能不是立即写到磁盘上。这样aof方式的持久化也还是有可能会丢失部分修改。不过我们可以通过配置文件告诉redis我们想要 通过fsync函数强制os写入到磁盘的时机。有三种方式如下(默认是:每秒fsync一次)

appendonly yes              //启用aof持久化方式
# appendfsync always      //每次收到写命令就立即强制写入磁盘,最慢的,但是保证完全的持久化,不推荐使用
appendfsync everysec     //每秒钟强制写入磁盘一次,在性能和持久化方面做了很好的折中,推荐
# appendfsync no    //完全依赖os,性能最好,持久化没保证

7.2.2 AOF的rewrite机制

aof 的方式也同时带来了另一个问题。持久化文件会变的越来越大。例如我们调用incr test命令100次,文件中必须保存全部的100条命令,其实有99条都是多余的。因为要恢复数据库的状态其实文件中保存一条set test 100就够了。

为了压缩aof的持久化文件。redis提供了bgrewriteaof命令。收到此命令redis将使用与快照类似的方式将内存中的数据 以命令的方式保存到临时文件中,最后替换原来的文件。具体过程如下

  • redis调用fork ,现在有父子两个进程
  • 子进程根据内存中的数据库快照,往临时文件中写入重建数据库状态的命令
  • 父进程继续处理client请求,除了把写命令写入到原来的aof文件中。同时把收到的写命令缓存起来。这样就能保证如果子进程重写失败的话并不会出问题。
  • 当子进程把快照内容写入已命令方式写到临时文件中后,子进程发信号通知父进程。然后父进程把缓存的写命令也写入到临时文件。
  • 现在父进程可以使用临时文件替换老的aof文件,并重命名,后面收到的写命令也开始往新的aof文件中追加。

需要注意到是重写aof文件的操作,并没有读取旧的aof文件,而是将整个内存中的数据库内容用命令的方式重写了一个新的aof文件,这点和快照有点类似。

7.2.3 AOF的优缺点

优势

  • 使用 AOF 持久化会让 Redis 变得非常耐久(much more durable):你可以设置不同的 fsync 策略,比如无 fsync ,每秒钟一次 fsync ,或者每次执行写入命令时 fsync 。 AOF 的默认策略为每秒钟 fsync 一次,在这种配置下,Redis 仍然可以保持良好的性能,并且就算发生故障停机,也最多只会丢失一秒钟的数据( fsync 会在后台线程执行,所以主线程可以继续努力地处理命令请求)。
  • AOF 文件是一个只进行追加操作的日志文件(append only log), 因此对 AOF 文件的写入不需要进行 seek , 即使日志因为某些原因而包含了未写入完整的命令(比如写入时磁盘已满,写入中途停机,等等), redis-check-aof 工具也可以轻易地修复这种问题。
    Redis 可以在 AOF 文件体积变得过大时,自动地在后台对 AOF 进行重写: 重写后的新 AOF 文件包含了恢复当前数据集所需的最小命令集合。 整个重写操作是绝对安全的,因为 Redis 在创建新 AOF 文件的过程中,会继续将命令追加到现有的 AOF 文件里面,即使重写过程中发生停机,现有的 AOF 文件也不会丢失。 而一旦新 AOF 文件创建完毕,Redis 就会从旧 AOF 文件切换到新 AOF 文件,并开始对新 AOF 文件进行追加操作。
  • AOF 文件有序地保存了对数据库执行的所有写入操作, 这些写入操作以 Redis 协议的格式保存, 因此 AOF 文件的内容非常容易被人读懂, 对文件进行分析(parse)也很轻松。 导出(export) AOF 文件也非常简单: 举个例子, 如果你不小心执行了 FLUSHALL 命令, 但只要 AOF 文件未被重写, 那么只要停止服务器, 移除 AOF 文件末尾的 FLUSHALL 命令, 并重启 Redis , 就可以将数据集恢复到 FLUSHALL 执行之前的状态。

劣势

  • 对于相同的数据集来说,AOF 文件的体积通常要大于 RDB 文件的体积。
  • 因为redis是单线程的,每次aof文件同步写入都要等(可以参考下面AOF持久化详解文章中的文件写入和同步模块)。根据所使用的 fsync 策略,AOF 的速度可能会慢于 RDB 。 在一般情况下, 每秒 fsync 的性能依然非常高, 而关闭 fsync 可以让 AOF 的速度和 RDB 一样快, 即使在高负荷之下也是如此。 不过在处理巨大的写入载入时,RDB 可以提供更有保证的最大延迟时间(latency)。
  • AOF 在过去曾经发生过这样的 bug : 因为个别命令的原因,导致 AOF 文件在重新载入时,无法将数据集恢复成保存时的原样。 (举个例子,阻塞命令 BRPOPLPUSH 就曾经引起过这样的 bug 。) 测试套件里为这种情况添加了测试: 它们会自动生成随机的、复杂的数据集, 并通过重新载入这些数据来确保一切正常。 虽然这种 bug 在 AOF 文件中并不常见, 但是对比来说, RDB 几乎是不可能出现这种 bug 的。

7.3 两者对比

两者的开启并不是冲突的,如果都开启,系统默认是优先加载aof的文件来进行恢复。那个两种持久化方式如何关闭,可以参考下面的方法。RDB和AOF的关闭方法

AOF持久化详解

参考

7.4扩展:

1、RDB 持久化方式能够在指定的时间间隔内对你的数据进行快照存储
2、AOF 持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF命令以Redis 协议追加保存每次写的操作到文件末尾,Redis还能对AOF文件进行后台重写,使得AOF文件的体积不至于过大。
3、只做缓存,如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何持久化
4、同时开启两种持久化方式在这种情况下,当redis重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整。RDB 的数据不实时,同时使用两者时服务器重启也只会找AOF文件,那要不要只使用AOF呢?作者建议不要,因为RDB更适合用于备份数据库(AOF在不断变化不好备份),快速重启,而且不会有AOF可能潜在的Bug,留着作为一个万一的手段。
5、性能建议
因为RDB文件只用作后备用途,建议只在Slave上持久化RDB文件,而且只要15分钟备份一次就够了,只保留 save 900 1 这条规则。如果Enable AOF ,好处是在最恶劣情况下也只会丢失不超过两秒数据,启动脚本较简单只load自己的AOF文件就可以了,代价一是带来了持续的IO,二是AOF rewrite 的最后将 rewrite 过程中产生的新数据写到新文件造成的阻塞几乎是不可避免的。只要硬盘许可,应该尽量减少AOF rewrite的频率,AOF重写的基础大小默认值64M太小了,可以设到5G以上,默认超过原大小100%大小重写可以改到适当的数值。如果不Enable AOF ,仅靠 Master-Slave Repllcation 实现高可用性也可以,能省掉一大笔IO,也减少了rewrite时带来的系统波动。代价是如果Master/Slave 同时倒掉,会丢失十几分钟的数据,启动脚本也要比较两个 Master/Slave 中的 RDB文件,载入较新的那个,微博就是这种架构。

8.Redis发布订阅(暂时简单了解)

发布订阅的命令

image-20200929214049296

8.1 测试

订阅端:

127.0.0.1:6379> SUBSCRIBE mychannel # 订阅一个频道 mychannel
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "mychannel"
3) (integer) 1
# 等待读取推送的信息
1) "message" # 消息
2) "mychannel" # 那个频道的消息
3) "hello,channel" # 消息的具体内容
1) "message"
2) "mychannel"
3) "hello,redis"

发送端:

127.0.0.1:6379> PUBLISH mychannel "hello,channel" # 发布者发布消息到频道!
(integer) 1
127.0.0.1:6379> PUBLISH mychannel "hello,redis" # 发布者发布消息到频道!
(integer) 1

8.2 原理

Redis是使用C实现的,通过分析 Redis 源码里的 pubsub.c 文件,了解发布和订阅机制的底层实现,籍此加深对 Redis 的理解。

Redis 通过 PUBLISH 、SUBSCRIBE 和 PSUBSCRIBE 等命令实现发布和订阅功能。
通过 SUBSCRIBE 命令订阅某频道后,redis-server 里维护了一个字典,字典的键就是一个个 频道!而字典的值则是一个链表,链表中保存了所有订阅这个 channel 的客户端。SUBSCRIBE 命令的关键,就是将客户端添加到给定 channel 的订阅链表中。

通过 PUBLISH 命令向订阅者发送消息,redis-server 会使用给定的频道作为键,在它所维护的 channel字典中查找记录了订阅这个频道的所有客户端的链表,遍历这个链表,将消息发布给所有订阅者。

Pub/Sub 从字面上理解就是发布(Publish)与订阅(Subscribe),在Redis中,你可以设定对某一个key值进行消息发布及消息订阅,当一个key值上进行了消息发布后,所有订阅它的客户端都会收到相应的消息。这一功能最明显的用法就是用作实时消息系统,比如普通的即时聊天,群聊等功能。

9.Redis主从复制

9.1 概念

主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master/leader),后者称为从节点(slave/follower);数据的复制是单向的,只能由主节点到从节点。
Master以写为主,Slave 以读为主。

可以使用命令 info replication查看redis的相关信息如下:

# Replication
role:master
connected_slaves:0
master_replid:a63afb2b95c60ac0a9d20b5cb76d1505a54bac58
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

默认情况下,每台独立Redis服务器都是主节点;
且一个主节点可以有多个从节点(或没有从节点),但一个从节点只能有一个主节点。

主从复制的作用主要包括:
1、数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
2、故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余。
3、负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis数据时应用连接主节点,读Redis数据时应用连接从节点),分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。
4、高可用(集群)基石:除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。

一般来说,要将Redis运用于工程项目中,只使用一台Redis是万万不能的(宕机),原因如下:
1、从结构上,单个Redis服务器会发生单点故障,并且一台服务器需要处理所有的请求负载,压力较大;
2、从容量上,单个Redis服务器内存容量有限,就算一台Redis服务器内存容量为256G,也不能将所有内存用作Redis存储内存,一般来说,单台Redis最大使用内存不应该超过20G。
电商网站上的商品,一般都是一次上传,无数次浏览的,说专业点也就是"多读少写"。

<img src="https://gitee.com/jsnucrh/blog-sharding_1/raw/master/img/20201219225402.png" alt="image-20200929224316529" style="zoom:80%;" />

9.2 环境配置

如果资源比较少,只有一台服务器的话,可以复制3个配置文件,然后修改对应的信息
1、端口
2、pid 名字
3、log文件名字
4、dump.rdb 名字
修改完毕之后,通过启动命令指定不同的配置文件实现主从复制。

daemonize yes
port 6379
pidfile /var/run/redis_6379.pid
logfile "redis_6379.log"
dbfilename 6379.rdb

9.3 一主二从

默认情况下,每台Redis服务器都是主节点; 我们一般情况下只用配置从机就好了!可以使用命令 SLAVEOF ip port来实现主从,但是这样是临时的。如果要永久生效就要在配置文件中配置,在REPLICATION下有个slaveof,配置主机 和 端口 就好,如果主有密码就把密码也配置上就好了。

从机只能读不能写

使用SLAVEOF命令进行主从设置的时候,如果中途从机断了,然后从机再写入数据,那么再次进行slaveof的时候,从机独有的数据会被清除,也就是从机的数据在进行主从的时候会同步和主机的数据完全一致,不多不少。

9.4 哨兵模式

主从切换技术的方法是:当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用。这不是一种推荐的方式,更多时候,我们优先考虑哨兵模式。Redis从2.8开始正式提供了Sentinel(哨兵) 架构来解决这个问题。
哨兵模式能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库。哨兵模式是一种特殊的模式,首先Redis提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例。

img

这里的哨兵有两个作用

  • 通过发送命令,让Redis服务器返回监控其运行状态,包括主服务器和从服务器。
  • 当哨兵监测到master宕机,会自动将slave切换成master,然后通过发布订阅模式通知其他的从服务器,修改配置文件,让它们切换主机。

然而一个哨兵进程对Redis服务器进行监控,可能会出现问题,为此,我们可以使用多个哨兵进行监控。各个哨兵之间还会进行监控,这样就形成了多哨兵模式。

img

假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行failover过程,仅仅是哨兵1主观的认为主服务器不可用,这个现象成为主观下线。当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,那么哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行failover[故障转移]操作。切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为客观下线

9.5 哨兵模式的设置

  • 配置哨兵配置文件 sentinel.conf

    # sentinel monitor 被监控的名称 host port 1
    # 哨兵模式的最后的一个 1 的意思是哨兵判断该节点多少次才算死亡,就是几个哨兵都得到他死了才算死(即几个哨兵认为他死了他才算死)
    sentinel monitor myredis 127.0.0.1 6379 1
  • 指定自己的配置文件启动

    ./bin/redis-sentinel myconfig/sentinel.conf

  • 观察日志即可

9.6 哨兵模式总结

优点:
1、哨兵集群,基于主从复制模式,所有的主从配置优点,它全有
2、 主从可以切换,故障可以转移,系统的可用性就会更好
3、哨兵模式就是主从模式的升级,手动到自动,更加健壮!
缺点:
1、Redis 不好啊在线扩容的,集群容量一旦到达上限,在线扩容就十分麻烦!
2、实现哨兵模式的配置其实是很麻烦的,里面有很多选择!

哨兵模式的全部配置!

# Example sentinel.conf
# 哨兵sentinel实例运行的端口 默认26379
port 26379
# 哨兵sentinel的工作目录
dir /tmp
# 哨兵sentinel监控的redis主节点的 ip port
# master-name 可以自己命名的主节点名字 只能由字母A-z、数字0-9 、这三个字符".-_"组成。
# 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无法正常启动成功。
#通知脚本
# shell编程
# 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 # 一般都是由运维来配置!

10.Redis缓存击穿、穿透和雪崩

这部分内容可以看我之前的博客


njitzyd
58 声望7 粉丝