Redis 01 概述

简介

NOSQL

NoSQL(Not Only SQL),即不仅是SQL,泛指非关系型数据库。
NoSQL 易扩展,NoSQL 数据库种类繁多(MongoDB、Redis 等),共同的特点都是去掉关系数据库的关系型特性。
数据之间无关系,这样就非常容易扩展,无形之间也在架构的层面上带来了可扩展的能力。
大数据量下 NoSQL 数据库具有非常高的读写性能,这得益于它的无关系性,数据库的结构简单。
NoSQL 数据库的典型代表就是 Redis

Redis

Redis(Remote Dictionary Server ),即远程字典服务
一个开源的使用 ANSI C 语言编写、支持网络、可基于内存亦可持久化的日志型、Key - Value 数据库,并提供多种语言的 API。
从 2010 年 3 月 15 日起,Redis 的开发工作由 VMware 主持。
从 2013 年 5 月开始,Redis的开发由 Pivotal 赞助。
官网:https://redis.io/

结构

文件介绍

  • redis-server.exe:服务端程序,提供 Redis 服务。
  • redis-cli.exe: 客户端程序,通过它连接 Redis 服务并进行操作。
  • redis-check-dump.exe:RDB 文件修复工具。
  • redis-check-aof.exe:AOF 文件修复工具。
  • redis-benchmark.exe:性能测试工具,用以模拟同时由 N 个客户端发送 M 个 SETs/GETs 查询(类似于 Apache 的 ab 工具)。
  • redis.windows.conf: 配置文件,将 Redis 作为普通软件使用的配置,命令行关闭则 Redis 关闭。
  • redis.windows-service.conf:配置文件,将 Redis 作为系统服务的配置。

使用

双击 redis-server.exe,会弹出窗口一闪而过,如果不想窗口消失,可以在 cmd 中打开。

redis官网使用_redis官网使用


这里启动的是 Redis 的服务端,用于提供服务。

双击 redis-cli.exe

redis官网使用_redis官网使用_02


这里启动的是 Redis 的客户端,用于连接服务。

测试连接

redis官网使用_redis官网使用_03


如此即说明 Redis 可以正常使用

以下均在linux系统下运行~~

这里列举下 redis.conf 中比较重要的配置项

配置项名称

配置项值范围

说明

daemonize

yes、no

yes表示启用守护进程,默认是no即不以守护进程方式运行。其中Windows系统下不支持启用守护进程方式运行

port

指定 Redis 监听端口,默认端口为 6379

bind

绑定的主机地址,如果需要设置远程访问则直接将这个属性备注下或者改为bind * 即可,这个属性和下面的protected-mode控制了是否可以远程访问 。

protected-mode

yes、no

保护模式,该模式控制外部网是否可以连接redis服务,默认是yes,所以默认我们外网是无法访问的,如需外网连接rendis服务则需要将此属性改为no。

timeout

300

当客户端闲置多长时间后关闭连接,如果指定为 0,表示关闭该功能

loglevel

debug、verbose、notice、warning

日志级别,默认为 notice

protected-mode

yes、no

设置数据库的数量,默认的数据库是0。整个通过客户端工具可以看得到

databases

16

指定存储至本地数据库时是否压缩数据,默认为 yes,Redis 采用 LZF 压缩,如果为了节省 CPU 时间,可以关闭该选项,但会导致数据库文件变的巨大。

dbfilename

dump.rdb

指定本地数据库文件名,默认值为 dump.rdb

dir

指定本地数据库存放目录

requirepass

设置 Redis 连接密码,如果配置了连接密码,客户端在连接 Redis 时需要通过 AUTH 命令提供密码,默认关闭

maxclients

0

设置同一时间最大客户端连接数,默认无限制,Redis 可以同时打开的客户端连接数为 Redis 进程可以打开的最大文件描述符数,如果设置 maxclients 0,表示不作限制。当客户端连接数到达限制时,Redis 会关闭新的连接并向客户端返回 max number of clients reached 错误信息。

maxmemory

XXX

指定 Redis 最大内存限制,Redis 在启动时会把数据加载到内存中,达到最大内存后,Redis 会先尝试清除已到期或即将到期的 Key,当此方法处理 后,仍然到达最大内存设置,将无法再进行写入操作,但仍然可以进行读取操作。Redis 新的 vm 机制,会把 Key 存放内存,Value 会存放在 swap 区。配置项值范围列里XXX为数值。

查看

查看进程方式

1.ps -ef|grep redis

[root@sail redis]# ps -ef|grep redis
root 11953 3410 0 16:15 pts/1 00:00:02 ./bin/redis-server *:6379

查看端口方式

Redis 的默认端口为 6379,一般不会更改,可以通过该端口查询 Redis 允许情况。

netstat -lanp | grep 6379

[root@sail redis]# netstat -lanp | grep 6379
tcp 0 0 0.0.0.0:6379 0.0.0.0:* LISTEN 11953/./bin/redis-s

性能测试

bin 目录下的 redis-benchmark 可以进行性能测试

  • -c(clients):客户端的并发量(默认 50)。
  • -n(num):客户端请求数量(默认 100000)。
  • -q:仅仅显示 redis-benchmark 的 requests per second 信息。
  • -r(random):向 Redis 插入更多随机的值。
  • -P :每个请求pipeline的数据量(默认为 1)。
  • -k :客户端是否使用 keepalive,1 为使用,0 为不使用,默认值为 1。
  • -t:对指定命令进行基准测试。
  • –csv:将结果按照 csv 格式输出。

redis-benchmark -h localhost -p 6379 -c 100 -n 100000

Redis 02 基础命令

Redis 默认有 16 个数据库
默认使用的是第 0 个数据库
不同数据库存不同的值

数据库

切换数据库

select

127.0.0.1:6379> select 1
OK
127.0.0.1:6379[1]>

[]中的数字为数据库编号(0号数据库为默认,不展示编号)

查看当前数据库大小

dbsize

127.0.0.1:6379[1]> dbszie
(integer) 1

清空当前库

flushdb

127.0.0.1:6379[1]> flushdb
OK

清空所有库

flushall

127.0.0.1:6379[1]> flushall
OK

查看当前数据库所有的键

keys *

127.0.0.1:6379[1]> keys *
1) "hello"

判断键是否存在

exists 键名称

127.0.0.1:6379[1]> exists hello
(integer) 1
127.0.0.1:6379[1]> exists hello1
(integer) 0

移动键到其他数据库

move 键名称 数据库编号

127.0.0.1:6379[1]> exists hello
(integer) 1
127.0.0.1:6379[1]> exists hello1
(integer) 0

设置键生存时间

expire 键名称

127.0.0.1:6379[1]> expire hello 10
(integer) 1

setex 键名称 生存时间 值

127.0.0.1:6379[1]> setex hello1 10 HelloWorld
OK
127.0.0.1:6379[1]> get hello1
"HelloWorld"
127.0.0.1:6379[1]> get hello1
"HelloWorld"
127.0.0.1:6379[1]> get hello1
"HelloWorld"
127.0.0.1:6379[1]> get hello1
"HelloWorld"
127.0.0.1:6379[1]> get hello1
"HelloWorld"
127.0.0.1:6379[1]> get hello1
(nil)

查看键生存时间

ttl 键名称

127.0.0.1:6379[1]> ttl hello
(integer) 3
127.0.0.1:6379[1]> ttl hello
(integer) 1
127.0.0.1:6379[1]> ttl hello
(integer) -2
127.0.0.1:6379[1]> get hello
(nil)

结果为 -2 则代表该键已经过期。

查看键类型

type 键名称

127.0.0.1:6379[1]> set hello World
OK
127.0.0.1:6379[1]> type hello
string

Redis 03 字符串

应用场景:计数器、统计多单元的数量、粉丝数、对象缓存存储。

赋值

普通赋值

set

127.0.0.1:6379> set hello Hello World
OK

不存在才赋值

setnx

127.0.0.1:6379> setnx hello Hello
(integer) 0
127.0.0.1:6379> get hello
"Hello World"

由于hello已经有值赋值没有成功

127.0.0.1:6379> setnx hello1 Hello
(integer) 1
127.0.0.1:6379> get hello1
"Hello"

不存在的key才能赋值

批量赋值

mset

127.0.0.1:6379> mset hello1 Hello1 hello2 Hello2
OK
127.0.0.1:6379> get hello1
"Hello1"
127.0.0.1:6379> get hello2
"Hello2"

原子性批量赋值

msetnx

127.0.0.1:6379> msetnx hello2 "Hello World2" hello3 "Hello World3"
(integer) 0
127.0.0.1:6379> get hello2
"Hello2"
127.0.0.1:6379> get hello3
(nil)

要么同时成功,要么同时失败。

设置对象

set 对象

127.0.0.1:6379> set user:1 {name:sail,age:27}
OK
127.0.0.1:6379> set user:2 {name:hello,age:28}
OK
127.0.0.1:6379> get user:1
"{name:sail,age:27}"
127.0.0.1:6379> get user:2
"{name:hello,age:28}"

设置一个 user:1 和 user:2 对象,值为 Json 字符串来保存一个对象。

追加

append

127.0.0.1:6379> append hello "Hello World"
(integer) 11
127.0.0.1:6379> get hello
"Hello World"

如果当前 key 不存在,就相当于 set key

取值

普通取值

get

127.0.0.1:6379> get hello
"Hello World"

先取值再赋值

getset

127.0.0.1:6379> getset hello4 "Hello4"
(nil)

由于 hello4 前面没有赋值,这里先取值为空。

获取长度

strlen

127.0.0.1:6379> strlen hello
(integer) 11

key

查看全部 key

keys *

127.0.0.1:6379> keys *
1) "hello"

删除 key

del

127.0.0.1:6379> keys *
1) "hello"
127.0.0.1:6379> del hello
(integer) 1
127.0.0.1:6379> keys *
(empty array)

key 是否存在

exists

127.0.0.1:6379> exists hello
(integer) 1
127.0.0.1:6379> exists hello1
(integer) 0

存在返回 1,不存在返回 0。

增减

自增 1

incr

127.0.0.1:6379> set num1 0
OK
127.0.0.1:6379> incr num1
(integer) 1
127.0.0.1:6379> incr num1
(integer) 2

执行一次会让对应 key 的值加 1。

自减 1

decr

127.0.0.1:6379> decr num1
(integer) 1
127.0.0.1:6379> decr num1
(integer) 0

执行一次会让对应 key 的值减 1。

自增步长

incrby

127.0.0.1:6379> incrby num1 10
(integer) 10
127.0.0.1:6379> incrby num1 10
(integer) 20

执行一次会按照设置的步长增加一次

自减步长

decrby

127.0.0.1:6379> decrby num1 10
(integer) 10
127.0.0.1:6379> decrby num1 10
(integer) 0

执行一次会按照设置的步长减少一次

截取

截取指定下标

getrange

127.0.0.1:6379> getrange hello 0 4
"Hello"

这里截取的规则是含头含尾

截取全部

getrange 0 -1

127.0.0.1:6379> getrange hello 0 -1
"Hello World"

这样与 get key 的效果一致,建议还是用 get key,更加简洁。

从指定位置替换

setrange

127.0.0.1:6379> setrange hello 6 sail
(integer) 11
127.0.0.1:6379> get hello
"Hello saild"

过期

设置过期

setex

127.0.0.1:6379> setex hello 60 "Hello World"
OK

最后一个参数是设置替换成的字符串

查看过期剩余时间

ttl

127.0.0.1:6379> ttl hello
(integer) 56
127.0.0.1:6379> ttl hello
(integer) 55
127.0.0.1:6379> ttl hello
(integer) -2
127.0.0.1:6379> get hello
(nil)

当结果为 -2 时代表已到期,其值为空。

Redis 04 列表

在 Redis 里面,可以把 List 当成栈、队列、阻塞队列使用。
list 实际是一个链表,左右都可以插入值。
如果 key 不存在,创建新的链表。
如果移除了所有元素,空链表也代表不存在。
在两边插入或者改动值,效率最高;操作中间元素,效率相对低一些。
应用场景:消息排队

赋值

Lpush

将一个值或多个值,插入列表的头部,即从左插入

127.0.0.1:6379> Lpush list one # 从左插入一个值
(integer) 1
127.0.0.1:6379> Lpush list two three # 从左插入多个值
(integer) 3
127.0.0.1:6379> Lrange list 0 -1 # -1 即表示查询所有元素
1) "three"
2) "two"
3) "one"
127.0.0.1:6379> Lrange list 0 1 # 查询指定下标范围元素
1) "three"
2) "two"

先进的排在后面,后进的排在前面

赋值

从左插入

Lpush

将一个值或多个值,插入列表的头部,即从左插入

127.0.0.1:6379> Lpush list one # 从左插入一个值
(integer) 1
127.0.0.1:6379> Lpush list two three # 从左插入多个值
(integer) 3
127.0.0.1:6379> Lrange list 0 -1 # -1 即表示查询所有元素
1) "three"
2) "two"
3) "one"
127.0.0.1:6379> Lrange list 0 1 # 查询指定下标范围元素
1) "three"
2) "two"

先进的排在后面,后进的排在前面

从右插入

Rpush

将一个值或者多个值,插入列表的尾部,即从右插入。

127.0.0.1:6379> Rpush list four # 从右插入一个值
(integer) 4
127.0.0.1:6379> Rpush list five six # 从右插入多个值
(integer) 6
127.0.0.1:6379> Lrange list 0 -1
1) "three"
2) "two"
3) "one"
4) "four"
5) "five"
6) "six"

先进的排在前面,后进的排在后面。

元素前后插入值

Linsert

127.0.0.1:6379> Lrange list 0 -1
1) "two"
2) "one"
127.0.0.1:6379> Linsert list before two three # two 之前插入 three
(integer) 3
127.0.0.1:6379> Lrange list 0 -1
1) "three"
2) "two"
3) "one"
127.0.0.1:6379> Linsert list after two three # two 之后插入 three
(integer) 4
127.0.0.1:6379> Lrange list 0 -1
1) "three"
2) "two"
3) "three"
4) "one"

指定下标赋值

Lset

127.0.0.1:6379> Lrange list 0 -1
1) "two"
127.0.0.1:6379> Lset list 0 one # 赋值列表指定下标元素
OK
127.0.0.1:6379> Lrange list 0 -1
1) "one"

如果列表不存在或者列表指定下标不存在,赋值失败。

取值

查看列表

Lrange

127.0.0.1:6379> Lrange list 0 -1 # -1 即表示查询所有元素
1) "three"
2) "two"
3) "one"
127.0.0.1:6379> Lrange list 0 1 # 查询指定下标范围元素
1) "three"
2) "two"

下标获取元素

Lindex

127.0.0.1:6379> Lrange list 0 -1
1) "two"
2) "one"
127.0.0.1:6379> Lindex list 0 # 下标从 0 开始
"two"
127.0.0.1:6379> Lindex list 1
"one"

Redis 显示的下标是从 1 开始的,实际的下标还是从 0 开始的。

列表长度

Llen

127.0.0.1:6379> Llen list
(integer) 2

列表是否存在

exists

127.0.0.1:6379> exists list
(integer) 1
127.0.0.1:6379> exists list3
(integer) 0

删除

从左移除

Lpop

127.0.0.1:6379> Lrange list 0 -1
1) "three"
2) "two"
3) "one"
4) "four"
127.0.0.1:6379> Lpop list # 移除最左边的元素
"three"
127.0.0.1:6379> Lrange list 0 -1
1) "two"
2) "one"
3) "four"

从右移除

Rpop

127.0.0.1:6379> Lrange list 0 -1
1) "two"
2) "one"
3) "four"
127.0.0.1:6379> Rpop list # 移除最右边的元素
"four"
127.0.0.1:6379> Lrange list 0 -1
1) "two"
2) "one"

移除元素

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 # 移除一个指定元素
(integer) 1
127.0.0.1:6379> Lrange list 0 -1
1) "three"
2) "three"
3) "two"
127.0.0.1:6379> Lrem list 2 three # 移除两个指定元素
(integer) 2
127.0.0.1:6379> Lrange list 0 -1
1) "two"

截取

截取下标范围的元素

Ltrim

127.0.0.1:6379> Lrange list 0 -1
1) "one"
2) "two"
3) "three"
4) "four"
127.0.0.1:6379> Ltrim list 1 2 # 截取下标 1 到 2 的元素
OK
127.0.0.1:6379> Lrange list 0 -1
1) "two"
2) "three"

移动

移除列表最后一个元素并移动到新列表中

Rpoplpush

127.0.0.1:6379> Lrange list 0 -1
1) "two"
2) "three"
127.0.0.1:6379> Rpoplpush list list2 # 移除列表最后一个元素并移动到新列表中
"three"
127.0.0.1:6379> Lrange list 0 -1 # 原来的列表
1) "two"
127.0.0.1:6379> Lrange list2 0 -1 # 新的列表
1) "three"

Redis 05 集合

Set 中的值是不能重复的
应用场景:共同关注

赋值

插入值

Sadd

127.0.0.1:6379> Sadd set hello
(integer) 1
127.0.0.1:6379> Sadd set world
(integer) 1
127.0.0.1:6379> Sadd set world # 插入了重复值,没有生效
(integer) 0
127.0.0.1:6379> Smembers set
1) "world"
2) "hello"

取值

所有元素

Smembers

127.0.0.1:6379> Smembers set
1) "world"
2) "hello"

元素是否存在

Sismember

127.0.0.1:6379> Smembers set
1) "world"
2) "hello"
127.0.0.1:6379> Sismember set hello # 存在返回 1
(integer) 1
127.0.0.1:6379> Sismember set hello1 # 不存在返回 0
(integer) 0

元素个数

Scard

127.0.0.1:6379> Scard set
(integer) 2

随机元素

Srandmember

127.0.0.1:6379> Smembers set
1) "world"
2) "hello"
127.0.0.1:6379> Srandmember set
"world"
127.0.0.1:6379> Srandmember set
"hello"

两个集合的差集

Sdiff

127.0.0.1:6379> Smembers set1
1) "b"
2) "a"
127.0.0.1:6379> Smembers set2
1) "b"
2) "c"
127.0.0.1:6379> Sdiff set1 set2 # 取 set1 对于 set2 的差集
1) "a"
127.0.0.1:6379> Sdiff set2 set1 # 取 set2 对于 set1 的差集
1) "c"

两个集合的交集

Sinter

127.0.0.1:6379> Smembers set1
1) "b"
2) "a"
127.0.0.1:6379> Smembers set2
1) "b"
2) "c"
127.0.0.1:6379> Sinter set1 set2 # 取 set1 和 set2 的交集
1) "b"

可以用来获取共同关注

两个集合的并集

Sunion

127.0.0.1:6379> Smembers set1
1) "b"
2) "a"
127.0.0.1:6379> Smembers set2
1) "b"
2) "c"
127.0.0.1:6379> Sunion set1 set2
1) "b"
2) "c"
3) "a"

删除

指定元素

Srem

127.0.0.1:6379> Srem set world
(integer) 1
127.0.0.1:6379> Smembers set
1) "hello"

随机元素

Spop

127.0.0.1:6379> Smembers set
1) "world"
2) "hello"
127.0.0.1:6379> Spop set
"world"

移动

指定元素到其他集合

Smove

127.0.0.1:6379> Smembers set
1) "hello"
127.0.0.1:6379> Smove set set1 hello # 移动 set 中的 hello 到 set1 中(set1 是存在的)
(integer) 1
127.0.0.1:6379> Smembers set1
1) "hello"
2) "world"
127.0.0.1:6379> Smembers set
(empty array)
127.0.0.1:6379> Smove set1 set2 hello # 移动 set1 中的 hello 到 set2 中(set2 不存在则创建)
(integer) 1
127.0.0.1:6379> Smembers set2
1) "hello"

Redis 06 哈希

哈希就是 key - map 的数据结构
应用场景:对象存储

赋值

单个哈希

Hset

127.0.0.1:6379> Hset hash f1 sail
(integer) 1

多个哈希

Hmset

127.0.0.1:6379> Hmset hash f2 sail2 f3 sail3
OK

不存在才赋值

Hsetnx

127.0.0.1:6379> Hkeys hash
1) "f1"
2) "f2"
3) "f3"
127.0.0.1:6379> Hsetnx hash f4 1 # f4 不存在,赋值成功
(integer) 1
127.0.0.1:6379> Hget hash f4
"1"
127.0.0.1:6379> Hsetnx hash f4 2 # f4 存在,赋值失败
(integer) 0
127.0.0.1:6379> Hget hash f4
"1"

自增

Hincrby

127.0.0.1:6379> Hset hash f3 1
(integer) 1
127.0.0.1:6379> Hincrby hash f3 1 # 自增 1
(integer) 2
127.0.0.1:6379> Hincrby hash f3 -1 # 自减 1(哈希没有自减命令,用自增负数实现自减)
(integer) 1

取值

单个哈希

Hget

127.0.0.1:6379> Hget hash f1
"sail"

多个哈希

Hmget

127.0.0.1:6379> Hmget hash f2 f3
1) "sail2"
2) "sail3"

所有值

Hgetall

127.0.0.1:6379> Hgetall hash
1) "f1"
2) "sail"
3) "f2"
4) "sail2"
5) "f3"
6) "sail3"

所有 key

Hkeys

127.0.0.1:6379> Hkeys hash
1) "f1"
2) "f2"

所有 value

Hvals

127.0.0.1:6379> Hvals hash
1) "sail"
2) "sail2"

长度

Hlen

127.0.0.1:6379> Hgetall hash
1) "f1"
2) "sail"
3) "f2"
4) "sail2"
127.0.0.1:6379> Hlen hash
(integer) 2

字段是否存在

Hexists

127.0.0.1:6379> Hgetall hash
1) "f1"
2) "sail"
3) "f2"
4) "sail2"
127.0.0.1:6379> Hexists hash f1 # 存在返回 1
(integer) 1
127.0.0.1:6379> Hexists hash f3 # 不存在返回 0
(integer) 0

删除

指定字段

Hdel

127.0.0.1:6379> Hdel hash f3
(integer) 1
127.0.0.1:6379> Hgetall hash
1) "f1"
2) "sail"
3) "f2"
4) "sail2"

Redis 07 有序集合

Zset 就是 Set 的有序集合
应用场景:排行榜

赋值

一个或多个元素

Zadd

127.0.0.1:6379> Zadd zset 1 one
(integer) 1
127.0.0.1:6379> Zadd zset 2 two 3 three
(integer) 2

取值

正序查询指定下标范围

Zrange

127.0.0.1:6379> Zrange zset 0 1
1) "one"
2) "two"
127.0.0.1:6379> Zrange zset 0 -1 # 0 -1 即为查询所有的元素
1) "one"
2) "two"
3) "three"

倒序查询指定下标范围

Zrevrange

127.0.0.1:6379> Zrevrange zset 0 -1
1) "three"
2) "two"
3) "one"

元素个数

Zcard

127.0.0.1:6379> Zrange zset 0 -1
1) "one"
2) "two"
127.0.0.1:6379> Zcard zset
(integer) 2

指定区间元素数量

Zcount

127.0.0.1:6379> Zrange zset 0 -1
1) "one"
2) "two"
3) "three"
127.0.0.1:6379> Zcount zset 1 3 # 这里下标从 1 开始
(integer) 3
127.0.0.1:6379> Zcount zset 1 2
(integer) 2

排序

正序

Zrangebyscore

127.0.0.1:6379> Zrangebyscore zset -inf +inf # 负无穷到正无穷正序排列
1) "one"
2) "two"
3) "three"
127.0.0.1:6379> Zrangebyscore zset -inf 2 # 负无穷到 2 排序
1) "one"
2) "two"
127.0.0.1:6379> Zrangebyscore zset -inf 2 limit 0 1 # 排序结果只取 0 到 1 条
1) "one"
127.0.0.1:6379> Zrangebyscore zset -inf +inf withscores # 排序结果带上分数
1) "one"
2) "1"
3) "two"
4) "2"
5) "three"
6) "3"

倒序

Zrevrangebyscore

127.0.0.1:6379> Zrevrangebyscore zset +inf -inf # 正无穷到负无穷倒序排列
1) "three"
2) "two"
3) "one"

删除

指定元素

Zrem

127.0.0.1:6379> Zrange zset 0 -1
1) "one"
2) "two"
3) "three"
127.0.0.1:6379> Zrem zset three # 移除指定元素
(integer) 1
127.0.0.1:6379> Zrange zset 0 -1
1) "one"
2) "two"

Redis 08 地理位置

Redis 的 GEO 特性在 3.2 版本中推出, 这个功能可以将用户给定的地理位置信息储存起来。
通常用以实现诸如附近位置、摇一摇这类依赖于地理位置信息的功能。
geo 的数据类型为 zset。
GEO 的数据结构总共有六个常用命令:geoaddgeoposgeodistgeoradiusgeoradiusbymembergethash

官方文档:https://www.redis.net.cn/order/3685.html

赋值

添加

geoadd

将给定的空间元素(纬度、经度、名字)添加到指定的键里面。

geoadd key longitude latitude member ...
  • 这些数据会以有序集的形式被储存在键里面,从而使得 georadiusgeoradiusbymember 这样的命令可以在之后通过位置查询取得这些元素。
  • geoadd 命令以标准的x,y格式接受参数,所以用户必须先输入经度,然后再输入纬度。
  • geoadd 能够记录的坐标是有限的:非常接近两极的区域无法被索引。
  • 有效的经度介于 -180 ~ 180 度之间,有效的纬度介于 -85.05112878 ~ 85.05112878 度之间。当输入超出范围的经度或者纬度,geoadd 将返回一个错误。
127.0.0.1:6379> geoadd china:city 116.23 40.22 北京
(integer) 1
127.0.0.1:6379> geoadd china:city 121.48 31.40 上海 113.88 22.55 深圳 120.21
(error) ERR syntax error
127.0.0.1:6379> geoadd china:city 121.48 31.40 上海 113.88 22.55 深圳 120.21 30.20 杭州
(integer) 3
127.0.0.1:6379> geoadd china:city 106.54 29.40 重庆 108.93 34.23 西安 114.02 30.58 武汉
(integer) 3

取值

经纬度

geopos

从 key 里返回所有给定位置元素的位置(经度和纬度)

geopos key member [member...]
127.0.0.1:6379> geopos china:city 北京
1) 1) "116.23000055551528931"
   2) "40.2200010338739844"
127.0.0.1:6379> geopos china:city 重庆 上海
1) 1) "106.54000014066696167"
   2) "29.39999880018641676"
2) 1) "121.48000091314315796"
   2) "31.40000025319353938"
127.0.0.1:6379> geopos china:city 云南
1) (nil)

两个位置的距离

geodist

返回两个给定位置之间的距离,如果两个位置之间的其中一个不存在,那么命令返回空值。

geodist key member1 member2 [unit]

指定单位的参数 unit 必须是以下单位的其中一个:

  • m 表示单位为米
  • km 表示单位为千米
  • mi 表示单位为英里
  • ft 表示单位为英尺
    如果用户没有显式地指定单位参数,那么 geodist 默认使用米作为单位。

geodist 在计算距离时会假设地球为完美的球形,在极限情况下,这一假设最大会造成 0.5% 的误差。

127.0.0.1:6379> geodist china:city 北京 上海
"1088785.4302"
127.0.0.1:6379> geodist china:city 北京 上海 km
"1088.7854"
127.0.0.1:6379> geodist china:city 重庆 北京 km
"1491.6716"

半径内的元素

georadius

以给定的经纬度为中心,找出某一半径内的元素。

georadius key longitude latitude radius m|km|ft|mi [withcoord][withdist][withhash][asc|desc][count count]

在 china:city 中寻找坐标 100 30 半径为 1000km 的城市

127.0.0.1:6379> georadius china:city 100 30 1000 km
1) "\xe9\x87\x8d\xe5\xba\x86"
2) "\xe8\xa5\xbf\xe5\xae\x89"

此时中文输出为乱码,需要在连接 redis-cli 时增加参数 --raw ,可以强制输出中文,不然会乱码。

[root@sail ~]# redis-cli --raw
127.0.0.1:6379> georadius china:city 100 30 1000 km
重庆
西安

withdist 返回位置名称和中心距离

127.0.0.1:6379> georadius china:city 100 30 1000 km withdist
重庆
635.2850
西安
963.3171

withcoord 返回位置名称和经纬度

127.0.0.1:6379> georadius china:city 100 30 1000 km withcoord
重庆
106.54000014066696167
29.39999880018641676
西安
108.92999857664108276
34.23000121926852302

withdist withcoord 返回位置名称 距离 和 经纬度
count 限定寻找个数

127.0.0.1:6379> georadius china:city 100 30 1000 km withcoord withdist count 1
重庆
635.2850
106.54000014066696167
29.39999880018641676
127.0.0.1:6379> georadius china:city 100 30 1000 km withcoord withdist count 2
重庆
635.2850
106.54000014066696167
29.39999880018641676
西安
963.3171
108.92999857664108276
34.23000121926852302

指定范围内的元素

georadiusbymember

找出位于指定范围内的元素,中心点是由给定的位置元素决定

georadiusbymember key member radius m|km|ft|mi [withcoord][withdist][withhash][asc|desc][count count]

北京 1000 km 内的城市

127.0.0.1:6379> georadiusbymember china:city 北京 1000 km
北京
西安

上海 400 km 内的城市

127.0.0.1:6379> georadiusbymember china:city 上海 400 km
杭州
上海

经纬度字符串

geohash

将二维经纬度转换为一维字符串,字符串越长表示位置更精确,两个字符串越相似表示距离越近。

geohash key member [member...]
127.0.0.1:6379> geohash china:city 北京 重庆
wx4sucu47r0
wm5z22h53v0
127.0.0.1:6379> geohash china:city 北京 上海
wx4sucu47r0
wtw6sk5n300

删除

指定元素

zrem

GEO 没有提供删除成员的命令,但是因为 GEO 的底层实现是 zset,所以可以借用 zrem 命令实现对地理位置信息的删除。

# 查看全部的元素
127.0.0.1:6379> zrange china:city 0 -1
chongqing
重庆
西安
深圳
武汉
杭州
上海
北京
# 移除元素
127.0.0.1:6379> zrem china:city 上海
1
# 查看全部的元素
127.0.0.1:6379> zrange china:city 0 -1
重庆
西安
深圳
武汉
杭州
北京

Redis 09 基数

概述

Redis 在 2.8.9 版本添加了 HyperLogLog 结构,用来做基数统计的算法

其优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的,并且是很小的。

每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2 ^ 64 个不同元素的基数。

HyperLogLog 是一种算法,它提供了不精确的去重计数方案。

基数

比如数据集 {1, 3, 5, 7, 5, 7, 8}, 那么这个数据集的基数集为 {1, 3, 5 ,7, 8},基数(不重复元素)为 5。

基数估计就是在误差可接受的范围内,快速计算基数。

示例

比如统计网页的浏览用户数量,一天内同一个用户多次访问只算一次。

传统的解决方案是使用 Set 来保存用户 id,然后统计 Set 中的元素数量。

这种方案只能承载少量用户,一旦用户数量大起来就需要消耗大量的空间。

而且目的是统计用户数量而不是保存用户,这是个吃力不讨好的方案。

使用 HyperLogLog 最多需要 12k 就可以统计大量的用户数。

尽管它大概有 0.81% 的错误率,但对于统计用户数量这种不需要很精确的数据是可以忽略不计的。

赋值

添加元素

pfadd

pfadd key element [element ...]
127.0.0.1:6379> pfadd mykey a b c d e f g h i j
(integer) 1
127.0.0.1:6379> pfadd mykey2 a b b c
(integer) 1

取值

获取基值

pfcount

pfcount key [key ...]
127.0.0.1:6379> pfcount mykey
(integer) 10
127.0.0.1:6379> pfcount mykey2
(integer) 3

mykey2 因为 b 重复,所以基值为 3。

合并

并集合并

pfmerge

pfmerge destkey sourcekey [sourcekey ...]
127.0.0.1:6379> pfmerge mykey3 mykey mykey2
OK
127.0.0.1:6379> pfcount mykey3
(integer) 10

因为 mykey2 的元素与 mykey 中重复,所以取并集后基值依然为 10。

Redis 10 位图

概述

Redis 从 2.2 版本增加了 Bitmap(位图)

当需要统计用户一年的某些信息,如活跃或不活跃,登录或不登录,打卡或没打卡。

如果使用普通的 key / value存储,则要记录 365 条记录,如果用户量很大,需要的空间也会很大。

Redis 提供了 Bitmap 位图这种数据结构,Bitmap 就是通过操作二进制位来进行记录,即为 0 和 1。

如果要记录 365 天的打卡情况,使用 Bitmap 表示的形式大概如下:0101000111000111……

这样 365 天相当于 365 bit,又 1 字节 = 8 bit , 所以相当于使用 46 个字节即可。

BitMap 就是通过一个 bit 位来表示某个元素对应的值或者状态,其中的 key 就是对应元素本身。

实际上底层也是通过对字符串的操作来实现的。

赋值

单项赋值

setbit

setbit key offset value

设置 key 的第 offset 位为 value (1 或 0)

使用 bitmap 来记录一周的打卡记录(1 为打卡,0 为没打卡)

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

取值

单项取值

getbit

getbit key offset

获取 offset 设置的值,未设置过默认返回 0。

# 查看周四是否打卡
127.0.0.1:6379> getbit sign 3
(integer) 1
# 查看周六是否打卡
127.0.0.1:6379> getbit sign 5
(integer) 0

统计

bitcount

bitcount key [start, end]

统计 key 上位为 1 的个数

统计这周打卡的记录

127.0.0.1:6379> bitcount sign
(integer) 3

只有 3 个值为 1,即只有 3 天是打卡的状态。

Redis 11 配置

基本配置

Redis 的配置文件位于 Redis 安装目录下,文件名为 redis.conf

在 Linux 中,可以使用 whereis redis 查找 Redis 的安装目录

[root@sail]# whereis redis
redis: /usr/local/redis
[root@sail]# cd /usr/local/redis
[root@sail redis]# ls
00-RELEASENOTES  BUGS     CONTRIBUTING  deps      INSTALL   MANIFESTO  redis.conf  runtest-cluster    runtest-sentinel  src    TLS.md
bin              CONDUCT  COPYING       dump.rdb  Makefile  README.md  runtest     runtest-moduleapi  sentinel.conf     tests  utils

命令查看所有配置

127.0.0.1:6379> config get *
  1) "rdbchecksum"
  2) "yes"
  3) "daemonize"
  4) "no"
  5) "io-threads-do-reads"
  6) "no"
  7) "lua-replicate-commands"
  8) "yes"
  9) "always-show-logo"
 10) "no"
 11) "protected-mode"
 12) "yes"
 13) "rdbcompression"
 14) "yes"
 15) "rdb-del-sync-files"
 16) "no"
 17) "activerehashing"
 18) "yes"
 19) "stop-writes-on-bgsave-error"
 20) "yes"
 21) "set-proc-title"
 22) "yes"
 23) "dynamic-hz"
 24) "yes"
 25) "lazyfree-lazy-eviction"
 26) "no"
 27) "lazyfree-lazy-expire"
 28) "no"
 29) "lazyfree-lazy-server-del"
 30) "no"
 31) "lazyfree-lazy-user-del"
 32) "no"
 33) "lazyfree-lazy-user-flush"
 34) "no"
 35) "repl-disable-tcp-nodelay"
 36) "no"
 37) "repl-diskless-sync"
 38) "no"
 39) "gopher-enabled"
 40) "no"
 41) "aof-rewrite-incremental-fsync"
 42) "yes"
 43) "no-appendfsync-on-rewrite"
 44) "no"
 45) "cluster-require-full-coverage"
 46) "yes"
 47) "rdb-save-incremental-fsync"
 48) "yes"
 49) "aof-load-truncated"
 50) "yes"
 51) "aof-use-rdb-preamble"
 52) "yes"
 53) "cluster-replica-no-failover"
 54) "no"
 55) "cluster-slave-no-failover"
 56) "no"
 57) "replica-lazy-flush"
 58) "no"
 59) "slave-lazy-flush"
 60) "no"
 61) "replica-serve-stale-data"
 62) "yes"
 63) "slave-serve-stale-data"
 64) "yes"
 65) "replica-read-only"
 66) "yes"
 67) "slave-read-only"
 68) "yes"
 69) "replica-ignore-maxmemory"
 70) "yes"
 71) "slave-ignore-maxmemory"
 72) "yes"
 73) "jemalloc-bg-thread"
 74) "yes"
 75) "activedefrag"
 76) "no"
 77) "syslog-enabled"
 78) "no"
 79) "cluster-enabled"
 80) "no"
 81) "appendonly"
 82) "no"
 83) "cluster-allow-reads-when-down"
 84) "no"
 85) "crash-log-enabled"
 86) "yes"
 87) "crash-memcheck-enabled"
 88) "yes"
 89) "use-exit-on-panic"
 90) "no"
 91) "disable-thp"
 92) "yes"
 93) "cluster-allow-replica-migration"
 94) "yes"
 95) "replica-announced"
 96) "yes"
 97) "aclfile"
 98) ""
 99) "unixsocket"
100) ""
101) "pidfile"
102) ""
103) "replica-announce-ip"
104) ""
105) "slave-announce-ip"
106) ""
107) "masteruser"
108) ""
109) "cluster-announce-ip"
110) ""
111) "syslog-ident"
112) "redis"
113) "dbfilename"
114) "dump.rdb"
115) "appendfilename"
116) "appendonly.aof"
117) "server_cpulist"
118) ""
119) "bio_cpulist"
120) ""
121) "aof_rewrite_cpulist"
122) ""
123) "bgsave_cpulist"
124) ""
125) "ignore-warnings"
126) ""
127) "proc-title-template"
128) "{title} {listen-addr} {server-mode}"
129) "masterauth"
130) ""
131) "requirepass"
132) ""
133) "supervised"
134) "no"
135) "syslog-facility"
136) "local0"
137) "repl-diskless-load"
138) "disabled"
139) "loglevel"
140) "notice"
141) "maxmemory-policy"
142) "noeviction"
143) "appendfsync"
144) "everysec"
145) "oom-score-adj"
146) "no"
147) "acl-pubsub-default"
148) "allchannels"
149) "sanitize-dump-payload"
150) "no"
151) "databases"
152) "16"
153) "port"
154) "6379"
155) "io-threads"
156) "1"
157) "auto-aof-rewrite-percentage"
158) "100"
159) "cluster-replica-validity-factor"
160) "10"
161) "cluster-slave-validity-factor"
162) "10"
163) "list-max-ziplist-size"
164) "-2"
165) "tcp-keepalive"
166) "300"
167) "cluster-migration-barrier"
168) "1"
169) "active-defrag-cycle-min"
170) "1"
171) "active-defrag-cycle-max"
172) "25"
173) "active-defrag-threshold-lower"
174) "10"
175) "active-defrag-threshold-upper"
176) "100"
177) "lfu-log-factor"
178) "10"
179) "lfu-decay-time"
180) "1"
181) "replica-priority"
182) "100"
183) "slave-priority"
184) "100"
185) "repl-diskless-sync-delay"
186) "5"
187) "maxmemory-samples"
188) "5"
189) "maxmemory-eviction-tenacity"
190) "10"
191) "timeout"
192) "0"
193) "replica-announce-port"
194) "0"
195) "slave-announce-port"
196) "0"
197) "tcp-backlog"
198) "511"
199) "cluster-announce-bus-port"
200) "0"
201) "cluster-announce-port"
202) "0"
203) "cluster-announce-tls-port"
204) "0"
205) "repl-timeout"
206) "60"
207) "repl-ping-replica-period"
208) "10"
209) "repl-ping-slave-period"
210) "10"
211) "list-compress-depth"
212) "0"
213) "rdb-key-save-delay"
214) "0"
215) "key-load-delay"
216) "0"
217) "active-expire-effort"
218) "1"
219) "hz"
220) "10"
221) "min-replicas-to-write"
222) "0"
223) "min-slaves-to-write"
224) "0"
225) "min-replicas-max-lag"
226) "10"
227) "min-slaves-max-lag"
228) "10"
229) "maxclients"
230) "10000"
231) "active-defrag-max-scan-fields"
232) "1000"
233) "slowlog-max-len"
234) "128"
235) "acllog-max-len"
236) "128"
237) "lua-time-limit"
238) "5000"
239) "cluster-node-timeout"
240) "15000"
241) "slowlog-log-slower-than"
242) "10000"
243) "latency-monitor-threshold"
244) "0"
245) "proto-max-bulk-len"
246) "536870912"
247) "stream-node-max-entries"
248) "100"
249) "repl-backlog-size"
250) "1048576"
251) "maxmemory"
252) "0"
253) "hash-max-ziplist-entries"
254) "512"
255) "set-max-intset-entries"
256) "512"
257) "zset-max-ziplist-entries"
258) "128"
259) "active-defrag-ignore-bytes"
260) "104857600"
261) "hash-max-ziplist-value"
262) "64"
263) "stream-node-max-bytes"
264) "4096"
265) "zset-max-ziplist-value"
266) "64"
267) "hll-sparse-max-bytes"
268) "3000"
269) "tracking-table-max-keys"
270) "1000000"
271) "client-query-buffer-limit"
272) "1073741824"
273) "repl-backlog-ttl"
274) "3600"
275) "auto-aof-rewrite-min-size"
276) "67108864"
277) "logfile"
278) ""
279) "watchdog-period"
280) "0"
281) "dir"
282) "/usr/local/redis"
283) "save"
284) "3600 1 300 100 60 10000"
285) "client-output-buffer-limit"
286) "normal 0 0 0 slave 268435456 67108864 60 pubsub 33554432 8388608 60"
287) "unixsocketperm"
288) "0"
289) "slaveof"
290) ""
291) "notify-keyspace-events"
292) ""
293) "bind"
294) ""
295) "oom-score-adj-values"
296) "0 200 800"

单位

Units

# Redis configuration file example.
#
# Note that in order to read the configuration file, Redis must be
# started with the file path as first argument:
#
# ./redis-server /path/to/redis.conf
# 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
#
# units are case insensitive so 1GB 1Gb 1gB are all the same.

开头定义了一些基本的度量单位,只支持 bytes,不支持 bit

这些单位对大小写是不敏感的。

包含

INCLUDES

################################## INCLUDES ###################################
# Include one or more other config files here.  This is useful if you
# have a standard template that goes to all Redis servers but also need
# to customize a few per-server settings.  Include files can include
# other files, so use this wisely.
#
# Note that option "include" won't be rewritten by command "CONFIG REWRITE"
# from admin or Redis Sentinel. Since Redis always uses the last processed
# line as value of a configuration directive, you'd better put includes
# at the beginning of this file to avoid overwriting config change at runtime.
#
# If instead you are interested in using includes to override configuration
# options, it is better to use include as the last line.
#
# include /path/to/local.conf
# include /path/to/other.conf

和 Spring 配置文件类似,可以通过 include 包含,redis.conf 可以作为总文件,可以包含其他文件。

网络配置

NETWORK

# 绑定的 IP
bind 127.0.0.1 
# 保护模式
protected-mode yes 
# 默认端口
port 6379

通用

GENERAL

# 默认情况下,Redis 不作为守护进程运行。需要开启的话,改为 yes
daemonize yes 
# 可通过 upstart 和 systemd 管理 Redis 守护进程
supervised no 
# 以后台进程方式运行 redis,则需要指定 pid 文件
pidfile /var/run/redis_6379.pid 
# 日志级别。可选项有:
# debug(记录大量日志信息,适用于开发、测试阶段);
# verbose(较多日志信息);
# notice(适量日志信息,使用于生产环境);
# warning(仅有部分重要、关键信息才会被记录)。
loglevel notice 
# 日志文件的位置,当指定为空字符串时,为标准输出
logfile "" 
# 设置数据库的数目。默认的数据库是 0
databases 16 
# 是否总是显示 logo
always-show-logo yes

快照

SNAPSHOPTING

# 900秒(15分钟)内至少 1 个 key 值改变(则进行数据库保存--持久化)
save 900 1
# 300秒(5分钟)内至少 10 个 key 值改变(则进行数据库保存--持久化)
save 300 10
# 60秒(1分钟)内至少 10000 个 key 值改变(则进行数据库保存--持久化)
save 60 10000
# 持久化出现错误后,是否依然进行继续进行工作
stop-writes-on-bgsave-error yes 
# 使用压缩 rdb 文件 yes:压缩,但是需要一些 cpu 的消耗;no:不压缩,需要更多的磁盘空间
rdbcompression yes 
# 是否校验 rdb 文件,更有利于文件的容错性,但是在保存 rdb 文件的时候,会有大概 10% 的性能损耗
rdbchecksum yes 
# dbfilenamerdb 文件名称
dbfilename dump.rdb 
# dir 数据目录,数据库的写入会在这个目录。rdb、aof 文件也会写在这个目录
dir ./

安全

SECURITY

访问密码的查看,设置和取消。

# 获取密码
127.0.0.1:6379> config get requirepass
1) "requirepass"
2) ""
# 设置密码
127.0.0.1:6379> config set requirepass "123456"
OK
# 重新连接
[root@sail redis]# redis-cli
# 发现使用不了了
127.0.0.1:6379> ping
(error) NOAUTH Authentication required.
# 验证
127.0.0.1:6379> auth 123456
OK
# 验证后可以正常使用了
127.0.0.1:6379> ping
PONG

限制

MEMORY

# 获取密码
# 设置能连上 redis 的最大客户端连接数量
maxclients 10000 
# redis 配置的最大内存容量
maxmemory <bytes> 
# maxmemory-policy 内存达到上限的处理策略:
#     volatile-lru:利用 LRU 算法移除设置过过期时间的 key。
#     volatile-random:随机移除设置过过期时间的 key。
#     volatile-ttl:移除即将过期的 key,根据最近过期时间来删除(辅以 TTL)
#     allkeys-lru:利用 LRU 算法移除任何 key。
#     allkeys-random:随机移除任何 key。
#     noeviction:不移除任何 key,只是返回一个写错误。
maxmemory-policy noeviction

仅追加模式

APPEND ONLY MODE

# 是否以 append only 模式作为持久化方式,默认使用的是 rdb 方式持久化,这种方式在许多应用中已经足够用了
appendonly no 
# appendfilename AOF 文件名称
appendfilename "appendonly.aof" 
# appendfsync aof 持久化策略的配置:
#     no:不执行 fsync,由操作系统保证数据同步到磁盘,速度最快。
#     always:每次写入都执行 fsync,以保证数据同步到磁盘。
#     everysec:每秒执行一次 fsync,可能会导致丢失这1s数据。
appendfsync everysec

配置介绍

daemonize no

Redis 默认不是以守护进程的方式运行,可以通过该配置项修改,使用 yes 启用守护进程。

pidfile /var/run/redis.pid

当 Redis 以守护进程方式运行时,Redis 默认会把 pid 写入 /var/run/redis.pid 文件,可以通过 pidfile 指定。

port 6379

指定 Redis 监听端口,默认端口为 6379。

作者在自己的一篇博文中解释了为什么选用 6379 作为默认端口,因为 6379 在手机按键上 MERZ 对应的号码,而 MERZ 取自意大利歌女 Alessia Merz 的名字。

bind 127.0.0.1

绑定的主机地址。

timeout 300

当客户端闲置多长时间后关闭连接,如果指定为 0,表示关闭该功能。

loglevel verbose

指定日志记录级别,Redis总共支持四个级别:debug、verbose、notice、warning,默认为 verbose。

  • debug:记录大量日志信息,适用于开发、测试阶段
  • verbose:较多日志信息
  • notice:适量日志信息,使用于生产环境
  • warning:仅有部分重要、关键信息才会被记录

logfile stdout

设置数据库的数量,默认数据库为 0,可以使用 SELECT 命令在连接上指定数据库 id。

save 900 1
save 300 10
save 60 10000

指定在多长时间内,有多少次更新操作,就将数据同步到数据文件,可以多个条件配合。

Redis 默认配置文件中提供了三个条件:

  • 900 秒(15 分钟)内有 1 个更改
  • 300 秒(5 分钟)内有 10 个更改
  • 60 秒(1 分钟)内有 10000 个更改

rdbcompression yes

指定存储至本地数据库时是否压缩数据,默认为 yes,Redis 采用 LZF 压缩,如果为了节省 CPU 时间,可以关闭该选项,但会导致数据库文件变的巨大。

dbfilename dump.rdb

指定本地数据库文件名,默认值为 dump.rdb。

dir ./

指定本地数据库存放目录。

slaveof

设置当本机为 slav 服务时,设置 master 服务的 IP 地址及端口,在 Redis 启动时,它会自动从 master 进行数据同步。

masterauth

当 master 服务设置了密码保护时,slav 服务连接 master 的密码。

requirepass foobared

设置Redis连接密码,如果配置了连接密码,客户端在连接 Redis 时需要通过 auth 命令提供密码,默认关闭。

maxclients 128

设置同一时间最大客户端连接数,默认无限制。

Redis 可以同时打开的客户端连接数为 Redis 进程可以打开的最大文件描述符数。

如果设置 maxclients 0,表示不作限制。

当客户端连接数到达限制时,Redis 会关闭新的连接并向客户端返回 max number of clients reached 错误信息。

maxmemory

指定 Redis 最大内存限制,Redis 在启动时会把数据加载到内存中。

达到最大内存后,Redis 会先尝试清除已到期或即将到期的 Key。

当此方法处理后,仍然到达最大内存设置,将无法再进行写入操作,但仍然可以进行读取操作。

Redis 新的 vm 机制,会把 Key 存放内存,Value 会存放在 swap 区。

appendonly no

指定是否在每次更新操作后进行日志记录,Redis 在默认情况下是异步的把数据写入磁盘。

如果不开启,可能会在断电时导致一段时间内的数据丢失。

因为 redis 本身同步数据文件是按上面 save 条件来同步的,所以有的数据会在一段时间内只存在于内存中。

默认为 no。

appendfilename appendonly.aof

指定更新日志文件名,默认为 appendonly.aof。

appendfsync everysec

指定更新日志条件,共有 3 个可选值:

  • no:表示等操作系统进行数据缓存同步到磁盘(快)
  • always:表示每次更新操作后手动调用 fsync() 将数据写到磁盘(慢,安全)
  • everysec:表示每秒同步一次(折衷,默认值)

vm-enabled no

指定是否启用虚拟内存机制,默认值为 no。

VM 机制将数据分页存放,由 Redis 将访问量较少的页即冷数据 swap 到磁盘上,访问多的页面由磁盘自动换出到内存中。

vm-swap-file /tmp/redis.swap

虚拟内存文件路径,默认值为 /tmp/redis.swap,不可多个 Redis 实例共享。

vm-max-memory 0

将所有大于 vm-max-memory 的数据存入虚拟内存。

无论 vm-max-memory 设置多小,所有索引数据都是内存存储的(Redis 的索引数据 就是 keys)。

也就是说,当 vm-max-memory 设置为 0 的时候,其实是所有 value 都存在于磁盘。

默认值为 0。

vm-page-size 32

Redis swap 文件分成了很多的 page,一个对象可以保存在多个 page 上面,但一个 page 上不能被多个对象共享。

vm-page-size 是要根据存储的数据大小来设定的,作者建议如果存储很多小对象。

page 大小最好设置为 32 bytes 或者 64 bytes。

如果存储很大大对象,则可以使用更大的 page,如果不确定,就使用默认值。

vm-pages 134217728

设置访问 swap 文件的线程数,最好不要超过机器的核数。

如果设置为 0,那么所有对 swap 文件的操作都是串行的,可能会造成比较长时间的延迟。

默认值为 4。

glueoutputbuf yes

设置在向客户端应答时,是否把较小的包合并为一个包发送,默认为开启。

hash-max-zipmap-entries 64
hash-max-zipmap-value 512

指定在超过一定的数量或者最大的元素超过某一临界值时,采用一种特殊的哈希算法。

activerehashing yes

指定是否激活重置哈希,默认为开启。

include /path/to/local.conf

指定包含其它的配置文件,可以在同一主机上多个 Redis 实例之间使用同一份配置文件,而同时各个实例又拥有自己的特定配置文件。

Redis 12 持久化

概述

Redis 是内存数据库,即数据存储在内存。

如果不将内存中的数据保存到磁盘,一旦服务器进程退出,服务器中的数据也会消失。

这样会造成巨大的损失,所以 Redis 提供了持久化功能。

RDB

RDB,即 Redis DataBase

在指定的时间间隔内将内存中的数据集快照写入磁盘。

也就是 Snapshot 快照,恢复时是将快照文件直接读到内存里。

Redis会单独创建(fork)一个子进程来进行持久化。

会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。

整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能。

如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那 RDB 方式要比 AOF 方式更加的高效。

RDB 的缺点是最后一次持久化后的数据可能丢失。

赋值

Fork 的作用是复制一个与当前进程一样的进程。

新进程的所有数据(变量,环境变量,程序计数器等)数值都和原进程一致。

这是一个全新的进程,并作为原进程的子进程。

RDB 保存的是 dump.rdb 文件:

[root@sail redis]# ls
00-RELEASENOTES  BUGS     CONTRIBUTING  deps      INSTALL   MANIFESTO  redis.conf  runtest-cluster    runtest-sentinel  src    TLS.md
bin              CONDUCT  COPYING       dump.rdb  Makefile  README.md  runtest     runtest-moduleapi  sentinel.conf     tests  utils

配置

配置文件redis.conf中的快照配置

################################ SNAPSHOTTING  ################################
# Save the DB to disk.
#
# save <seconds> <changes>
#
# Redis will save the DB if both the given number of seconds and the given
# number of write operations against the DB occurred.
#
# Snapshotting can be completely disabled with a single empty string argument
# as in following example:
#
# save ""
#
# Unless specified otherwise, by default Redis will save the DB:
#   * After 3600 seconds (an hour) if at least 1 key changed
#   * After 300 seconds (5 minutes) if at least 100 keys changed
#   * After 60 seconds if at least 10000 keys changed
#
# You can set these explicitly by uncommenting the three following lines.
#
# save 3600 1
# save 300 100
# save 60 10000
# By default Redis will stop accepting writes if RDB snapshots are enabled
# (at least one save point) and the latest background save failed.
# This will make the user aware (in a hard way) that data is not persisting
# on disk properly, otherwise chances are that no one will notice and some
# disaster will happen.
#
# If the background saving process will start working again Redis will
# automatically allow writes again.
#
# However if you have setup your proper monitoring of the Redis server
# and persistence, you may want to disable this feature so that Redis will
# continue to work as usual even if there are problems with disk,
# permissions, and so forth.
stop-writes-on-bgsave-error yes
# Compress string objects using LZF when dump .rdb databases?
# By default compression is enabled as it's almost always a win.
# If you want to save some CPU in the saving child set it to 'no' but
# the dataset will likely be bigger if you have compressible values or keys.
rdbcompression yes
# Since version 5 of RDB a CRC64 checksum is placed at the end of the file.
# This makes the format more resistant to corruption but there is a performance
# hit to pay (around 10%) when saving and loading RDB files, so you can disable it
# for maximum performances.
#
# RDB files created with checksum disabled have a checksum of zero that will
# tell the loading code to skip the check.
rdbchecksum yes
# Enables or disables full sanitation checks for ziplist and listpack etc when
# loading an RDB or RESTORE payload. This reduces the chances of a assertion or
# crash later on while processing commands.
# Options:
#   no         - Never perform full sanitation
#   yes        - Always perform full sanitation
#   clients    - Perform full sanitation only for user connections.
#                Excludes: RDB files, RESTORE commands received from the master
#                connection, and client connections which have the
#                skip-sanitize-payload ACL flag.
# The default should be 'clients' but since it currently affects cluster
# resharding via MIGRATE, it is temporarily set to 'no' by default.
#
# sanitize-dump-payload no
# The filename where to dump the DB
dbfilename dump.rdb
# Remove RDB files used by replication in instances without persistence
# enabled. By default this option is disabled, however there are environments
# where for regulations or other security concerns, RDB files persisted on
# disk by masters in order to feed replicas, or stored on disk by replicas
# in order to load them for the initial synchronization, should be deleted
# ASAP. Note that this option ONLY WORKS in instances that have both AOF
# and RDB persistence disabled, otherwise is completely ignored.
#
# An alternative (and sometimes better) way to obtain the same effect is
# to use diskless replication on both master and replicas instances. However
# in the case of replicas, diskless is not always an option.
rdb-del-sync-files no
# The working directory.
#
# The DB will be written inside this directory, with the filename specified
# above using the 'dbfilename' configuration directive.
#
# The Append Only File will also be created inside this directory.
#
# Note that you must specify a directory here, not a file name.
dir ./

RDB 是整合内存的压缩过的 Snapshot,RDB 的数据结构,可以配置复合的快照触发条件。

save

save 3600 1
save 300 100
save 60 10000

默认:

  • 1 分钟内改了 1 万次
  • 5 分钟内改了 10 次
  • 15 分钟内改了 1 次
    如果想禁用 RDB 持久化的策略,只要不设置任何 save 指令,或者给 save 传入一个空字符串参数也可以。

若要修改完毕需要立马生效,可以手动使用 save 命令,立马生效 。

stop-writes-on-bgsave-error

如果配置为 no,表示你不在乎数据不一致或者有其他的手段发现和控制,默认为 yes。

rbdcompression

对于存储到磁盘中的快照,可以设置是否进行压缩存储。

如果是的话,redis 会采用 LZF 算法进行压缩,如果你不想消耗 CPU 来进行压缩的话,可以设置为关闭此功能。

rdbchecksum

在存储快照后,还可以让 redis 使用 CRC64 算法来进行数据校验。

但是这样做会增加大约 10% 的性能消耗,如果希望获取到最大的性能提升,可以关闭此功能。

默认为 yes。

触发

01.配置文件中默认的快照配置,建议多用一台机子作为备份,复制一份 dump.rdb。
02.保存配置:

  • save:只管保存,其他不管,全部阻塞。
  • bgsave:Redis 会在后台异步进行快照操作,快照同时还可以响应客户端请求。
  • lastsave:获取最后一次成功执行快照的时间。
    03.执行 flushall 命令,也会产生 dump.rdb 文件,但里面是空的,无意义 。
    04.退出的时候也会产生 dump.rdb 文件。

恢复

将备份文件 dump.rdb 移动到 redis 安装目录并启动服务即可。

本地数据库存放目录:

127.0.0.1:6379> config get dir
1) "dir"
2) "/usr/local/redis"

优缺点

有点

  • 适合大规模的数据恢复。
  • 对数据完整性和一致性要求不高时适用。

缺点

  • 在一定间隔时间做一次备份,所以如果 redis 意外 down 掉的话,就会丢失最后一次快照后的所有修改。
  • Fork 的时候,内存中的数据被克隆了一份,大致 2 倍的膨胀性需要考虑。

小结

redis官网使用_Redis_04

AOF

AOF,即 Append Only File

以日志的形式来记录每个写操作,将 Redis 执行过的所有指令记录下来(读操作不记录)。

只许追加文件,但不可以改写文件,Redis 启动之初会读取该文件重新构建数据。

换言之,Redis 重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。

配置

AOF 保存的是 appendonly.aof 文件:

# 是否以append only模式作为持久化方式,默认使用的是rdb方式持久化,这种方式在许多应用中已经足够用了
appendonly no 
# appendfilename AOF 文件名称
appendfilename "appendonly.aof" 
# appendfsync aof持久化策略的配置:
#     no:不执行fsync,由操作系统保证数据同步到磁盘,速度最快。
#     always:每次写入都执行fsync,以保证数据同步到磁盘。
#     everysec:每秒执行一次fsync,可能会导致丢失这1s数据。
appendfsync everysec 
# 重写时是否可以运用Appendfsync,用默认no即可,保证数据安全性
No-appendfsync-on-rewrite 
# 设置重写的基准值
Auto-aof-rewrite-min-size 
# 设置重写的基准值
Auto-aof-rewrite-percentage

恢复

正常恢复

01.启动:修改配置。修改默认的 appendonly no,改为 yes。
02.复制:将有数据的 aof 文件复制一份保存到对应目录(config get dir)。
03.恢复:重启 redis 然后重新加载。

异常恢复

01.启动:修改配置。修改默认的 appendonly no,改为 yes。
02.破坏:故意破坏 appendonly.aof 文件(写一些非 Redis 命令)。
03.修复:redis-check-aof --fix appendonly.aof 进行修复。
04.恢复:重启 redis 然后重新加载。

重写

AOF 采用文件追加方式,文件会越来越大,为避免出现此种情况,新增了重写机制。

当AOF文件的大小超过所设定的阈值时,Redis 就会启动 AOF 文件的内容压缩。

只保留可以恢复数据的最小指令集,可以使用命令 bgrewriteaof

重写原理

AOF 文件持续增长而过大时,会 Fork 出一条新进程来将文件重写(也是先写临时文件最后再 rename)。

遍历新进程的内存中数据,每条记录有一条的 set 语句。

重写 aof 文件的操作,并没有读取旧的 aof 文件,这点和快照有点类似。

触发机制

Redis 会记录上次重写时的 AOF 大小,默认配置是当 AOF 文件大小是上次 rewrite 后大小的 1 倍且文件大于 64M 时触发。

优缺点

优点

  • appendfsync always:每次修改同步。同步持久化,每次发生数据变更会被立即记录到磁盘。性能较差,但数据完整性比较好。
  • appendfsync everysec:每秒同步。异步操作,每秒记录 ,如果一秒内宕机,有数据丢失。
  • appendfsync no:不同步。从不同步。

缺点

  • 相同数据集的数据而言,AOF 文件要远大于 RDB 文件,恢复速度慢于 RDB。
  • AOF 运行效率要慢于 RDB,每秒同步策略效率较好,不同步效率和 RDB 相同。

小结

redis官网使用_Redis_05

总结

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

Redis 13 事务

概述

Redis 事务的本质是一组命令的集合

事务支持一次执行多个命令,一个事务中所有命令都会被序列化。

在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。

所以说:Redis 事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。

Redis 事务没有隔离级别的概念

批量操作在发送 EXEC 命令前被放入队列缓存,并不会被实际执行。

Redis 事务不保证原子性

Redis中,单条命令是原子性执行的,但事务不保证原子性,且没有回滚。

事务中任意命令执行失败,其余的命令仍会被执行。

Redis事务的三个阶段

01.开始事务

02.命令入队

03.执行事务

命令

监听

watch

watch key1 key2 ...

监视一或多个 key,如果在事务执行之前,被监视的 key 被其他命令改动,则事务被打断(类似乐观锁)。

取消监听

unwatch

取消对所有 key 的监控。

标记

multi

标记一个事务块的开始,形成队列(queued)。

执行

exec

执行所有事务块(一旦执行 exec 后,之前加的监控锁都会被取消掉)。

取消

discard

取消事务,放弃事务块中的所有命令。

实践

正常执行

127.0.0.1:6379> multi # 开启事务
OK
127.0.0.1:6379(TX)> set k1 v1 # 命令入队
QUEUED
127.0.0.1:6379(TX)> set k2 v2 # 命令入队
QUEUED
127.0.0.1:6379(TX)> get k2 # 命令入队
QUEUED
127.0.0.1:6379(TX)> set k3 v3 # 命令入队
QUEUED
127.0.0.1:6379(TX)> exec # 执行事务
1) OK
2) OK
3) "v2"
4) OK
127.0.0.1:6379> get k1 # set命令执行成功
"v1"
127.0.0.1:6379> get k2 # set命令执行成功
"v2"

开启事务后,会出现 TX 标志,此时所有的操作不会马上有结果,而是形成队列(QUEUED),待执行事务后,会将所有命令按顺序执行。

放弃事务

127.0.0.1:6379> multi # 开启事务
OK
127.0.0.1:6379(TX)> set k1 v1 # 命令入队
QUEUED
127.0.0.1:6379(TX)> set k2 v2 # 命令入队
QUEUED
127.0.0.1:6379(TX)> set k3 33 # 命令入队
QUEUED
127.0.0.1:6379(TX)> discard # 取消事务
OK
127.0.0.1:6379> get k3 # set命令未执行
"v3"

事务中存在命令性错误

若在事务队列中存在命令性错误(类似于java编译性错误),则执行 exec 命令时,所有命令都不会执行。

127.0.0.1:6379> multi # 开启事务
OK
127.0.0.1:6379(TX)> set k1 11 # 命令入队
QUEUED
127.0.0.1:6379(TX)> getset k2 # 错误命令
(error) ERR wrong number of arguments for 'getset' command
127.0.0.1:6379(TX)> set k2 22 # 命令入队
QUEUED
127.0.0.1:6379(TX)> exec # 执行事务,报错
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get k1 # set命令未执行
"v1"
127.0.0.1:6379> get k2 # set命令未执行
"v2"

事务中存在语法性错误

若在事务队列中存在语法性错误(类似于 Java 的的运行时异常),则执行 exec 命令时,其他正确命令会被执行,错误命令抛出异常。

127.0.0.1:6379> multi # 开启事务
OK
127.0.0.1:6379(TX)> set k4 v4 # 命令入队
QUEUED
127.0.0.1:6379(TX)> incr k4 # 命令入队(对“v4”进行 +1 ,会报语法错误)
QUEUED
127.0.0.1:6379(TX)> set k5 v5 # 命令入队
QUEUED
127.0.0.1:6379(TX)> exec # 执行事务
1) OK
2) (error) ERR value is not an integer or out of range # 执行错误的命令会报错,其余命令正常执行
3) OK
127.0.0.1:6379> get k4 # set命令执行成功
"v4"
127.0.0.1:6379> get k5 # set命令执行成功
"v5"

监听

悲观锁

悲观锁(Pessimistic Lock),顾名思义,就是很悲观。

每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁。

这样别人想拿到这个数据就会 block 直到它拿到锁。

传统的关系型数据库里面就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在操作之前先上锁。

乐观锁

乐观锁(Optimistic Lock),顾名思义,就是很乐观。

每次去拿数据的时候都认为别人不会修改,所以不会上锁。

但是在更新的时候会判断一下再此期间别人有没有去更新这个数据,可以使用版本号等机制。

乐观锁适用于多读的应用类型,这样可以提高吞吐量。

乐观锁策略:提交版本必须大于记录当前版本才能执行更新。

实践

初始化信用卡可用余额和欠额

127.0.0.1:6379> set balance 100
OK
127.0.0.1:6379> set debt 0
OK

使用 watch 监听 balance,事务期间 balance 数据未变动,事务执行成功。

127.0.0.1:6379> watch balance
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> decrby balance 20
QUEUED
127.0.0.1:6379(TX)> incrby debt 20
QUEUED
127.0.0.1:6379(TX)> exec
1) (integer) 80
2) (integer) 20

使用 watch 监听 balance,事务期间 balance 数据变动,事务执行失败。

窗口1:

127.0.0.1:6379> watch balance
OK
127.0.0.1:6379> multi
OK

窗口2:

127.0.0.1:6379> get balance
"80"
127.0.0.1:6379> set balance 200
OK

窗口1:

127.0.0.1:6379(TX)> decrby balance 20
QUEUED
127.0.0.1:6379(TX)> incrby detb 20
QUEUED
127.0.0.1:6379(TX)> exec
(nil)
127.0.0.1:6379> get balance
"200"

由于窗口 1 监听 balance 并开启事务后,窗口 2 修改了 balance 的值,导致窗口 1 的监听失败,执行事务后展示为空,且 balance 的值不是预期值。

监听失败后放弃监听,然后重来

窗口 1:

127.0.0.1:6379> unwatch
OK
127.0.0.1:6379> watch balance
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> decrby balance 20
QUEUED
127.0.0.1:6379(TX)> incrby debt 20
QUEUED
127.0.0.1:6379(TX)> exec
1) (integer) 180
2) (integer) 40

小结

  • 一旦执行 exec 开启事务后,无论事务是否执行成功, watch 对变量的监听都将被取消。
  • 当事务执行失败后,需重新执行 watch 命令对变量进行监听,并开启新的事务进行操作。
  • watch 指令类似于乐观锁,在事务提交时,如果 watch 监控的多个 key 中任何 key 的值已经被其他客户端更改。

则使用 exec 执行事务时,事务队列将不会被执行,同时返回 (nil) 应答以通知调用者事务执行失败。

Redis 14 发布订阅

概述

Redis 发布订阅(pub / sub)是一种消息通信模式

发送者(pub)发送消息,订阅者(sub)接收消息。

Redis 客户端可以订阅任意数量的频道。

订阅/发布消息图:

redis官网使用_redis_06

频道和订阅频道的客户端之间的关系:

redis官网使用_redis官网使用_07


当有新消息通过 publish 命令发送给频道, 这个消息就会被发送给订阅它的客户端:

redis官网使用_redis_08

命令

这些命令被广泛用于构建即时通信应用,比如网络聊天室(chat room)和实时广播、实时提醒等。

redis官网使用_redis官网使用_09

实践

以下实例演示了发布订阅是如何工作的。

在我们实例中我们创建了订阅频道名为 redisChat
窗口 1,订阅频道:

127.0.0.1:6379> subscribe redisChat
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "redisChat"
3) (integer) 1

窗口 2,订阅频道:

127.0.0.1:6379> subscribe redisChat
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "redisChat"
3) (integer) 1

窗口 3,频道发布消息:

127.0.0.1:6379> publish redisChat "Hello,Redis"
(integer) 2
127.0.0.1:6379> publish redisChat "Hello,World"
(integer) 2

窗口 1 和窗口 2 都会收到发布的消息:

1) "message"
2) "redisChat"
3) "Hello,Redis"
1) "message"
2) "redisChat"
3) "Hello,World"

原理

  • Redis 是使用 C 实现的,通过分析 Redis 源码里的 pubsub.c 文件,可以了解发布和订阅机制的底层实现。
  • Redis 通过 publish 、subscribe 和 psubscribe 等命令实现发布和订阅功能。
  • 通过 subscribe 命令订阅某频道后,redis-server 里维护了一个字典,字典的键就是一个个 channel。
  • 而字典的值则是一个链表,链表中保存了所有订阅这个 channel 的客户端。
  • subscribe 命令的关键,就是将客户端添加到给定 channel 的订阅链表中。
  • 通过 publish 命令向订阅者发送消息,redis-server 会使用给定的频道作为键,在它所维护的 channel 字典中查找记录了订阅这个频道的所有客户端的链表,遍历这个链表,将消息发布给所有订阅者。
  • pub / sub 从字面上理解就是发布(Publish)与订阅(Subscribe),在 Redis 中,你可以设定对某一个 key 值进行消息发布及消息订阅,当一个 key 值上进行了消息发布后,所有订阅它的客户端都会收到相应的消息。
  • 这一功能最明显的用法就是用作实时消息系统,比如普通的即时聊天,群聊等功能。

Redis 15 主从复制

概述

主从复制,是指将一台 Redis 服务器的数据,复制到其他的 Redis 服务器

前者称为主节点(master / leader),后者称为从节点(slave / follower)。

数据的复制是单向的,只能由主节点到从节点。

Master 以写为主,Slave 以读为主。

一个主节点可以有多个从节点(或没有从节点),但一个从节点只能有一个主节点。

默认情况下,每台 Redis 服务器都是主节点。

作用

数据冗余

主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。

故障恢复

当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复。这也是一种服务的冗余。

负载均衡

在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务,分担服务器负载。

尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高 Redis 服务器的并发量。

高可用

主从复制是哨兵和集群能够实施的基础,因此说主从复制是 Redis 高可用的基础。

一般来说,要将 Redis 运用于工程项目中,只使用一台 Redis 是万万不能的,原因如下:

  • 结构上:单个 Redis 服务器会发生单点故障,并且一台服务器需要处理所有的请求负载,压力较大。
  • 容量上:单个 Redis 服务器内存容量有限,一般来说,单台 Redis 最大使用内存不应该超过 20G。

应用

电商网站上的商品,一般都是一次上传,无数次浏览的,说专业点也就是多读少写

对于这种场景,我们可以使用如下这种架构:

redis官网使用_redis_10

环境配置

命令方式

指定主库(配从库不配主库)

slaveof 主库IP 主库端口

这种方式每次与主库断开后,都需要重新连接,非常不方便,不推荐。

配置文件方式

拷贝多个 redis.conf 文件

标准的配置是一主三从,受限于内存大小,这里演示一主二从,所以拷贝 3 份配置文件演示。

[root@sail redis]# cp redis.conf redis6379.conf
[root@sail redis]# cp redis.conf redis6380.conf
[root@sail redis]# cp redis.conf redis6381.conf

这种方式每次与主库断开后,都需要重新连接,非常不方便,不推荐。

修改配置文件

修改 redis6379.conf

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

修改 redis6380.conf

daemonize yes
pidfile /var/run/redis_6380.pid
logfile "6380.log"
dbfilename dump6380.rdb

修改 redis6381.conf

daemonize yes
pidfile /var/run/redis_6381.pid
logfile "6381.log"
dbfilename dump6381.rdb

一主二从

启动服务

[root@sail redis]# redis-server redis6379.conf
[root@sail redis]# redis-server redis6380.conf
[root@sail redis]# redis-server redis6381.conf

查看启动的服务

[root@sail redis]# ps -ef|grep redis
root      8066  6267  0 21:40 pts/3    00:00:00 redis-cli -p 6380
root      8388     1  0 21:46 ?        00:00:00 redis-server 127.0.0.1:6380
root      8397     1  0 21:46 ?        00:00:00 redis-server 127.0.0.1:6381
root      8417  6213  0 21:47 pts/0    00:00:00 grep --color=auto redis
root     11953     1  0 Mar15 ?        02:02:44 ./bin/redis-server *:6379

查看主从复制信息
info replication

redis官网使用_Redis_11


默认三个都是 master,都是主机。

配置为一个 master 两个 slave
slaveof 地址 端口号

redis官网使用_redis官网使用_12


也可以通过配置文件直接指定主机,这样比较方便,也便于维护。

redis官网使用_redis官网使用_13

主机赋值,从机取值

redis官网使用_数据库_14


从机是不能赋值的,这样就实现了读写分离

主机断开

redis官网使用_数据库_15


主机恢复以后赋的值从机依然能够读取到,这样可以保证高可用。

从机断开

redis官网使用_redis官网使用_16


从机断开后,主机赋了新值,从机启动后无法获取到新值,原因是从机启动后又默认为主机了。

如果想启动即为从机,可以按照前面说的配置 replicaof 进行设置。

层层链路

从机也可以被其他从机当作主机,可以有效减轻主机的写压力。

redis官网使用_redis官网使用_17


redis官网使用_数据库_18


6381 指定 6380 作为主机,6380 依然是从机,只是有了从机节点。

这样 6379 赋的值只需要复制到 6380,6380 再复制到 6381,这样就有效的减轻主机的写压力。

谋朝篡位

slaveof no one

redis官网使用_Redis_19


主机断开后,从机如果想要当主机,可以使用 slaveof no one 进行“谋朝篡位”,从而变成主机。

但此时其他节点还是很“忠心”,依然认定之前的主机为主机,这样变成的主机是没有从机的,是个“孤家寡人”。

主机如果恢复,可以“平息叛乱”,之前的从机依旧认定它为主机。

改朝换代

前面的操作在实际场景中并不适用,因为我们希望的是主机断开后有从机作为主机,依旧实现主从复制。

所以在从机“谋朝篡位”后,还需要让剩余的从机“认主”,让他们都“归顺”于新的主机。

这样原来的主机恢复后就变成了“孤家寡人”。

redis官网使用_redis官网使用_20

Redis 16 哨兵模式

概述

主从切换技术的操作是:当主机宕机后,需要手动把一台从机切换为主机。

这就需要人工干预,费事费力,还会造成一段时间内服务不可用。

这不是一种推荐的方式,更多时候,我们优先考虑哨兵模式

Redis 从 2.8 开始正式提供了 Sentinel(哨兵) 架构来解决这个问题。

它是“谋朝篡位”的自动版,能够后台监控主机是否故障,如果故障了根据投票数自动将从机转换为主机。

哨兵模式是一种特殊的模式,首先 Redis 提供了哨兵的命令,哨兵是一个独立的进程,它会独立运行。

其原理是哨兵通过发送命令,等待 Redis 服务器响应,从而监控运行的多个 Redis 实例。

redis官网使用_redis官网使用_21


这里的哨兵有两个作用:

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

然而一个哨兵进程对 Redis 服务器进行监控,可能会出现问题,为此,我们可以使用多个哨兵进行监控。

各个哨兵之间还会进行监控,这样就形成了多哨兵模式:

redis官网使用_redis_22


假设主机宕机,哨兵 1 先检测到这个结果,系统并不会马上进行 failover(故障转移) 过程,仅仅是哨兵 1 主观的认为主机不可用,这个现象称为主观下线

当后面的哨兵也检测到主机不可用,并且数量达到一定值时,哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行 failover 操作。

切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从机实现切换主机,这个过程称为客观下线

使用

配置

受限于内存大小,这里只演示一个哨兵进程监测一个主机的情况。

在 redis 目录下新建 myconfig 目录,并创建文件 sentinel.conf

文件名必须为 sentinel.conf

[root@sail redis]# mkdir myconfig
[root@sail redis]# cd myconfig/
[root@sail myconfig]# vim sentinel.conf

编写配置

sentinel monitor myredis 127.0.0.1 6379 1

末尾的 1 代表选票达到多少时选举成功。

启动

redis-sentinel myconfig/sentinel.conf

[root@sail redis]# redis-sentinel myconfig/sentinel.conf 
16728:X 08 Jun 2022 22:23:38.464 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
16728:X 08 Jun 2022 22:23:38.464 # Redis version=6.2.6, bits=64, commit=00000000, modified=0, pid=16728, just started
16728:X 08 Jun 2022 22:23:38.464 # Configuration loaded
16728:X 08 Jun 2022 22:23:38.465 * monotonic clock: POSIX clock_gettime
                _._                                                  
           _.-``__ ''-._                                             
      _.-``    `.  `_.  ''-._           Redis 6.2.6 (00000000/0) 64 bit
  .-`` .-```.  ```\/    _.,_ ''-._                                  
 (    '      ,       .-`  | `,    )     Running in sentinel mode
 |`-._`-...-` __...-.``-._|'` _.-'|     Port: 26379
 |    `-._   `._    /     _.-'    |     PID: 16728
  `-._    `-._  `-./  _.-'    _.-'                                   
 |`-._`-._    `-.__.-'    _.-'_.-'|                                  
 |    `-._`-._        _.-'_.-'    |           https://redis.io       
  `-._    `-._`-.__.-'_.-'    _.-'                                   
 |`-._`-._    `-.__.-'    _.-'_.-'|                                  
 |    `-._`-._        _.-'_.-'    |                                  
  `-._    `-._`-.__.-'_.-'    _.-'                                   
      `-._    `-.__.-'    _.-'                                       
          `-._        _.-'                                           
              `-.__.-'                                               
16728:X 08 Jun 2022 22:23:38.466 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128.
16728:X 08 Jun 2022 22:23:38.469 # Sentinel ID is a7ebdd5f7260485b8f6108169d96910b747e3c8f
16728:X 08 Jun 2022 22:23:38.469 # +monitor master myredis 127.0.0.1 6379 quorum 1
16728:X 08 Jun 2022 22:23:38.469 * +slave slave 127.0.0.1:6380 127.0.0.1 6380 @ myredis 127.0.0.1 6379

主机断开

redis官网使用_redis_23


主机断开后,哨兵进程会监测到,然后发起选举,调用选举算法,最后选举 6380 为新的主机,6381 也认定其为主机。

主机恢复

redis官网使用_redis_24


之前断开的主机恢复后,哨兵进程也会检测到,但此时并不会将其再设为主机,而是设为新的主机的从机。

优缺点

优点

哨兵集群模式是基于主从模式的,所有主从的优点,哨兵模式同样具有。

主从可以切换,故障可以转移,系统可用性更好。

哨兵模式是主从模式的升级,系统更健壮,可用性更高。

缺点

Redis较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。

实现哨兵模式的配置也不简单,甚至可以说有些繁琐。

完整配置

前面自定义的 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 无法正常启动成功。
# 通知脚本
# sentinel notification-script <master-name> <script-path>
sentinel notification-script mymaster /var/redis/notify.sh
# 客户端重新配置主节点参数脚本
# 当一个 master 由于 failover 而发生改变时,这个脚本将会被调用,通知相关的客户端关于 master 地址已经发生改变的信息。
# 以下参数将会在调用脚本时传给脚本:
# <master-name> <role> <state> <from-ip> <from-port> <to-ip> <to-port>
# 目前 <state> 总是 “failover”,<role> 是 “leader” 或者 “observer” 中的一个。
# 参数 from-ip,from-port,to-ip,to-port是用来和旧的 master 和新的 master (即旧的 slave)通信的
# 这个脚本应该是通用的,能被多次调用,不是针对性的。
# sentinel client-reconfig-script <master-name> <script-path>
sentinel client-reconfig-script mymaster /var/redis/reconfig.sh

Redis 17 缓存穿透 缓存击穿 缓存雪崩

使用缓存的问题

Redis 缓存的使用,极大的提升了应用程序的性能和效率,特别是数据查询方面。

但同时,它也带来了一些问题。其中,最要害的问题,就是数据的一致性问题,从严格意义上讲,这个问题无解。

如果对数据的一致性要求很高,那么就不能使用缓存。

另外的一些典型问题就是,缓存穿透、缓存雪崩和缓存击穿。目前,业界也都有比较流行的解决方案。

缓存穿透

概念

这里先介绍下日常使用缓存的逻辑:

查询一个数据,先到缓存中查询。

如果缓存中存在,则返回。

如果缓存中不存在,则到数据库查询。

如果数据库中存在,则返回数据,且存到缓存。

如果数据库中不存在,则返回空值。

缓存穿透

缓存穿透出现的情况就是数据库和缓存中都没有。

这样缓存就不能拦截,数据库中查不到值也就不能存到缓存。

这样每次这样查询都会到数据库,相当于直达了,即穿透

这样会给数据库造成很大的压力。

解决方案

布隆过滤器

布隆过滤器是一种数据结构,对所有可能查询的参数以 hash 形式存储,在控制层先进行校验,不符合则丢弃,从而避免了对底层存储系统的查询压力。

redis官网使用_redis_25


缓存空对象

当存储层不命中后,即使返回的空对象也将其缓存起来,同时会设置一个过期时间,之后再访问这个数据将会从缓存中获取,保护了后端数据源。

redis官网使用_Redis_26


但是这种方法会存在两个问题:

  • 如果空值能够被缓存起来,这就意味着缓存需要更多的空间存储更多的键,因为这当中可能会有很多的空值的键。
  • 即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响。

缓存击穿

概念

缓存击穿,是指一个 key 非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问。

当这个 key 在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。

当某个 key 在过期的瞬间,有大量的请求并发访问,这类数据一般是热点数据。

由于缓存过期,会同时访问数据库来查询最新数据,并且回写缓存,会导使数据库瞬间压力过大。

解决方案

设置热点数据永不过期
从缓存层面来看,没有设置过期时间,所以不会出现热点 key 过期后产生的问题。
加互斥锁
分布式锁:使用分布式锁,保证对于每个 key 同时只有一个线程去查询后端服务,其他线程没有获得分布式锁的权限,因此只能等待。

这种方式将高并发的压力转移到了分布式锁,因此对分布式锁的考验很大。

缓存雪崩

概念

缓存雪崩,是指在某一个时间段,缓存集中过期失效。

产生雪崩的原因之一,比如马上就要到双十一零点,很快就会迎来一波抢购。

这波商品时间比较集中的放入了缓存,假设缓存一个小时。

那么到了凌晨一点钟的时候,这批商品的缓存就都过期了。

而对这批商品的访问查询,都落到了数据库上,对于数据库而言,就会产生周期性的压力波峰。

于是所有的请求都会达到存储层,存储层的调用量会暴增,造成存储层也会挂掉的情况。

redis官网使用_redis_27


其实集中过期,倒不是非常致命。

比较致命的缓存雪崩,是缓存服务器某个节点宕机或断网。

因为自然形成的缓存雪崩,一定是在某个时间段集中创建缓存。

这个时候,数据库也是可以顶住压力的,无非就是对数据库产生周期性的压力而已。

而缓存服务节点的宕机,对数据库服务器造成的压力是不可预知的,很有可能瞬间就把数据库压垮。

解决方案

搭建集群
实现 Redis 的高可用,既然一台服务有可能挂掉,那就多增设几台服务。

这样一台挂掉之后其他的还可以继续工作,其实就是搭建的集群。
限流降级
在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。

比如对某个 key 只允许一个线程查询数据和写缓存,其他线程等待。
数据预热
数据加热的含义就是在正式部署之前,先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。

在即将发生大并发访问前手动触发加载缓存不同的 key,设置不同的过期时间,让缓存失效的时间点尽量均匀。

Redis 18 Jedis

概述

Jedis 是 Redis 官方推荐的 Java 连接开发工具。

Jedis 客户端同时支持单机模式、分片模式、集群模式的访问模式:

  • 通过构建 Jedis 类对象实现单机模式下的数据访问。
  • 通过构建 ShardedJedis 类对象实现分片模式的数据访问。
  • 通过构建 JedisCluster 类对象实现集群模式下的数据访问。
    Jedis 客户端支持单命令和 Pipeline 方式访问 Redis 集群,通过 Pipeline 的方式能够提高集群访问的效率。

测试

01.新建一个普通的 Maven 项目。
02.导入 Jedis、slf4j-log4j12 的依赖。

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>4.2.3</version>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>1.7.25</version>
</dependency>

导入 slf4j-log4j12 的依赖是因为启动项目的时候报 Failed to load class "org.slf4j.impl.StaticLoggerBinder"

如果启动时没有报这个错误,也可以不导入这个依赖。

01.编写测试代码

// 47.100.222.85 为阿里云服务器地址
Jedis jedis = new Jedis("47.100.222.85", 6379);
System.out.println(jedis.ping());

输出结果为:

PONG

出现这个结果即代表 Java 连接 Redis 成功。

这里使用的是连接阿里云服务器上的 Redis 服务

连接前,需要先进行检查:

redis 是否关闭了远程连接

检查 redis.conf 中的配置

如果设置了 bind 127.0.0.1 ::1 ,代表只能本地访问,需要注释掉。

如果设置了 protected-mode yes ,代表开启了保护模式,需要改为 protected-mode no,关闭保护模式。

修改配置后需要重启 Redis 服务。

防火墙是否拦截了 6379 端口

放行 6379 端口

firewall-cmd --zone=public --add-port=6379/tcp --permanent

重启防火墙

systemctl restart firewalld

如果是阿里云服务器需要配置安全组

redis官网使用_redis_28


以上配置后就可以连接远程 Redis 服务了。

基本API

验证密码

jedis.auth("");

如果没有密码则不需要验证。

连接

jedis.connect();

断开连接

jedis.disconnect();

操作 key

Jedis jedis = new Jedis("47.100.222.85", 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());

操作 String

Jedis jedis = new Jedis("47.100.222.85", 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("key01", "value01", "key02", "value02", "key03", "value03"));
System.out.println("获取多个键值对:" + jedis.mget("key01", "key02", "key03"));
System.out.println("获取多个键值对:" + jedis.mget("key01", "key02", "key03", "key04"));
System.out.println("删除多个键值对:" + jedis.del("key01", "key02"));
System.out.println("获取多个键值对:" + jedis.mget("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(3);
} catch (InterruptedException e) {
    e.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", 2, 4));

操作 List

Jedis jedis = new Jedis("127.0.0.1", 6379);
jedis.flushDB();
System.out.println("===========添加一个list===========");
jedis.lpush("collections", "ArrayList", "Vector", "Stack", "HashMap", "WeakHashMap", "LinkedHashMap");
jedis.lpush("collections", "HashSet");
jedis.lpush("collections", "TreeSet");
jedis.lpush("collections", "TreeMap");
System.out.println("collections的内容:" + jedis.lrange("collections", 0, -1));//-1代表倒数第一个元素,-2代表倒数第二个元素,end为-1表示查询全部
System.out.println("collections区间0-3的元素:" + jedis.lrange("collections", 0, 3));
System.out.println("===============================");
// 删除列表指定的值 ,第二个参数为删除的个数(有重复时),后add进去的值先被删,类似于出栈
System.out.println("删除指定元素个数:" + jedis.lrem("collections", 2, "HashMap"));
System.out.println("collections的内容:" + jedis.lrange("collections", 0, -1));
System.out.println("删除下表0-3区间之外的元素:" + jedis.ltrim("collections", 0, 3));
System.out.println("collections的内容:" + jedis.lrange("collections", 0, -1));
System.out.println("collections列表出栈(左端):" + jedis.lpop("collections"));
System.out.println("collections的内容:" + jedis.lrange("collections", 0, -1));
System.out.println("collections添加元素,从列表右端,与lpush相对应:" + jedis.rpush("collections", "EnumMap"));
System.out.println("collections的内容:" + jedis.lrange("collections", 0, -1));
System.out.println("collections列表出栈(右端):" + jedis.rpop("collections"));
System.out.println("collections的内容:" + jedis.lrange("collections", 0, -1));
System.out.println("修改collections指定下标1的内容:" + jedis.lset("collections", 1, "LinkedArrayList"));
System.out.println("collections的内容:" + jedis.lrange("collections", 0, -1));
System.out.println("===============================");
System.out.println("collections的长度:" + jedis.llen("collections"));
System.out.println("获取collections下标为2的元素:" + jedis.lindex("collections", 2));
System.out.println("===============================");
jedis.lpush("sortedList", "3", "6", "2", "0", "7", "4");
System.out.println("sortedList排序前:" + jedis.lrange("sortedList", 0, -1));
System.out.println(jedis.sort("sortedList"));
System.out.println("sortedList排序后:" + jedis.lrange("sortedList", 0, -1));

操作 Set

Jedis jedis = new Jedis("127.0.0.1", 6379);
jedis.flushDB();
System.out.println("============向集合中添加元素(不重复)============");
System.out.println(jedis.sadd("eleSet", "e1", "e2", "e4", "e3", "e0", "e8", "e7", "e5"));
System.out.println(jedis.sadd("eleSet", "e6"));
System.out.println(jedis.sadd("eleSet", "e6"));
System.out.println("eleSet的所有元素为:" + jedis.smembers("eleSet"));
System.out.println("删除一个元素e0:" + jedis.srem("eleSet", "e0"));
System.out.println("eleSet的所有元素为:" + jedis.smembers("eleSet"));
System.out.println("删除两个元素e7和e6:" + jedis.srem("eleSet", "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", "e1", "e2", "e4", "e3", "e0", "e8", "e7", "e5"));
System.out.println(jedis.sadd("eleSet2", "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("eleSet1", "eleSet2"));
System.out.println("eleSet1和eleSet2的并集:" + jedis.sunion("eleSet1", "eleSet2"));
//eleSet1中有,eleSet2中没有
System.out.println("eleSet1和eleSet2的差集:" + jedis.sdiff("eleSet1", "eleSet2"));
//求交集并将交集保存到dstkey的集合
jedis.sinterstore("eleSet4", "eleSet1", "eleSet2");
System.out.println("eleSet4中的元素:" + jedis.smembers("eleSet4"));

操作 Hash

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");
// 添加名称为 hash(key)的 hash 元素
jedis.hmset("hash", map);
// 向名称为 hash 的 hash 中添加 key 为 key5,value 为 value5 元素
jedis.hset("hash", "key5", "value5");
// return Map<String,String>
System.out.println("散列hash的所有键值对为:" + jedis.hgetAll(" hash"));
// return Set<String>
System.out.println("散列hash的所有键为:" + jedis.hkeys("hash"));
// return List<String>
System.out.println("散列hash的所有值为:" + jedis.hvals("hash"));
System.out.println("将key6保存的值加上一个整数,如果key6不存在则添加key6:" + jedis.hincrBy(" hash", " key6", 6));
System.out.println("散列hash的所有键值对为:" + jedis.hgetAll("hash"));
System.out.println("将key6保存的值加上一个整数,如果key6不存在则添加key6:" + jedis.hincrBy(" hash", " key6", 3));
System.out.println("散列hash的所有键值对为:" + jedis.hgetAll("hash"));
System.out.println("删除一个或者多个键值对:" + jedis.hdel("hash", "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", "key3"));
System.out.println("获取hash中的值:" + jedis.hmget(" hash", " key3", " key4"));

事务

//创建客户端连接服务端,redis服务端需要被开启
Jedis jedis = new Jedis("127.0.0.1", 6379);
jedis.flushDB();
JSONObject jsonObject = new JSONObject();
jsonObject.put("hello", "world");
jsonObject.put("name", "java");
//开启事务
Transaction multi = jedis.multi();
String result = jsonObject.toJSONString();
try {
    //向redis存入一条数据
    multi.set("json", result);
    //再存入一条数据
    multi.set("json2", result);
    //这里引发了异常,用0作为被除数
    int i = 100 / 0;
    //如果没有引发异常,执行进入队列的命令
    multi.exec();
} catch (Exception e) {
    e.printStackTrace();
    //如果出现异常,回滚
    multi.discard();
} finally {
    System.out.println(jedis.get("json"));
    System.out.println(jedis.get("json2"));
    //最终关闭客户端
    jedis.close();
}

Redis 19 整合SpringBoot

概述

SpringBoot 整合 Redis 是使用 SpringData 实现的。

SpringData 是与 SpringBoot 齐名的顶级项目,整合了对常用数据库的模板型操作。

在 SpringBoot 2.x 之后,Jedis 被 Lettuce 替代了。

Jedis

采用的直连,多个线程操作的话,是不安全的。

如果想要避免不安全,就要使用 Jedis pool 连接池解决。

这样是有一些弊端的,比如线程数量太多了,Redis 服务就比较庞大,而且它是阻塞的。

Lettuce

底层采用 Netty,实例可以在多个线程中进行共享。

不存在线程不安全的情况,可以减少线程数量。

使用

导入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

yaml 配置

spring:
  redis:
    host: 47.100.222.85
    port: 6379
    jedis:
      pool:
        max-active: 8
        max-wait: -1ms
        max-idle: 500
        min-idle: 0
    lettuce:
      shutdown-timeout: 0ms

测试

@Resource
private RedisTemplate redisTemplate;
@Test
void contextLoads() {
    redisTemplate.opsForValue().set("myKey", "myValue");
    System.out.println(redisTemplate.opsForValue().get("myKey"));
}

输出为:myValue,连接成功。

源码分析

RedisAutoConfiguration

@AutoConfiguration
@ConditionalOnClass({RedisOperations.class})
@EnableConfigurationProperties({RedisProperties.class})
@Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class})
public class RedisAutoConfiguration {
    public RedisAutoConfiguration() {
    }
    @Bean
    @ConditionalOnMissingBean(
        name = {"redisTemplate"}
    )
    @ConditionalOnSingleCandidate(RedisConnectionFactory.class)
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        // 默认的 RedisTemplate 没有过多的设置,Redis 对象都是需要序列化的
        // 两个泛型都是 Object 的类型,我们使用需要强制转换,很不方便,预期是 <String, Object>
        RedisTemplate<Object, Object> template = new RedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }
    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnSingleCandidate(RedisConnectionFactory.class)
    // 由于 String 是 Redis 最常使用的类型,所以说单独提出来了一个 Bean
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
        return new StringRedisTemplate(redisConnectionFactory);
    }
}

通过源码可以看出,SpringBoot 自动帮我们在容器中生成了一个 RedisTemplate 和一个 StringRedisTemplate。

但是,这个 RedisTemplate 的泛型是 <Object, Object>,写代码不方便,需要写好多类型转换的代码。

我们需要一个泛型为 <String, Object> 形式的 RedisTemplate。

并且,这个 RedisTemplate 没有设置数据存在 Redis 时,key 及 value 的序列化方式。

@ConditionalOnMissingBean 可以看出,如果 Spring 容器中有了自定义的 RedisTemplate 对象,自动配置的 RedisTemplate 不会实例化。

因此我们可以直接自己写个配置类,配置 RedisTemplate。

自定义 Redis 模板

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
 * 自定义 Redis 模板
 *
 * @author LiaoHang
 * @date 2022-06-09 22:06
 */
@Configuration
public class RedisConfig {
    @Bean
    @SuppressWarnings("all")
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        // 定义泛型为 <String, Object> 的 RedisTemplate
        RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
        // 设置连接工厂
        template.setConnectionFactory(factory);
        // 定义 Json 序列化
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        // Json 转换工具
        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 操作 Redis,比较繁琐。

因此直接封装好一个 RedisUtils,这样写代码更方便点。

这个 RedisUtils 交给Spring容器实例化,使用时直接注解注入。

Redis 工具类

package cn.sail.redisspringboot.util;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import javax.annotation.Resource;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
 * Redis 工具类
 *
 * @author LiaoHang
 * @date 2022-06-09 22:15
 */
@Component
public class RedisUtil {
    @Resource
    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) {
        // 如果返回值为 null,则返回 0L
        return redisTemplate.getExpire(key, TimeUnit.SECONDS);
    }
    /**
     * 判断key是否存在
     *
     * @param key 键
     * @return true 存在 false不存在
     */
    public boolean hasKey(String key) {
        try {
            return Boolean.TRUE.equals(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((Collection<String>) 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);
    }
    // ===============================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 {
            return redisTemplate.opsForList().remove(key, count, value);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
    // ============================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 {
            return redisTemplate.opsForSet().remove(key, values);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
    // ================================Hash 哈希=================================
    /**
     * 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);
    }
}