Redis


1、Nosql 概述


1.1、为什么使用 nosql


单机 Mysql 时代

image-20210516220238091

90 年代,一个网站的访问量一般不会太大,单个数据库完全够用。随着用户增多,网站出现以下问题

  1. 数据量增加到一定程度,单机数据库就放不下了
  2. 数据的索引(B+ Tree),一个机器内存也存放不下
  3. 访问量变大后(读写混合),一台服务器承受不住。

Memcached(缓存) + Mysql + 垂直拆分(读写分离)

网站 80%的情况都是在读,每次都要去查询数据库的话就十分的麻烦!所以说我们希望减轻数据库的压力,我们可以使用缓存来保证效率!

image-20210516220519407

优化过程经历了以下几个过程:

  1. 优化数据库的数据结构和索引(难度大)
  2. 文件缓存,通过 IO 流获取比每次都访问数据库效率略高,但是流量爆炸式增长时候,IO 流也承受不了
  3. MemCache,当时最热门的技术,通过在数据库和数据库访问层之间加上一层缓存,第一次访问时查询数据库,将结果保存到缓存,后续的查询先检查缓存,若有直接拿去使用,效率显著提升。

分库分表 + 水平拆分 + Mysql 集群

image-20210516220720755

如今最近的年代

如今信息量井喷式增长,各种各样的数据出现(用户定位数据,图片数据等),大数据的背景下关系型数据库(RDBMS)无法满足大量数据要求。Nosql 数据库就能轻松解决这些问题。

目前一个基本的互联网项目

image-20210516220816818

为什么要用 NoSQL ?

用户的个人信息,社交网络,地理位置。用户自己产生的数据,用户日志等等爆发式增长!
这时候我们就需要使用 NoSQL 数据库的,Nosql 可以很好的处理以上的情况!

1.2、什么是 NoSql

NoSQL = Not Only SQL(不仅仅是 SQL)

Not Only Structured Query Language

关系型数据库:列+行,同一个表下数据的结构是一样的。

非关系型数据库:数据存储没有固定的格式,并且可以进行横向扩展。

NoSQL 泛指非关系型数据库,随着 web2.0 互联网的诞生,传统的关系型数据库很难对付 web2.0 时代!尤其是超大规模的高并发的社区,暴露出来很多难以克服的问题,NoSQL 在当今大数据环境下发展的十分迅速,Redis 是发展最快的。

1.3、Nosql 特点

  1. 方便扩展(数据之间没有关系,很好扩展!)

  2. 大数据量高性能(Redis 一秒可以写 8 万次,读 11 万次,NoSQL 的缓存记录级,是一种细粒度的缓存,性能会比较高!)

  3. 数据类型是多样型的!(不需要事先设计数据库,随取随用)

  4. 传统的 RDBMS 和 NoSQL

    1
    2
    3
    4
    5
    6
    7
    传统的 RDBMS(关系型数据库)
    - 结构化组织
    - SQL
    - 数据和关系都存在单独的表中 row col
    - 操作,数据定义语言
    - 严格的一致性
    - 基础的事务
  5. ```bush
    Nosql

    • 不仅仅是数据
    • 没有固定的查询语言
    • 键值对存储,列存储,文档存储,图形数据库(社交关系)
    • 最终一致性
    • CAP定理和BASE
    • 高性能,高可用,高扩展
    • ..
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48

      **了解:3V + 3 高**

      大数据时代的 3V :主要是描述问题的

      1. 海量 Velume
      2. 多样 Variety
      3. 实时 Velocity

      大数据时代的 3 高 : 主要是对程序的要求

      1. 高并发
      2. 高可扩
      3. 高性能

      真正在公司中的实践:NoSQL + RDBMS 一起使用才是最强的。

      ### 1.4、阿里巴巴演进分析

      推荐阅读:阿里云的这群疯子https://yq.aliyun.com/articles/653511

      ![1](https://img-blog.csdnimg.cn/20200820103829446.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0RERERlbmdf,size_16,color_FFFFFF,t_70#pic_center)

      ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200820103851613.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0RERERlbmdf,size_16,color_FFFFFF,t_70#pic_center)

      ```bush
      # 商品信息
      - 一般存放在关系型数据库:Mysql,阿里巴巴使用的Mysql都是经过内部改动的。

      # 商品描述、评论(文字居多)
      - 文档型数据库:MongoDB

      # 图片
      - 分布式文件系统 FastDFS
      - 淘宝:TFS
      - Google: GFS
      - Hadoop: HDFS
      - 阿里云: oss

      # 商品关键字 用于搜索
      - 搜索引擎:solr,elasticsearch
      - 阿里:Isearch 多隆

      # 商品热门的波段信息
      - 内存数据库:Redis,Memcache

      # 商品交易,外部支付接口
      - 第三方应用

1.5、Nosql 的四大分类

KV 键值对

  • 新浪:Redis
  • 美团:Redis + Tair
  • 阿里、百度:Redis + Memcache

文档型数据库(bson 数据格式):

  • MongoDB(掌握)
    • 基于分布式文件存储的数据库。C++编写,用于处理大量文档。
    • MongoDB 是 RDBMS 和 NoSQL 的中间产品。MongoDB 是非关系型数据库中功能最丰富的,NoSQL 中最像关系型数据库的数据库。
  • ConthDB

列存储数据库

  • HBase(大数据必学)
  • 分布式文件系统

图关系数据库

用于广告推荐,社交网络

  • Neo4j、InfoGrid
分类 Examples 举例 典型应用场景 数据模型 优点 缺点
键值对(key-value) Tokyo Cabinet/Tyrant, Redis, Voldemort, Oracle BDB 内容缓存,主要用于处理大量数据的高访问负载,也用于一些日志系统等等。 Key 指向 Value 的键值对,通常用 hash table 来实现 查找速度快 数据无结构化,通常只被当作字符串或者二进制数据
列存储数据库 Cassandra, HBase, Riak 分布式的文件系统 以列簇式存储,将同一列数据存在一起 查找速度快,可扩展性强,更容易进行分布式扩展 功能相对局限
文档型数据库 CouchDB, MongoDb Web 应用(与 Key-Value 类似,Value 是结构化的,不同的是数据库能够了解 Value 的内容) Key-Value 对应的键值对,Value 为结构化数据 数据结构要求不严格,表结构可变,不需要像关系型数据库一样需要预先定义表结构 查询性能不高,而且缺乏统一的查询语法。
图形(Graph)数据库 Neo4J, InfoGrid, Infinite Graph 社交网络,推荐系统等。专注于构建关系图谱 图结构 利用图结构相关算法。比如最短路径寻址,N 度关系查找等 很多时候需要对整个图做计算才能得出需要的信息,而且这种结构不太好做分布式的集群

2、Redis 入门


2.1、概述

Redis 是什么?

Redis(Remote Dictionary Server ),即远程字典服务。

是一个开源的使用 ANSI C 语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value 数据库,并提供多种语言的 API。

与 memcached 一样,为了保证效率,数据都是缓存在内存中。区别的是 redis 会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了 master-slave(主从)同步。

Redis 能该干什么?

  1. 内存存储、持久化,内存是断电即失的,所以需要持久化(RDB、AOF)
  2. 高效率、用于高速缓冲
  3. 发布订阅系统
  4. 地图信息分析
  5. 计时器、计数器(eg:浏览量)
  6. 。。。

特性

  1. 多样的数据类型

  2. 持久化

  3. 集群

  4. 事务

2.2、环境搭建

官网:https://redis.io/

推荐使用 Linux 服务器学习。

windows 版本的 Redis 已经停更很久了…

2.2.1、Linux 安装

  1. 下载安装包!redis-5.0.8.tar.gz
  2. 解压 Redis 的安装包!程序一般放在 /opt 目录下

image-20210516221633789

​ 3.基本环境安装

1
2
3
4
5
yum install gcc-c++
# 然后进入redis目录下执行
make
# 然后执行
make install

image-20210516221849467

4.redis 默认安装路径 /usr/local/bin

image-20210516221928738

5.将 redis 的配置文件复制到 程序安装目录 /usr/local/bin/huangconfig

image-20210516222019691

6.redis 默认不是后台启动的,需要修改配置文件!

image-20210516222100044

7.通过制定的配置文件启动 redis 服务

image-20210516222117568

8.使用 redis-cli 连接指定的端口号测试,Redis 的默认端口 6379

image-20210516222345701

9.查看 redis 进程是否开启

image-20210516222451085

10.关闭 Redis 服务 shutdown

image-20210516222514046

11.再次查看进程

12.后面我们会使用单机多 Redis 启动集群测试

2.3 测试性能

redis-benchmark:Redis 官方提供的性能测试工具,参数选项如下:

image-20210516222701671

简单测试:

1
2
# 测试:100个并发连接 100000请求
redis-benchmark -h localhost -p 6379 -c 100 -n 100000

image-20210517111003925

基础知识

redis 默认有 16 个数据库

image-20210517111019576

默认使用的第 0 个;

16 个数据库为:DB 0~DB 15
默认使用 DB 0 ,可以使用select n切换到 DB n,dbsize可以查看当前数据库的大小,与 key 数量相关。

1
2
3
4
5
6
7
8
127.0.0.1:6379> config get databases # 命令行查看数据库数量databases
1) "databases"
2) "16"

127.0.0.1:6379> select 8 # 切换数据库 DB 8
OK
127.0.0.1:6379[8]> dbsize # 查看数据库大小
(integer) 0

# 不同数据库之间 数据是不能互通的,并且 dbsize 是根据库中 key 的个数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
127.0.0.1:6379> set name sakura
OK
127.0.0.1:6379> SELECT 8
OK
127.0.0.1:6379[8]> get name # db8中并不能获取db0中的键值对。
(nil)
127.0.0.1:6379[8]> DBSIZE
(integer) 0
127.0.0.1:6379[8]> SELECT 0
OK
127.0.0.1:6379> keys *
1) "counter:__rand_int__"
2) "mylist"
3) "name"
4) "key:__rand_int__"
5) "myset:__rand_int__"
127.0.0.1:6379> DBSIZE # size和key个数相关
(integer) 5

keys * :查看当前数据库中所有的 key。

flushdb:清空当前数据库中的键值对。

flushall:清空所有数据库的键值对。

Redis 是单线程的,Redis 是基于内存操作的。

所以 Redis 的性能瓶颈不是 CPU,而是机器内存和网络带宽。

那么为什么 Redis 的速度如此快呢,性能这么高呢?QPS 达到 10W+

Redis 为什么单线程还这么快?

  • 误区 1:高性能的服务器一定是多线程的?
  • 误区 2:多线程(CPU 上下文会切换!)一定比单线程效率高!

核心:Redis 是将所有的数据放在内存中的,所以说使用单线程去操作效率就是最高的,多线程(CPU 上下文会切换:耗时的操作!),对于内存系统来说,如果没有上下文切换效率就是最高的,多次读写都是在一个 CPU 上的,在内存存储数据情况下,单线程就是最佳的方案。

3、五大数据类型


Redis 是一个开源(BSD 许可),内存存储的数据结构服务器,可用作数据库,高速缓存和消息队列代理。它支持字符串哈希表列表集合有序集合位图hyperloglogs等数据类型。内置复制、Lua 脚本、LRU 收回、事务以及不同级别磁盘持久化功能,同时通过 Redis Sentinel 提供高可用,通过 Redis Cluster 提供自动分区

3.1、Redis-key

在 redis 中无论什么数据类型,在数据库中都是以 key-value 形式保存,通过进行对 Redis-key 的操作,来完成对数据库中数据的操作。

下面学习的命令:

  • exists key:判断键是否存在
  • del key:删除键值对
  • move key db:将键值对移动到指定数据库
  • expire key second:设置键值对的过期时间
  • type key:查看 value 的数据类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
127.0.0.1:6379> keys * # 查看当前数据库所有key
(empty list or set)
127.0.0.1:6379> set name qinjiang # set key
OK
127.0.0.1:6379> set age 20
OK
127.0.0.1:6379> keys *
1) "age"
2) "name"
127.0.0.1:6379> move age 1 # 将键值对移动到指定数据库
(integer) 1
127.0.0.1:6379> EXISTS age # 判断键是否存在
(integer) 0 # 不存在
127.0.0.1:6379> EXISTS name
(integer) 1 # 存在
127.0.0.1:6379> SELECT 1
OK
127.0.0.1:6379[1]> keys *
1) "age"
127.0.0.1:6379[1]> del age # 删除键值对
(integer) 1 # 删除个数


127.0.0.1:6379> set age 20
OK
127.0.0.1:6379> EXPIRE age 15 # 设置键值对的过期时间

(integer) 1 # 设置成功 开始计数
127.0.0.1:6379> ttl age # 查看key的过期剩余时间
(integer) 13
127.0.0.1:6379> ttl age
(integer) 11
127.0.0.1:6379> ttl age
(integer) 9
127.0.0.1:6379> ttl age
(integer) -2 # -2 表示key过期,-1表示key未设置过期时间

127.0.0.1:6379> get age # 过期的key 会被自动delete
(nil)
127.0.0.1:6379> keys *
1) "name"

127.0.0.1:6379> type name # 查看value的数据类型
string

关于TTL命令

Redis 的 key,通过 TTL 命令返回 key 的过期时间,一般来说有 3 种:

  1. 当前 key 没有设置过期时间,所以会返回-1.
  2. 当前 key 有设置过期时间,而且 key 已经过期,所以会返回-2.
  3. 当前 key 有设置过期时间,且 key 还没有过期,故会返回 key 的正常剩余时间.

关于重命名RENAMERENAMENX

  • RENAME key newkey修改 key 的名称
  • RENAMENX key newkey仅当 newkey 不存在时,将 key 改名为 newkey 。

更多命令学习:https://www.redis.net.cn/order/

3.2、String(字符串)

普通的 set、get 直接略过。

命令 描述 示例
APPEND key value 向指定的 key 的 value 后追加字符串 127.0.0.1:6379> set msg hello OK 127.0.0.1:6379> append msg “ world” (integer) 11 127.0.0.1:6379> get msg “hello world”
DECR/INCR key 将指定 key 的 value 数值进行+1/-1(仅对于数字) 127.0.0.1:6379> set age 20 OK 127.0.0.1:6379> incr age (integer) 21 127.0.0.1:6379> decr age (integer) 20
INCRBY/DECRBY key n 按指定的步长对数值进行加减 127.0.0.1:6379> INCRBY age 5 (integer) 25 127.0.0.1:6379> DECRBY age 10 (integer) 15
INCRBYFLOAT key n 为数值加上浮点型数值 127.0.0.1:6379> INCRBYFLOAT age 5.2 “20.2”
STRLEN key 获取 key 保存值的字符串长度 127.0.0.1:6379> get msg “hello world” 127.0.0.1:6379> STRLEN msg (integer) 11
GETRANGE key start end 按起止位置获取字符串(闭区间,起止位置都取) 127.0.0.1:6379> get msg “hello world” 127.0.0.1:6379> GETRANGE msg 3 9 “lo worl”
SETRANGE key offset value 用指定的 value 替换 key 中 offset 开始的值 127.0.0.1:6379> SETRANGE msg 2 hello (integer) 7 127.0.0.1:6379> get msg “tehello”
GETSET key value 将给定 key 的值设为 value ,并返回 key 的旧值(old value)。 127.0.0.1:6379> GETSET msg test “hello world”
SETNX key value 仅当 key 不存在时进行 set 127.0.0.1:6379> SETNX msg test (integer) 0 127.0.0.1:6379> SETNX name sakura (integer) 1
SETEX key seconds value set 键值对并设置过期时间 127.0.0.1:6379> setex name 10 root OK 127.0.0.1:6379> get name (nil)
MSET key1 value1 [key2 value2..] 批量 set 键值对 127.0.0.1:6379> MSET k1 v1 k2 v2 k3 v3 OK
MSETNX key1 value1 [key2 value2..] 批量设置键值对,仅当参数中所有的 key 都不存在时执行 127.0.0.1:6379> MSETNX k1 v1 k4 v4 (integer) 0
MGET key1 [key2..] 批量获取多个 key 保存的值 127.0.0.1:6379> MGET k1 k2 k3 1) “v1” 2) “v2” 3) “v3”
PSETEX key milliseconds value 和 SETEX 命令相似,但它以毫秒为单位设置 key 的生存时间,
getset key value 如果不存在值,则返回 nil,如果存在值,获取原来的值,并设置新的值

如果不存在值,则返回 nil,如果存在值,获取原来的值,并设置新的值

String 类似的使用场景:value 除了是字符串还可以是数字,用途举例:

  • 计数器
  • 统计多单位的数量:uid:123666:follow 0
  • 粉丝数
  • 对象存储缓存

3.3、List(列表)

Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)

一个列表最多可以包含 232 - 1 个元素 (4294967295, 每个列表超过 40 亿个元素)。

image-20210517194417732

正如图 Redis 中 List 是可以进行双端操作的,所以命令也就分为了 LXXX 和 RLLL 两类,有时候 L 也表示 List 例如 LLEN

命令 描述
LPUSH/RPUSH key value1[value2..] 从左边/右边向列表中 PUSH 值(一个或者多个)。
LRANGE key start end 获取 list 起止元素==(索引从左往右 递增)==
LPUSHX/RPUSHX key value 向已存在的列名中 push 值(一个或者多个)
`LINSERT key BEFORE AFTER pivot value` 在指定列表元素的前/后 插入 value
LLEN key 查看列表长度
LINDEX key index 通过索引获取列表元素
LSET key index value 通过索引为元素设值
LPOP/RPOP key 从最左边/最右边移除值 并返回
RPOPLPUSH source destination 将列表的尾部(右)最后一个值弹出,并返回,然后加到另一个列表的头部
LTRIM key start end 通过下标截取指定范围内的列表
LREM key count value List 中是允许 value 重复的 count > 0:从头部开始搜索 然后删除指定的 value 至多删除 count 个 count < 0:从尾部开始搜索… count = 0:删除列表中所有的指定 value。
BLPOP/BRPOP key1[key2] timout 移出并获取列表的第一个/最后一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。
BRPOPLPUSH source destination timeout RPOPLPUSH功能相同,如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
---------------------------LPUSH---RPUSH---LRANGE--------------------------------

127.0.0.1:6379> LPUSH mylist k1 # LPUSH mylist=>{1}
(integer) 1
127.0.0.1:6379> LPUSH mylist k2 # LPUSH mylist=>{2,1}
(integer) 2
127.0.0.1:6379> RPUSH mylist k3 # RPUSH mylist=>{2,1,3}
(integer) 3
127.0.0.1:6379> get mylist # 普通的get是无法获取list值的
(error) WRONGTYPE Operation against a key holding the wrong kind of value
127.0.0.1:6379> LRANGE mylist 0 4 # LRANGE 获取起止位置范围内的元素
1) "k2"
2) "k1"
3) "k3"
127.0.0.1:6379> LRANGE mylist 0 2
1) "k2"
2) "k1"
3) "k3"
127.0.0.1:6379> LRANGE mylist 0 1
1) "k2"
2) "k1"
127.0.0.1:6379> LRANGE mylist 0 -1 # 获取全部元素
1) "k2"
2) "k1"
3) "k3"

---------------------------LPUSHX---RPUSHX-----------------------------------

127.0.0.1:6379> LPUSHX list v1 # list不存在 LPUSHX失败
(integer) 0
127.0.0.1:6379> LPUSHX list v1 v2
(integer) 0
127.0.0.1:6379> LPUSHX mylist k4 k5 # 向mylist中 左边 PUSH k4 k5
(integer) 5
127.0.0.1:6379> LRANGE mylist 0 -1
1) "k5"
2) "k4"
3) "k2"
4) "k1"
5) "k3"

---------------------------LINSERT--LLEN--LINDEX--LSET----------------------------

127.0.0.1:6379> LINSERT mylist after k2 ins_key1 # 在k2元素后 插入ins_key1
(integer) 6
127.0.0.1:6379> LRANGE mylist 0 -1
1) "k5"
2) "k4"
3) "k2"
4) "ins_key1"
5) "k1"
6) "k3"
127.0.0.1:6379> LLEN mylist # 查看mylist的长度
(integer) 6
127.0.0.1:6379> LINDEX mylist 3 # 获取下标为3的元素
"ins_key1"
127.0.0.1:6379> LINDEX mylist 0
"k5"
127.0.0.1:6379> LSET mylist 3 k6 # 将下标3的元素 set值为k6
OK
127.0.0.1:6379> LRANGE mylist 0 -1
1) "k5"
2) "k4"
3) "k2"
4) "k6"
5) "k1"
6) "k3"

---------------------------LPOP--RPOP--------------------------

127.0.0.1:6379> LPOP mylist # 左侧(头部)弹出
"k5"
127.0.0.1:6379> RPOP mylist # 右侧(尾部)弹出
"k3"

---------------------------RPOPLPUSH--------------------------

127.0.0.1:6379> LRANGE mylist 0 -1
1) "k4"
2) "k2"
3) "k6"
4) "k1"
127.0.0.1:6379> RPOPLPUSH mylist newlist # 将mylist的最后一个值(k1)弹出,加入到newlist的头部
"k1"
127.0.0.1:6379> LRANGE newlist 0 -1
1) "k1"
127.0.0.1:6379> LRANGE mylist 0 -1
1) "k4"
2) "k2"
3) "k6"

---------------------------LTRIM--------------------------

127.0.0.1:6379> LTRIM mylist 0 1 # 截取mylist中的 0~1部分
OK
127.0.0.1:6379> LRANGE mylist 0 -1
1) "k4"
2) "k2"

# 初始 mylist: k2,k2,k2,k2,k2,k2,k4,k2,k2,k2,k2
---------------------------LREM--------------------------

127.0.0.1:6379> LREM mylist 3 k2 # 从头部开始搜索 至多删除3个 k2
(integer) 3
# 删除后:mylist: k2,k2,k2,k4,k2,k2,k2,k2

127.0.0.1:6379> LREM mylist -2 k2 #从尾部开始搜索 至多删除2个 k2
(integer) 2
# 删除后:mylist: k2,k2,k2,k4,k2,k2


---------------------------BLPOP--BRPOP--------------------------

mylist: k2,k2,k2,k4,k2,k2
newlist: k1

127.0.0.1:6379> BLPOP newlist mylist 30 # 从newlist中弹出第一个值,mylist作为候选
1) "newlist" # 弹出
2) "k1"
127.0.0.1:6379> BLPOP newlist mylist 30
1) "mylist" # 由于newlist空了 从mylist中弹出
2) "k2"
127.0.0.1:6379> BLPOP newlist 30
(30.10s) # 超时了

127.0.0.1:6379> BLPOP newlist 30 # 我们连接另一个客户端向newlist中push了test, 阻塞被解决。
1) "newlist"
2) "test"
(12.54s)

小结

  • list 实际上是一个链表,before Node after , left, right 都可以插入值
  • 如果 key 不存在,则创建新的链表
  • 如果 key 存在,新增内容
  • 如果移除了所有值,空链表,也代表不存在
  • 在两边插入或者改动值,效率最高!修改中间元素,效率相对较低

应用:

消息排队!消息队列(Lpush Rpop),栈(Lpush Lpop)

3.4、Set(集合)

Redis 的 Set 是string 类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。

Redis 中 集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。

集合中最大的成员数为 232 - 1 (4294967295, 每个集合可存储 40 多亿个成员)。

命令 描述
SADD key member1[member2..] 向集合中无序增加一个/多个成员
SCARD key 获取集合的成员数
SMEMBERS key 返回集合中所有的成员
SISMEMBER key member 查询 member 元素是否是集合的成员,结果是无序的
SRANDMEMBER key [count] 随机返回集合中 count 个成员,count 缺省值为 1
SPOP key [count] 随机移除并返回集合中 count 个成员,count 缺省值为 1
SMOVE source destination member 将 source 集合的成员 member 移动到 destination 集合
SREM key member1[member2..] 移除集合中一个/多个成员
SDIFF key1[key2..] 返回所有集合的差集 key1- key2 - …
SDIFFSTORE destination key1[key2..] 在 SDIFF 的基础上,将结果保存到集合中==(覆盖)==。不能保存到其他类型 key 噢!
SINTER key1 [key2..] 返回所有集合的交集
SINTERSTORE destination key1[key2..] 在 SINTER 的基础上,存储结果到集合中。覆盖
SUNION key1 [key2..] 返回所有集合的并集
SUNIONSTORE destination key1 [key2..] 在 SUNION 的基础上,存储结果到及和张。覆盖
SSCAN KEY [MATCH pattern] [COUNT count] 在大量数据环境下,使用此命令遍历集合中元素,每次遍历部分
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
---------------SADD--SCARD--SMEMBERS--SISMEMBER--------------------

127.0.0.1:6379> SADD myset m1 m2 m3 m4 # 向myset中增加成员 m1~m4
(integer) 4
127.0.0.1:6379> SCARD myset # 获取集合的成员数目
(integer) 4
127.0.0.1:6379> smembers myset # 获取集合中所有成员
1) "m4"
2) "m3"
3) "m2"
4) "m1"
127.0.0.1:6379> SISMEMBER myset m5 # 查询m5是否是myset的成员
(integer) 0 # 不是,返回0
127.0.0.1:6379> SISMEMBER myset m2
(integer) 1 # 是,返回1
127.0.0.1:6379> SISMEMBER myset m3
(integer) 1

---------------------SRANDMEMBER--SPOP----------------------------------

127.0.0.1:6379> SRANDMEMBER myset 3 # 随机返回3个成员
1) "m2"
2) "m3"
3) "m4"
127.0.0.1:6379> SRANDMEMBER myset # 随机返回1个成员
"m3"
127.0.0.1:6379> SPOP myset 2 # 随机移除并返回2个成员
1) "m1"
2) "m4"
# 将set还原到{m1,m2,m3,m4}

---------------------SMOVE--SREM----------------------------------------

127.0.0.1:6379> SMOVE myset newset m3 # 将myset中m3成员移动到newset集合
(integer) 1
127.0.0.1:6379> SMEMBERS myset
1) "m4"
2) "m2"
3) "m1"
127.0.0.1:6379> SMEMBERS newset
1) "m3"
127.0.0.1:6379> SREM newset m3 # 从newset中移除m3元素
(integer) 1
127.0.0.1:6379> SMEMBERS newset
(empty list or set)

# 下面开始是多集合操作,多集合操作中若只有一个参数默认和自身进行运算
# setx=>{m1,m2,m4,m6}, sety=>{m2,m5,m6}, setz=>{m1,m3,m6}

-----------------------------SDIFF------------------------------------

127.0.0.1:6379> SDIFF setx sety setz # 等价于setx-sety-setz
1) "m4"
127.0.0.1:6379> SDIFF setx sety # setx - sety
1) "m4"
2) "m1"
127.0.0.1:6379> SDIFF sety setx # sety - setx
1) "m5"


-------------------------SINTER---------------------------------------
# 共同关注(交集)

127.0.0.1:6379> SINTER setx sety setz # 求 setx、sety、setx的交集
1) "m6"
127.0.0.1:6379> SINTER setx sety # 求setx sety的交集
1) "m2"
2) "m6"

-------------------------SUNION---------------------------------------

127.0.0.1:6379> SUNION setx sety setz # setx sety setz的并集
1) "m4"
2) "m6"
3) "m3"
4) "m2"
5) "m1"
6) "m5"
127.0.0.1:6379> SUNION setx sety # setx sety 并集
1) "m4"
2) "m6"
3) "m2"
4) "m1"
5) "m5"

3.5、Hash(哈希)

Redis hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象。

Set 就是一种简化的 Hash,只变动 key,而 value 使用默认值填充。可以将一个 Hash 表作为一个对象进行存储,表中存放对象的信息。

命令 描述
HSET key field value 将哈希表 key 中的字段 field 的值设为 value 。重复设置同一个 field 会覆盖,返回 0
HMSET key field1 value1 [field2 value2..] 同时将多个 field-value (域-值)对设置到哈希表 key 中。
HSETNX key field value 只有在字段 field 不存在时,设置哈希表字段的值。
HEXISTS key field 查看哈希表 key 中,指定的字段是否存在。
HGET key field value 获取存储在哈希表中指定字段的值
HMGET key field1 [field2..] 获取所有给定字段的值
HGETALL key 获取在哈希表 key 的所有字段和值
HKEYS key 获取哈希表 key 中所有的字段
HLEN key 获取哈希表中字段的数量
HVALS key 获取哈希表中所有值
HDEL key field1 [field2..] 删除哈希表 key 中一个/多个 field 字段
HINCRBY key field n 为哈希表 key 中的指定字段的整数值加上增量 n,并返回增量后结果 一样只适用于整数型字段
HINCRBYFLOAT key field n 为哈希表 key 中的指定字段的浮点数值加上增量 n。
HSCAN key cursor [MATCH pattern] [COUNT count] 迭代哈希表中的键值对。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
------------------------HSET--HMSET--HSETNX----------------
127.0.0.1:6379> HSET studentx name sakura # 将studentx哈希表作为一个对象,设置name为sakura
(integer) 1
127.0.0.1:6379> HSET studentx name gyc # 重复设置field进行覆盖,并返回0
(integer) 0
127.0.0.1:6379> HSET studentx age 20 # 设置studentx的age为20
(integer) 1
127.0.0.1:6379> HMSET studentx sex 1 tel 15623667886 # 设置sex为1,tel为15623667886
OK
127.0.0.1:6379> HSETNX studentx name gyc # HSETNX 设置已存在的field
(integer) 0 # 失败
127.0.0.1:6379> HSETNX studentx email 12345@qq.com
(integer) 1 # 成功

----------------------HEXISTS--------------------------------
127.0.0.1:6379> HEXISTS studentx name # name字段在studentx中是否存在
(integer) 1 # 存在
127.0.0.1:6379> HEXISTS studentx addr
(integer) 0 # 不存在

-------------------HGET--HMGET--HGETALL-----------
127.0.0.1:6379> HGET studentx name # 获取studentx中name字段的value
"gyc"
127.0.0.1:6379> HMGET studentx name age tel # 获取studentx中name、age、tel字段的value
1) "gyc"
2) "20"
3) "15623667886"
127.0.0.1:6379> HGETALL studentx # 获取studentx中所有的field及其value
1) "name"
2) "gyc"
3) "age"
4) "20"
5) "sex"
6) "1"
7) "tel"
8) "15623667886"
9) "email"
10) "12345@qq.com"


--------------------HKEYS--HLEN--HVALS--------------
127.0.0.1:6379> HKEYS studentx # 查看studentx中所有的field
1) "name"
2) "age"
3) "sex"
4) "tel"
5) "email"
127.0.0.1:6379> HLEN studentx # 查看studentx中的字段数量
(integer) 5
127.0.0.1:6379> HVALS studentx # 查看studentx中所有的value
1) "gyc"
2) "20"
3) "1"
4) "15623667886"
5) "12345@qq.com"

-------------------------HDEL--------------------------
127.0.0.1:6379> HDEL studentx sex tel # 删除studentx 中的sex、tel字段
(integer) 2
127.0.0.1:6379> HKEYS studentx
1) "name"
2) "age"
3) "email"

-------------HINCRBY--HINCRBYFLOAT------------------------
127.0.0.1:6379> HINCRBY studentx age 1 # studentx的age字段数值+1
(integer) 21
127.0.0.1:6379> HINCRBY studentx name 1 # 非整数字型字段不可用
(error) ERR hash value is not an integer
127.0.0.1:6379> HINCRBYFLOAT studentx weight 0.6 # weight字段增加0.6
"90.8"

Hash 变更的数据 user name age,尤其是用户信息之类的,经常变动的信息!Hash 更适合于对象的存储,Sring 更加适合字符串存储!

3.6、Zset(有序集合)

不同的是每个元素都会关联一个 double 类型的分数(score)。redis 正是通过分数来为集合中的成员进行从小到大的排序。

score 相同:按字典顺序排序

有序集合的成员是唯一的,但分数(score)却可以重复。

命令 描述
ZADD key score member1 [score2 member2] 向有序集合添加一个或多个成员,或者更新已存在成员的分数
ZCARD key 获取有序集合的成员数
ZCOUNT key min max 计算在有序集合中指定区间 score 的成员数
ZINCRBY key n member 有序集合中对指定成员的分数加上增量 n
ZSCORE key member 返回有序集中,成员的分数值
ZRANK key member 返回有序集合中指定成员的索引
ZRANGE key start end 通过索引区间返回有序集合成指定区间内的成员
ZRANGEBYLEX key min max 通过字典区间返回有序集合的成员
ZRANGEBYSCORE key min max 通过分数返回有序集合指定区间内的成员==-inf 和 +inf 分别表示最小最大值,只支持开区间()==
ZLEXCOUNT key min max 在有序集合中计算指定字典区间内成员数量
ZREM key member1 [member2..] 移除有序集合中一个/多个成员
ZREMRANGEBYLEX key min max 移除有序集合中给定的字典区间的所有成员
ZREMRANGEBYRANK key start stop 移除有序集合中给定的排名区间的所有成员
ZREMRANGEBYSCORE key min max 移除有序集合中给定的分数区间的所有成员
ZREVRANGE key start end 返回有序集中指定区间内的成员,通过索引,分数从高到底
ZREVRANGEBYSCORRE key max min 返回有序集中指定分数区间内的成员,分数从高到低排序
ZREVRANGEBYLEX key max min 返回有序集中指定字典区间内的成员,按字典顺序倒序
ZREVRANK key member 返回有序集合中指定成员的排名,有序集成员按分数值递减(从大到小)排序
ZINTERSTORE destination numkeys key1 [key2 ..] 计算给定的一个或多个有序集的交集并将结果集存储在新的有序集合 key 中,numkeys:表示参与运算的集合数,将 score 相加作为结果的 score
ZUNIONSTORE destination numkeys key1 [key2..] 计算给定的一个或多个有序集的交集并将结果集存储在新的有序集合 key 中
ZSCAN key cursor [MATCH pattern\] [COUNT count] 迭代有序集合中的元素(包括元素成员和元素分值)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
-------------------ZADD--ZCARD--ZCOUNT--------------
127.0.0.1:6379> ZADD myzset 1 m1 2 m2 3 m3 # 向有序集合myzset中添加成员m1 score=1 以及成员m2 score=2..
(integer) 2
127.0.0.1:6379> ZCARD myzset # 获取有序集合的成员数
(integer) 2
127.0.0.1:6379> ZCOUNT myzset 0 1 # 获取score在 [0,1]区间的成员数量
(integer) 1
127.0.0.1:6379> ZCOUNT myzset 0 2
(integer) 2

----------------ZINCRBY--ZSCORE--------------------------
127.0.0.1:6379> ZINCRBY myzset 5 m2 # 将成员m2的score +5
"7"
127.0.0.1:6379> ZSCORE myzset m1 # 获取成员m1的score
"1"
127.0.0.1:6379> ZSCORE myzset m2
"7"

--------------ZRANK--ZRANGE-----------------------------------
127.0.0.1:6379> ZRANK myzset m1 # 获取成员m1的索引,索引按照score排序,score相同索引值按字典顺序顺序增加
(integer) 0
127.0.0.1:6379> ZRANK myzset m2
(integer) 2
127.0.0.1:6379> ZRANGE myzset 0 1 # 获取索引在 0~1的成员
1) "m1"
2) "m3"
127.0.0.1:6379> ZRANGE myzset 0 -1 # 获取全部成员
1) "m1"
2) "m3"
3) "m2"

#testset=>{abc,add,amaze,apple,back,java,redis} score均为0
------------------ZRANGEBYLEX---------------------------------
127.0.0.1:6379> ZRANGEBYLEX testset - + # 返回所有成员
1) "abc"
2) "add"
3) "amaze"
4) "apple"
5) "back"
6) "java"
7) "redis"
127.0.0.1:6379> ZRANGEBYLEX testset - + LIMIT 0 3 # 分页 按索引显示查询结果的 0,1,2条记录
1) "abc"
2) "add"
3) "amaze"
127.0.0.1:6379> ZRANGEBYLEX testset - + LIMIT 3 3 # 显示 3,4,5条记录
1) "apple"
2) "back"
3) "java"
127.0.0.1:6379> ZRANGEBYLEX testset (- [apple # 显示 (-,apple] 区间内的成员
1) "abc"
2) "add"
3) "amaze"
4) "apple"
127.0.0.1:6379> ZRANGEBYLEX testset [apple [java # 显示 [apple,java]字典区间的成员
1) "apple"
2) "back"
3) "java"

-----------------------ZRANGEBYSCORE---------------------
127.0.0.1:6379> ZRANGEBYSCORE myzset 1 10 # 返回score在 [1,10]之间的的成员
1) "m1"
2) "m3"
3) "m2"
127.0.0.1:6379> ZRANGEBYSCORE myzset 1 5
1) "m1"
2) "m3"

--------------------ZLEXCOUNT-----------------------------
127.0.0.1:6379> ZLEXCOUNT testset - +
(integer) 7
127.0.0.1:6379> ZLEXCOUNT testset [apple [java
(integer) 3

------------------ZREM--ZREMRANGEBYLEX--ZREMRANGBYRANK--ZREMRANGEBYSCORE--------------------------------
127.0.0.1:6379> ZREM testset abc # 移除成员abc
(integer) 1
127.0.0.1:6379> ZREMRANGEBYLEX testset [apple [java # 移除字典区间[apple,java]中的所有成员
(integer) 3
127.0.0.1:6379> ZREMRANGEBYRANK testset 0 1 # 移除排名0~1的所有成员
(integer) 2
127.0.0.1:6379> ZREMRANGEBYSCORE myzset 0 3 # 移除score在 [0,3]的成员
(integer) 2


# testset=> {abc,add,apple,amaze,back,java,redis} score均为0
# myzset=> {(m1,1),(m2,2),(m3,3),(m4,4),(m7,7),(m9,9)}
----------------ZREVRANGE--ZREVRANGEBYSCORE--ZREVRANGEBYLEX-----------
127.0.0.1:6379> ZREVRANGE myzset 0 3 # 按score递减排序,然后按索引,返回结果的 0~3
1) "m9"
2) "m7"
3) "m4"
4) "m3"
127.0.0.1:6379> ZREVRANGE myzset 2 4 # 返回排序结果的 索引的2~4
1) "m4"
2) "m3"
3) "m2"
127.0.0.1:6379> ZREVRANGEBYSCORE myzset 6 2 # 按score递减顺序 返回集合中分数在[2,6]之间的成员
1) "m4"
2) "m3"
3) "m2"
127.0.0.1:6379> ZREVRANGEBYLEX testset [java (add # 按字典倒序 返回集合中(add,java]字典区间的成员
1) "java"
2) "back"
3) "apple"
4) "amaze"

-------------------------ZREVRANK------------------------------
127.0.0.1:6379> ZREVRANK myzset m7 # 按score递减顺序,返回成员m7索引
(integer) 1
127.0.0.1:6379> ZREVRANK myzset m2
(integer) 4


# mathscore=>{(xm,90),(xh,95),(xg,87)} 小明、小红、小刚的数学成绩
# enscore=>{(xm,70),(xh,93),(xg,90)} 小明、小红、小刚的英语成绩
-------------------ZINTERSTORE--ZUNIONSTORE-----------------------------------
127.0.0.1:6379> ZINTERSTORE sumscore 2 mathscore enscore # 将mathscore enscore进行合并 结果存放到sumscore
(integer) 3
127.0.0.1:6379> ZRANGE sumscore 0 -1 withscores # 合并后的score是之前集合中所有score的和
1) "xm"
2) "160"
3) "xg"
4) "177"
5) "xh"
6) "188"

127.0.0.1:6379> ZUNIONSTORE lowestscore 2 mathscore enscore AGGREGATE MIN # 取两个集合的成员score最小值作为结果的
(integer) 3
127.0.0.1:6379> ZRANGE lowestscore 0 -1 withscores
1) "xm"
2) "70"
3) "xg"
4) "87"
5) "xh"
6) "93"

应用案例:

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

4、三种特殊数据类型


4.1、Geospatial(地理位置)

使用经纬度定位地理坐标并用一个有序集合 zset 保存,所以 zset 命令也可以使用

命令 描述
geoadd key longitud(经度) latitude(纬度) member [..] 将具体经纬度的坐标存入一个有序集合
geopos key member [member..] 获取集合中的一个/多个成员坐标
geodist key member1 member2 [unit] 返回两个给定位置之间的距离。默认以米作为单位。
`georadius key longitude latitude radius m km mi ft [WITHCOORD][WITHDIST] [WITHHASH] [COUNT count]` 以给定的经纬度为中心, 返回集合包含的位置元素当中, 与中心的距离不超过给定最大距离的所有位置元素。
GEORADIUSBYMEMBER key member radius... 功能与 GEORADIUS 相同,只是中心位置不是具体的经纬度,而是使用结合中已有的成员作为中心点。
geohash key member1 [member2..] 返回一个或多个位置元素的 Geohash 表示。使用 Geohash 位置 52 点整数编码。

有效经纬度

  • 有效的经度从-180 度到 180 度。
  • 有效的纬度从-85.05112878 度到 85.05112878 度。

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

  • m 表示单位为米。
  • km 表示单位为千米。
  • mi 表示单位为英里。
  • ft 表示单位为英尺。

关于 GEORADIUS 的参数

通过georadius就可以完成 附近的人功能

withcoord:带上坐标

withdist:带上距离,单位与半径单位相同

COUNT n : 只显示前 n 个(按距离递增排序)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
----------------georadius---------------------
127.0.0.1:6379> GEORADIUS china:city 120 30 500 km withcoord withdist # 查询经纬度(120,30)坐标500km半径内的成员
1) 1) "hangzhou"
2) "29.4151"
3) 1) "120.20000249147415"
2) "30.199999888333501"
2) 1) "shanghai"
2) "205.3611"
3) 1) "121.40000134706497"
2) "31.400000253193539"

------------geohash---------------------------
127.0.0.1:6379> geohash china:city yichang shanghai # 获取成员经纬坐标的geohash表示
1) "wmrjwbr5250"
2) "wtw6ds0y300"

4.2、Hyperloglog(基数统计)

Redis HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。

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

因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。

其底层使用 string 数据类型

什么是基数?

数据集中不重复的元素的个数。

应用场景:

网页的访问量(UV):一个用户多次访问,也只能算作一个人。

传统实现,存储用户的 id,然后每次进行比较。当用户变多之后这种方式及其浪费空间,而我们的目的只是计数,Hyperloglog 就能帮助我们利用最小的空间完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
----------PFADD--PFCOUNT---------------------
127.0.0.1:6379> PFADD myelemx a b c d e f g h i j k # 添加元素
(integer) 1
127.0.0.1:6379> type myelemx # hyperloglog底层使用String
string
127.0.0.1:6379> PFCOUNT myelemx # 估算myelemx的基数
(integer) 11
127.0.0.1:6379> PFADD myelemy i j k z m c b v p q s
(integer) 1
127.0.0.1:6379> PFCOUNT myelemy
(integer) 11

----------------PFMERGE-----------------------
127.0.0.1:6379> PFMERGE myelemz myelemx myelemy # 合并myelemx和myelemy 成为myelemz
OK
127.0.0.1:6379> PFCOUNT myelemz # 估算基数
(integer) 17

如果允许容错,那么一定可以使用 Hyperloglog !

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

4.3、BitMaps(位图)

使用位存储,信息状态只有 0 和 1

Bitmap 是一串连续的 2 进制数字(0 或 1),每一位所在的位置为偏移(offset),在 bitmap 上可执行 AND,OR,XOR,NOT 以及其它位操作。

应用场景

签到统计、状态统计

命令 描述
setbit key offset value 为指定 key 的 offset 位设置值
getbit key offset 获取 offset 位的值
bitcount key [start end] 统计字符串被设置为 1 的 bit 数,也可以指定统计范围按字节
bitop operration destkey key[key..] 对一个或多个保存二进制位的字符串 key 进行位元操作,并将结果保存到 destkey 上。
BITPOS key bit [start] [end] 返回字符串里面第一个被设置为 1 或者 0 的 bit 位。start 和 end 只能按字节,不能按位
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
------------setbit--getbit--------------
127.0.0.1:6379> setbit sign 0 1 # 设置sign的第0位为 1
(integer) 0
127.0.0.1:6379> setbit sign 2 1 # 设置sign的第2位为 1 不设置默认 是0
(integer) 0
127.0.0.1:6379> setbit sign 3 1
(integer) 0
127.0.0.1:6379> setbit sign 5 1
(integer) 0
127.0.0.1:6379> type sign
string

127.0.0.1:6379> getbit sign 2 # 获取第2位的数值
(integer) 1
127.0.0.1:6379> getbit sign 3
(integer) 1
127.0.0.1:6379> getbit sign 4 # 未设置默认是0
(integer) 0

-----------bitcount----------------------------
127.0.0.1:6379> BITCOUNT sign # 统计sign中为1的位数
(integer) 4

bitmaps 的底层

这样设置以后你能 get 到的值是:\xA2\x80,所以 bitmaps 是一串从左到右的二进制串

5、事务


Redis 的单条命令是保证原子性的,但是 redis 事务不能保证原子性

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

————————- 队列 set set set 执行 —————————-

事务中每条命令都会被序列化,执行过程中按顺序执行,不允许其他命令进行干扰。

  • 一次性
  • 顺序性
  • 排他性

  1. Redis 事务没有隔离级别的概念
  2. Redis 单条命令是保证原子性的,但是事务不保证原子性!

Redis 事务操作过程

  • 开启事务(multi
  • 命令入队
  • 执行事务(exec

所以事务中的命令在加入时都没有被执行,直到提交时才会开始执行(Exec)一次性完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 k1
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> keys *
QUEUED
127.0.0.1:6379> exec # 事务执行
1) OK
2) OK
3) "v1"
4) OK
5) 1) "k3"
2) "k2"
3) "k1"

取消事务(discurd)

1
2
3
4
5
6
7
8
9
10
11
12
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> DISCARD # 放弃事务
OK
127.0.0.1:6379> EXEC
(error) ERR EXEC without MULTI # 当前未开启事务
127.0.0.1:6379> get k1 # 被放弃事务中命令并未执行
(nil)

事务错误

代码语法错误(编译时异常)所有的命令都不执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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> error k1 # 这是一条语法错误命令
(error) ERR unknown command `error`, with args beginning with: `k1`, # 会报错但是不影响后续命令入队
127.0.0.1:6379> get k2
QUEUED
127.0.0.1:6379> EXEC
(error) EXECABORT Transaction discarded because of previous errors. # 执行报错
127.0.0.1:6379> get k1
(nil) # 其他命令并没有被执行

代码逻辑错误 (运行时异常) 其他命令可以正常执行 >>> 所以不保证事务原子性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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> INCR k1 # 这条命令逻辑错误(对字符串进行增量)
QUEUED
127.0.0.1:6379> get k2
QUEUED
127.0.0.1:6379> exec
1) OK
2) OK
3) (error) ERR value is not an integer or out of range # 运行时报错
4) "v2" # 其他命令正常执行

# 虽然中间有一条命令报错了,但是后面的指令依旧正常执行成功了。
# 所以说Redis单条指令保证原子性,但是Redis事务不能保证原子性。

监控

悲观锁:

  • 很悲观,认为什么时候都会出现问题,无论做什么都会加锁

乐观锁:

  • 很乐观,认为什么时候都不会出现问题,所以不会上锁!更新数据的时候去判断一下,在此期间是否有人修改过这个数据
  • 获取 version
  • 更新的时候比较 version

使用watch key监控指定数据,相当于乐观锁加锁。

正常执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
127.0.0.1:6379> set money 100 # 设置余额:100
OK
127.0.0.1:6379> set use 0 # 支出使用: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 use 20
QUEUED
127.0.0.1:6379> exec # 监视值没有被中途修改,事务正常执行
1) (integer) 80
2) (integer) 20

测试多线程修改值,使用 watch 可以当做 redis 的乐观锁操作(相当于 getversion)

我们启动另外一个客户端模拟插队线程。

线程 1:

1
2
3
4
5
6
7
8
9
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 use 20
QUEUED
127.0.0.1:6379> # 此时事务并没有执行

模拟线程插队,线程 2:

1
2
127.0.0.1:6379> INCRBY money 500 # 修改了线程一中监视的money
(integer) 600

回到线程 1,执行事务:

1
2
3
4
5
6
7
127.0.0.1:6379> EXEC # 执行之前,另一个线程修改了我们的值,这个时候就会导致事务执行失败
(nil) # 没有结果,说明事务执行失败

127.0.0.1:6379> get money # 线程2 修改生效
"600"
127.0.0.1:6379> get use # 线程1事务执行失败,数值没有被修改
"0"

解锁获取最新值,然后再加锁进行事务。

unwatch进行解锁。

注意:每次提交执行 exec 后都会自动释放锁,不管是否成功

6、Jedis


使用 Java 来操作 Redis,Jedis 是 Redis 官方推荐使用的 Java 连接 redis 的客户端。

  1. 导入依赖
1
2
3
4
5
6
7
8
9
10
11
12
<!--导入jredis的包-->
<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.70</version>
</dependency>

2.编码测试

  • 连接数据库

    1. 修改 redis 的配置文件

    image-20210519210942712

    将只绑定本地注释

    保护模式改为 no

image-20210519210805714

​ 开放端口 6379

1
firewall-cmd --zone=public --add-port=6379/tcp --permanet

​ 重启防火墙服务

1
systemctl restart firewalld.service

​ 重启 redis-server

1
redis-server huangconfig/redis.conf

​ 操作命令

​ TestPing.java

1
2
3
4
5
6
7
public class TestPing {
public static void main(String[] args) {
Jedis jedis = new Jedis("192.168.xx.xxx", 6379);
String response = jedis.ping();
System.out.println(response); // PONG
}
}

​ 事务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class TestTX {
public static void main(String[] args) {
Jedis jedis = new Jedis("39.99.xxx.xx", 6379);

JSONObject jsonObject = new JSONObject();
jsonObject.put("hello", "world");
jsonObject.put("name", "kuangshen");
// 开启事务
Transaction multi = jedis.multi();
String result = jsonObject.toJSONString();
// jedis.watch(result)
try {
multi.set("user1", result);
multi.set("user2", result);
// 执行事务
multi.exec();
}catch (Exception e){
// 放弃事务
multi.discard();
} finally {
// 关闭连接
System.out.println(jedis.get("user1"));
System.out.println(jedis.get("user2"));
jedis.close();
}
}
}

7、SpringBoot 整合


1.导入依赖

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

springboot 2.x 后 ,原来使用的 Jedis 被 lettuce 替换。

jedis:采用的直连,多个线程操作的话,是不安全的。如果要避免不安全,使用 jedis pool 连接池!更像 BIO 模式

lettuce:采用 netty,实例可以在多个线程中共享,不存在线程不安全的情况!可以减少线程数据了,更像 NIO 模式

我们在学习 SpringBoot 自动配置的原理时,整合一个组件并进行配置一定会有一个自动配置类 xxxAutoConfiguration,并且在 spring.factories 中也一定能找到这个类的完全限定名。Redis 也不例外。

image-20210519212123458

那么就一定还存在一个 RedisProperties 类

image-20210519212159624

之前我们说 SpringBoot2.x 后默认使用 Lettuce 来替换 Jedis,现在我们就能来验证了。

先看 Jedis:

image-20210519212229658

@ConditionalOnClass 注解中有两个类是默认不存在的,所以 Jedis 是无法生效的

然后再看 Lettuce:

image-20210519212252762

完美生效。

现在我们回到 RedisAutoConfiguratio

image-20210519212322212

只有两个简单的 Bean

  • RedisTemplate
  • StringRedisTemplate

当看到 xxTemplate 时可以对比 RestTemplat、SqlSessionTemplate,通过使用这些 Template 来间接操作组件。那么这俩也不会例外。分别用于操作 Redis 和 Redis 中的 String 数据类型。

在 RedisTemplate 上也有一个条件注解,说明我们是可以对其进行定制化的

说完这些,我们需要知道如何编写配置文件然后连接 Redis,就需要阅读 RedisProperties

image-20210519212404385

这是一些基本的配置属性。

image-20210519212449770

还有一些连接池相关的配置。注意使用时一定使用 Lettuce 的连接池。

image-20210519212510909

编写配置文件

1
2
3
4
#Springboot所有的配置类,都有一个自动配置类
#自动配置类都会绑定一个properties
spring.redis.host=192.168.222.130
spring.redis.port=6379

使用 RedisTemplate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@SpringBootTest
class Redis02SpringbootApplicationTests {

@Autowired
private RedisTemplate redisTemplate;

@Test
void contextLoads() {

// redisTemplate 操作不同的数据类型,api和我们的指令是一样的
// opsForValue 操作字符串 类似String
// opsForList 操作List 类似List
// opsForHah

// 除了基本的操作,我们常用的方法都可以直接通过redisTemplate操作,比如事务和基本的CRUD

// 获取连接对象
//RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
//connection.flushDb();
//connection.flushAll();

redisTemplate.opsForValue().set("mykey","kuangshen");
System.out.println(redisTemplate.opsForValue().get("mykey"));
}
}

测试结果:

此时我们回到 Redis 查看数据时候,惊奇发现全是乱码,可是程序中可以正常输出:

image-20210519212735113

这时候就关系到存储对象的序列化问题,在网络中传输的对象也是一样需要序列化,否者就全是乱码。

我们转到看那个默认的 RedisTemplate 内部什么样子:

image-20210519212822958

在最开始就能看到几个关于序列化的参数。

默认的序列化器是采用 JDK 序列化器

image-20210519212838577

而默认的 RedisTemplate 中的所有序列化器都是使用这个序列化器:

image-20210519212904729

后续我们定制 RedisTemplate 就可以对其进行修改。

RedisSerializer提供了多种序列化方案:

    • 直接调用 RedisSerializer 的静态方法来返回序列化器,然后 set
    • image-20210519212934410
    • 自己 new 相应的实现类,然后 set
    • image-20210519213011690
  1. 定制 RedisTemplate 的模板:

  2. 我们创建一个 Bean 加入容器,就会触发 RedisTemplate 上的条件注解使默认的 RedisTemplate 失效。

  3. ```java
    @Configuration
    public class RedisConfig {

    @Bean

    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
        // 将template 泛型设置为 <String, Object>
        RedisTemplate<String, Object> template = new RedisTemplate();
        // 连接工厂,不必修改
        template.setConnectionFactory(redisConnectionFactory);
        /*
         * 序列化设置
         */
        // key、hash的key 采用 String序列化方式
        template.setKeySerializer(RedisSerializer.string());
        template.setHashKeySerializer(RedisSerializer.string());
        // value、hash的value 采用 Jackson 序列化方式
        template.setValueSerializer(RedisSerializer.json());
        template.setHashValueSerializer(RedisSerializer.json());
        template.afterPropertiesSet();
    
        return template;
    }
    

    }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    300
    301
    302
    303
    304
    305
    306
    307
    308
    309
    310
    311
    312
    313
    314
    315
    316
    317
    318
    319
    320
    321
    322
    323
    324
    325
    326
    327
    328
    329
    330
    331
    332
    333
    334
    335
    336
    337
    338
    339
    340
    341
    342
    343
    344
    345
    346
    347
    348
    349
    350
    351
    352
    353
    354
    355
    356
    357
    358
    359
    360
    361
    362
    363
    364
    365
    366
    367
    368
    369
    370
    371
    372
    373
    374
    375
    376
    377
    378
    379
    380
    381
    382
    383
    384
    385
    386
    387
    388
    389
    390
    391
    392
    393
    394
    395
    396
    397
    398
    399
    400
    401
    402
    403
    404
    405
    406
    407
    408
    409
    410
    411
    412
    413
    414
    415
    416
    417
    418
    419
    420
    421
    422
    423
    424
    425
    426
    427
    428
    429
    430
    431
    432
    433
    434
    435
    436
    437
    438
    439
    440
    441
    442
    443
    444
    445
    446
    447
    448
    449
    450
    451
    452
    453
    454
    455
    456
    457
    458
    459
    460
    461
    462
    463
    464
    465
    466
    467
    468
    469
    470
    471
    472
    473
    474
    475
    476
    477
    478
    479
    480
    481
    482
    483
    484
    485
    486
    487
    488
    489
    490
    491
    492
    493
    494
    495
    496
    497
    498
    499
    500
    501
    502
    503
    504
    505
    506
    507
    508
    509
    510
    511
    512
    513
    514
    515
    516
    517
    518
    519
    520
    521
    522
    523
    524
    525
    526
    527
    528
    529
    530
    531
    532
    533
    534
    535
    536
    537
    538
    539
    540
    541
    542
    543
    544
    545
    546
    547
    548
    549
    550
    551
    552
    553
    554
    555
    556
    557
    558
    559
    560
    561
    562
    563
    564
    565
    566
    567
    568
    569
    570
    571
    572
    573
    574
    575
    576
    577
    578
    579
    580
    581
    582
    583
    584
    585
    586
    587
    588
    589
    590
    591
    592
    593
    594
    595
    596
    597
    598
    599
    600
    601
    602
    603
    604
    605
    606
    607
    608
    609
    610
    611
    612
    613
    614
    615
    616
    617
    618
    619
    620
    621
    622
    623
    624
    625
    626
    627
    628
    629
    630
    631
    632
    633
    634
    635
    636
    637
    638
    639
    640
    641
    642
    643
    644
    645
    646
    647
    648
    649
    650
    651
    652
    653
    654
    655
    656
    657
    658
    659
    660
    661
    662
    663
    664
    665
    666
    667
    668
    669
    670
    671
    672
    673
    674
    675
    676
    677
    678
    679
    680
    681
    682
    683
    684
    685
    686
    687
    688
    689
    690
    691
    692
    693
    694
    695
    696
    697
    698
    699
    700
    701
    702
    703
    704
    705
    706
    707
    708
    709
    710
    711
    712
    713
    714
    715
    716
    717
    718
    719
    720
    721
    722
    723
    724
    725
    726
    727
    728
    729
    730
    731
    732
    733
    734
    735
    736
    737
    738
    739
    740
    741
    742
    743
    744
    745
    746
    747
    748
    749
    750
    751
    752
    753
    754
    755
    756
    757
    758
    759
    760
    761
    762
    763
    764
    765
    766
    767
    768
    769
    770
    771
    772
    773
    774
    775
    776
    777
    778
    779
    780
    781
    782
    783
    784
    785
    786
    787
    788
    789
    790
    791
    792
    793
    794
    795
    796
    797
    798
    799
    800
    801
    802
    803
    804
    805
    806
    807
    808
    809
    810
    811
    812
    813
    814
    815
    816
    817
    818
    819
    820
    821
    822
    823
    824
    825
    826
    827
    828
    829
    830
    831
    832
    833
    834
    835
    836
    837
    838
    839
    840
    841
    842
    843
    844
    845
    846
    847
    848
    849
    850
    851
    852
    853
    854
    855
    856
    857
    858
    859
    860
    861
    862
    863
    864
    865
    866
    867
    868
    869
    870
    871
    872
    873
    874
    875
    876
    877
    878
    879
    880
    881
    882
    883
    884
    885
    886
    887
    888
    889
    890
    891
    892
    893
    894
    895
    896
    897
    898
    899
    900
    901
    902
    903
    904
    905
    906
    907
    908
    909
    910
    911
    912
    913
    914
    915
    916
    917
    918
    919
    920
    921
    922
    923
    924
    925
    926
    927
    928
    929
    930
    931
    932
    933
    934
    935
    936
    937
    938
    939
    940
    941
    942
    943
    944
    945
    946
    947
    948
    949
    950
    951
    952
    953
    954
    955
    956
    957
    958
    959
    960
    961
    962
    963
    964
    965
    966
    967
    968
    969
    970
    971
    972
    973
    974
    975
    976
    977
    978
    979
    980
    981
    982
    983
    984
    985
    986
    987
    988
    989
    990
    991
    992
    993
    994
    995
    996
    997
    998
    999
    1000
    1001
    1002
    1003
    1004
    1005
    1006
    1007
    1008
    1009
    1010
    1011
    1012
    1013
    1014
    1015
    1016
    1017
    1018
    1019
    1020
    1021
    1022
    1023
    1024
    1025
    1026
    1027
    1028
    1029
    1030
    1031
    1032
    1033
    1034
    1035
    1036
    1037
    1038
    1039
    1040
    1041
    1042
    1043
    1044
    1045
    1046
    1047
    1048
    1049
    1050
    1051
    1052
    1053
    1054
    1055
    1056
    1057
    1058
    1059
    1060
    1061
    1062
    1063
    1064
    1065
    1066
    1067
    1068
    1069
    1070
    1071
    1072
    1073
    1074
    1075
    1076
    1077
    1078
    1079
    1080
    1081
    1082
    1083
    1084
    1085
    1086
    1087
    1088
    1089
    1090
    1091
    1092
    1093
    1094
    1095
    1096
    1097
    1098
    1099
    1100
    1101
    1102
    1103
    1104
    1105
    1106
    1107
    1108
    1109
    1110
    1111
    1112
    1113
    1114
    1115
    1116
    1117
    1118
    1119
    1120
    1121
    1122
    1123
    1124
    1125
    1126
    1127
    1128
    1129
    1130
    1131
    1132
    1133
    1134
    1135
    1136
    1137
    1138
    1139
    1140
    1141
    1142
    1143
    1144
    1145
    1146
    1147
    1148
    1149
    1150
    1151
    1152
    1153
    1154
    1155
    1156
    1157
    1158
    1159
    1160
    1161
    1162
    1163
    1164
    1165
    1166
    1167
    1168
    1169
    1170
    1171
    1172
    1173
    1174
    1175
    1176
    1177
    1178
    1179
    1180
    1181
    1182
    1183
    1184
    1185
    1186
    1187
    1188
    1189
    1190
    1191
    1192
    1193
    1194
    1195
    1196
    1197
    1198
    1199
    1200
    1201
    1202
    1203
    1204
    1205
    1206
    1207
    1208
    1209
    1210
    1211
    1212
    1213
    1214
    1215
    1216
    1217
    1218
    1219
    1220
    1221
    1222
    1223
    1224
    1225
    1226
    1227
    1228
    1229
    1230
    1231
    1232
    1233
    1234
    1235
    1236
    1237
    1238
    1239
    1240
    1241
    1242
    1243
    1244
    1245
    1246
    1247
    1248
    1249
    1250
    1251
    1252
    1253
    1254
    1255
    1256
    1257
    1258
    1259
    1260
    1261
    1262
    1263
    1264
    1265
    1266
    1267
    1268
    1269
    1270
    1271
    1272
    1273
    1274
    1275
    1276
    1277
    1278
    1279
    1280
    1281
    1282
    1283
    1284
    1285
    1286
    1287
    1288
    1289
    1290
    1291
    1292
    1293
    1294
    1295
    1296
    1297
    1298
    1299
    1300
    1301
    1302
    1303
    1304
    1305
    1306
    1307
    1308
    1309
    1310
    1311
    1312
    1313
    1314
    1315
    1316
    1317
    1318
    1319
    1320
    1321
    1322
    1323
    1324
    1325
    1326
    1327
    1328
    1329
    1330
    1331
    1332
    1333
    1334
    1335
    1336
    1337
    1338
    1339
    1340
    1341
    1342
    1343
    1344
    1345
    1346
    1347
    1348
    1349
    1350
    1351
    1352
    1353
    1354
    1355
    1356
    1357
    1358
    1359
    1360
    1361
    1362
    1363
    1364
    1365
    1366
    1367
    1368
    1369
    1370
    1371
    1372
    1373
    1374
    1375
    1376
    1377
    1378
    1379
    1380
    1381
    1382

    ## 8、自定义 Redis 工具类

    ---

    使用 RedisTemplate 需要频繁调用`.opForxxx`然后才能进行对应的操作,这样使用起来代码效率低下,工作中一般不会这样使用,而是将这些常用的公共 API 抽取出来封装成为一个工具类,然后直接使用工具类来间接操作 Redis,不但效率高并且易用。

    工具类参考博客:

    https://www.cnblogs.com/zeng1994/p/03303c805731afc9aa9c60dbbd32a323.html

    https://www.cnblogs.com/zhzhlong/p/11434284.html

    RedisUtils

    ```java
    package com.huang.Utils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.redis.connection.DataType;
    import org.springframework.data.redis.core.Cursor;
    import org.springframework.data.redis.core.ScanOptions;
    import org.springframework.data.redis.core.StringRedisTemplate;
    import org.springframework.data.redis.core.ZSetOperations.TypedTuple;
    import org.springframework.stereotype.Component;

    import java.util.Collection;
    import java.util.Date;
    import java.util.List;
    import java.util.Map;
    import java.util.Map.Entry;
    import java.util.Set;
    import java.util.concurrent.TimeUnit;

    /**
    * @Author 黄远林
    * @Description:
    * @Date: Created in 7:50 2021/5/19
    * @Modified By:
    */

    /**
    * Redis工具类*/
    @Component
    public class RedisUtils {

    @Autowired
    private StringRedisTemplate redisTemplate;

    public void setRedisTemplate(StringRedisTemplate redisTemplate) {
    this.redisTemplate = redisTemplate;
    }

    public StringRedisTemplate getRedisTemplate() {
    return this.redisTemplate;
    }

    /** -------------------key相关操作--------------------- */

    /**
    * 删除key
    *
    * @param key
    */
    public void delete(String key) {
    redisTemplate.delete(key);
    }

    /**
    * 批量删除key
    *
    * @param keys
    */
    public void delete(Collection<String> keys) {
    redisTemplate.delete(keys);
    }

    /**
    * 序列化key
    *
    * @param key
    * @return
    */
    public byte[] dump(String key) {
    return redisTemplate.dump(key);
    }

    /**
    * 是否存在key
    *
    * @param key
    * @return
    */
    public Boolean hasKey(String key) {
    return redisTemplate.hasKey(key);
    }

    /**
    * 设置过期时间
    *
    * @param key
    * @param timeout
    * @param unit
    * @return
    */
    public Boolean expire(String key, long timeout, TimeUnit unit) {
    return redisTemplate.expire(key, timeout, unit);
    }

    /**
    * 设置过期时间
    *
    * @param key
    * @param date
    * @return
    */
    public Boolean expireAt(String key, Date date) {
    return redisTemplate.expireAt(key, date);
    }

    /**
    * 查找匹配的key
    *
    * @param pattern
    * @return
    */
    public Set<String> keys(String pattern) {
    return redisTemplate.keys(pattern);
    }

    /**
    * 将当前数据库的 key 移动到给定的数据库 db 当中
    *
    * @param key
    * @param dbIndex
    * @return
    */
    public Boolean move(String key, int dbIndex) {
    return redisTemplate.move(key, dbIndex);
    }

    /**
    * 移除 key 的过期时间,key 将持久保持
    *
    * @param key
    * @return
    */
    public Boolean persist(String key) {
    return redisTemplate.persist(key);
    }

    /**
    * 返回 key 的剩余的过期时间
    *
    * @param key
    * @param unit
    * @return
    */
    public Long getExpire(String key, TimeUnit unit) {
    return redisTemplate.getExpire(key, unit);
    }

    /**
    * 返回 key 的剩余的过期时间
    *
    * @param key
    * @return
    */
    public Long getExpire(String key) {
    return redisTemplate.getExpire(key);
    }

    /**
    * 从当前数据库中随机返回一个 key
    *
    * @return
    */
    public String randomKey() {
    return redisTemplate.randomKey();
    }

    /**
    * 修改 key 的名称
    *
    * @param oldKey
    * @param newKey
    */
    public void rename(String oldKey, String newKey) {
    redisTemplate.rename(oldKey, newKey);
    }

    /**
    * 仅当 newkey 不存在时,将 oldKey 改名为 newkey
    *
    * @param oldKey
    * @param newKey
    * @return
    */
    public Boolean renameIfAbsent(String oldKey, String newKey) {
    return redisTemplate.renameIfAbsent(oldKey, newKey);
    }

    /**
    * 返回 key 所储存的值的类型
    *
    * @param key
    * @return
    */
    public DataType type(String key) {
    return redisTemplate.type(key);
    }

    /** -------------------string相关操作--------------------- */

    /**
    * 设置指定 key 的值
    * @param key
    * @param value
    */
    public void set(String key, String value) {
    redisTemplate.opsForValue().set(key, value);
    }

    /**
    * 获取指定 key 的值
    * @param key
    * @return
    */
    public Object get(String key) {
    return redisTemplate.opsForValue().get(key);
    }

    /**
    * 返回 key 中字符串值的子字符
    * @param key
    * @param start
    * @param end
    * @return
    */
    public String getRange(String key, long start, long end) {
    return redisTemplate.opsForValue().get(key, start, end);
    }

    /**
    * 将给定 key 的值设为 value ,并返回 key 的旧值(old value)
    *
    * @param key
    * @param value
    * @return
    */
    public String getAndSet(String key, String value) {
    return redisTemplate.opsForValue().getAndSet(key, value);
    }

    /**
    * 对 key 所储存的字符串值,获取指定偏移量上的位(bit)
    *
    * @param key
    * @param offset
    * @return
    */
    public Boolean getBit(String key, long offset) {
    return redisTemplate.opsForValue().getBit(key, offset);
    }

    /**
    * 批量获取
    *
    * @param keys
    * @return
    */
    public List<String> multiGet(Collection<String> keys) {
    return redisTemplate.opsForValue().multiGet(keys);
    }

    /**
    * 设置ASCII码, 字符串'a'的ASCII码是97, 转为二进制是'01100001', 此方法是将二进制第offset位值变为value
    *
    * @param key 位置
    * @param value
    * 值,true为1, false为0
    * @return
    */
    public boolean setBit(String key, long offset, boolean value) {
    return redisTemplate.opsForValue().setBit(key, offset, value);
    }

    /**
    * 将值 value 关联到 key ,并将 key 的过期时间设为 timeout
    *
    * @param key
    * @param value
    * @param timeout
    * 过期时间
    * @param unit
    * 时间单位, 天:TimeUnit.DAYS 小时:TimeUnit.HOURS 分钟:TimeUnit.MINUTES
    * 秒:TimeUnit.SECONDS 毫秒:TimeUnit.MILLISECONDS
    */
    public void setEx(String key, String value, long timeout, TimeUnit unit) {
    redisTemplate.opsForValue().set(key, value, timeout, unit);
    }

    /**
    * 只有在 key 不存在时设置 key 的值
    *
    * @param key
    * @param value
    * @return 之前已经存在返回false,不存在返回true
    */
    public boolean setIfAbsent(String key, String value) {
    return redisTemplate.opsForValue().setIfAbsent(key, value);
    }

    /**
    * 用 value 参数覆写给定 key 所储存的字符串值,从偏移量 offset 开始
    *
    * @param key
    * @param value
    * @param offset
    * 从指定位置开始覆写
    */
    public void setRange(String key, String value, long offset) {
    redisTemplate.opsForValue().set(key, value, offset);
    }

    /**
    * 获取字符串的长度
    *
    * @param key
    * @return
    */
    public Long size(String key) {
    return redisTemplate.opsForValue().size(key);
    }

    /**
    * 批量添加
    *
    * @param maps
    */
    public void multiSet(Map<String, String> maps) {
    redisTemplate.opsForValue().multiSet(maps);
    }

    /**
    * 同时设置一个或多个 key-value 对,当且仅当所有给定 key 都不存在
    *
    * @param maps
    * @return 之前已经存在返回false,不存在返回true
    */
    public boolean multiSetIfAbsent(Map<String, String> maps) {
    return redisTemplate.opsForValue().multiSetIfAbsent(maps);
    }

    /**
    * 增加(自增长), 负数则为自减
    *
    * @param key
    * @return
    */
    public Long incrBy(String key, long increment) {
    return redisTemplate.opsForValue().increment(key, increment);
    }

    /**
    *
    * @param key
    * @return
    */
    public Double incrByFloat(String key, double increment) {
    return redisTemplate.opsForValue().increment(key, increment);
    }

    /**
    * 追加到末尾
    *
    * @param key
    * @param value
    * @return
    */
    public Integer append(String key, String value) {
    return redisTemplate.opsForValue().append(key, value);
    }

    /** -------------------hash相关操作------------------------- */

    /**
    * 获取存储在哈希表中指定字段的值
    *
    * @param key
    * @param field
    * @return
    */
    public Object hGet(String key, String field) {
    return redisTemplate.opsForHash().get(key, field);
    }

    /**
    * 获取所有给定字段的值
    *
    * @param key
    * @return
    */
    public Map<Object, Object> hGetAll(String key) {
    return redisTemplate.opsForHash().entries(key);
    }

    /**
    * 获取所有给定字段的值
    *
    * @param key
    * @param fields
    * @return
    */
    public List<Object> hMultiGet(String key, Collection<Object> fields) {
    return redisTemplate.opsForHash().multiGet(key, fields);
    }

    public void hPut(String key, String hashKey, String value) {
    redisTemplate.opsForHash().put(key, hashKey, value);
    }

    public void hPutAll(String key, Map<String, String> maps) {
    redisTemplate.opsForHash().putAll(key, maps);
    }

    /**
    * 仅当hashKey不存在时才设置
    *
    * @param key
    * @param hashKey
    * @param value
    * @return
    */
    public Boolean hPutIfAbsent(String key, String hashKey, String value) {
    return redisTemplate.opsForHash().putIfAbsent(key, hashKey, value);
    }

    /**
    * 删除一个或多个哈希表字段
    *
    * @param key
    * @param fields
    * @return
    */
    public Long hDelete(String key, Object... fields) {
    return redisTemplate.opsForHash().delete(key, fields);
    }

    /**
    * 查看哈希表 key 中,指定的字段是否存在
    *
    * @param key
    * @param field
    * @return
    */
    public boolean hExists(String key, String field) {
    return redisTemplate.opsForHash().hasKey(key, field);
    }

    /**
    * 为哈希表 key 中的指定字段的整数值加上增量 increment
    *
    * @param key
    * @param field
    * @param increment
    * @return
    */
    public Long hIncrBy(String key, Object field, long increment) {
    return redisTemplate.opsForHash().increment(key, field, increment);
    }

    /**
    * 为哈希表 key 中的指定字段的整数值加上增量 increment
    *
    * @param key
    * @param field
    * @param delta
    * @return
    */
    public Double hIncrByFloat(String key, Object field, double delta) {
    return redisTemplate.opsForHash().increment(key, field, delta);
    }

    /**
    * 获取所有哈希表中的字段
    *
    * @param key
    * @return
    */
    public Set<Object> hKeys(String key) {
    return redisTemplate.opsForHash().keys(key);
    }

    /**
    * 获取哈希表中字段的数量
    *
    * @param key
    * @return
    */
    public Long hSize(String key) {
    return redisTemplate.opsForHash().size(key);
    }

    /**
    * 获取哈希表中所有值
    *
    * @param key
    * @return
    */
    public List<Object> hValues(String key) {
    return redisTemplate.opsForHash().values(key);
    }

    /**
    * 迭代哈希表中的键值对
    *
    * @param key
    * @param options
    * @return
    */
    public Cursor<Entry<Object, Object>> hScan(String key, ScanOptions options) {
    return redisTemplate.opsForHash().scan(key, options);
    }

    /** ------------------------list相关操作---------------------------- */

    /**
    * 通过索引获取列表中的元素
    *
    * @param key
    * @param index
    * @return
    */
    public String lIndex(String key, long index) {
    return redisTemplate.opsForList().index(key, index);
    }

    /**
    * 获取列表指定范围内的元素
    *
    * @param key
    * @param start
    * 开始位置, 0是开始位置
    * @param end
    * 结束位置, -1返回所有
    * @return
    */
    public List<String> lRange(String key, long start, long end) {
    return redisTemplate.opsForList().range(key, start, end);
    }

    /**
    * 存储在list头部
    *
    * @param key
    * @param value
    * @return
    */
    public Long lLeftPush(String key, String value) {
    return redisTemplate.opsForList().leftPush(key, value);
    }

    /**
    *
    * @param key
    * @param value
    * @return
    */
    public Long lLeftPushAll(String key, String... value) {
    return redisTemplate.opsForList().leftPushAll(key, value);
    }

    /**
    *
    * @param key
    * @param value
    * @return
    */
    public Long lLeftPushAll(String key, Collection<String> value) {
    return redisTemplate.opsForList().leftPushAll(key, value);
    }

    /**
    * 当list存在的时候才加入
    *
    * @param key
    * @param value
    * @return
    */
    public Long lLeftPushIfPresent(String key, String value) {
    return redisTemplate.opsForList().leftPushIfPresent(key, value);
    }

    /**
    * 如果pivot存在,再pivot前面添加
    *
    * @param key
    * @param pivot
    * @param value
    * @return
    */
    public Long lLeftPush(String key, String pivot, String value) {
    return redisTemplate.opsForList().leftPush(key, pivot, value);
    }

    /**
    *
    * @param key
    * @param value
    * @return
    */
    public Long lRightPush(String key, String value) {
    return redisTemplate.opsForList().rightPush(key, value);
    }

    /**
    *
    * @param key
    * @param value
    * @return
    */
    public Long lRightPushAll(String key, String... value) {
    return redisTemplate.opsForList().rightPushAll(key, value);
    }

    /**
    *
    * @param key
    * @param value
    * @return
    */
    public Long lRightPushAll(String key, Collection<String> value) {
    return redisTemplate.opsForList().rightPushAll(key, value);
    }

    /**
    * 为已存在的列表添加值
    *
    * @param key
    * @param value
    * @return
    */
    public Long lRightPushIfPresent(String key, String value) {
    return redisTemplate.opsForList().rightPushIfPresent(key, value);
    }

    /**
    * 在pivot元素的右边添加值
    *
    * @param key
    * @param pivot
    * @param value
    * @return
    */
    public Long lRightPush(String key, String pivot, String value) {
    return redisTemplate.opsForList().rightPush(key, pivot, value);
    }

    /**
    * 通过索引设置列表元素的值
    *
    * @param key
    * @param index
    * 位置
    * @param value
    */
    public void lSet(String key, long index, String value) {
    redisTemplate.opsForList().set(key, index, value);
    }

    /**
    * 移出并获取列表的第一个元素
    *
    * @param key
    * @return 删除的元素
    */
    public String lLeftPop(String key) {
    return redisTemplate.opsForList().leftPop(key);
    }

    /**
    * 移出并获取列表的第一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止
    *
    * @param key
    * @param timeout
    * 等待时间
    * @param unit
    * 时间单位
    * @return
    */
    public String lBLeftPop(String key, long timeout, TimeUnit unit) {
    return redisTemplate.opsForList().leftPop(key, timeout, unit);
    }

    /**
    * 移除并获取列表最后一个元素
    *
    * @param key
    * @return 删除的元素
    */
    public String lRightPop(String key) {
    return redisTemplate.opsForList().rightPop(key);
    }

    /**
    * 移出并获取列表的最后一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止
    *
    * @param key
    * @param timeout
    * 等待时间
    * @param unit
    * 时间单位
    * @return
    */
    public String lBRightPop(String key, long timeout, TimeUnit unit) {
    return redisTemplate.opsForList().rightPop(key, timeout, unit);
    }

    /**
    * 移除列表的最后一个元素,并将该元素添加到另一个列表并返回
    *
    * @param sourceKey
    * @param destinationKey
    * @return
    */
    public String lRightPopAndLeftPush(String sourceKey, String destinationKey) {
    return redisTemplate.opsForList().rightPopAndLeftPush(sourceKey,
    destinationKey);
    }

    /**
    * 从列表中弹出一个值,将弹出的元素插入到另外一个列表中并返回它; 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止
    *
    * @param sourceKey
    * @param destinationKey
    * @param timeout
    * @param unit
    * @return
    */
    public String lBRightPopAndLeftPush(String sourceKey, String destinationKey,
    long timeout, TimeUnit unit) {
    return redisTemplate.opsForList().rightPopAndLeftPush(sourceKey,
    destinationKey, timeout, unit);
    }

    /**
    * 删除集合中值等于value得元素
    *
    * @param key
    * @param index
    * index=0, 删除所有值等于value的元素; index>0, 从头部开始删除第一个值等于value的元素;
    * index<0, 从尾部开始删除第一个值等于value的元素;
    * @param value
    * @return
    */
    public Long lRemove(String key, long index, String value) {
    return redisTemplate.opsForList().remove(key, index, value);
    }

    /**
    * 裁剪list
    *
    * @param key
    * @param start
    * @param end
    */
    public void lTrim(String key, long start, long end) {
    redisTemplate.opsForList().trim(key, start, end);
    }

    /**
    * 获取列表长度
    *
    * @param key
    * @return
    */
    public Long lLen(String key) {
    return redisTemplate.opsForList().size(key);
    }

    /** --------------------set相关操作-------------------------- */

    /**
    * set添加元素
    *
    * @param key
    * @param values
    * @return
    */
    public Long sAdd(String key, String... values) {
    return redisTemplate.opsForSet().add(key, values);
    }

    /**
    * set移除元素
    *
    * @param key
    * @param values
    * @return
    */
    public Long sRemove(String key, Object... values) {
    return redisTemplate.opsForSet().remove(key, values);
    }

    /**
    * 移除并返回集合的一个随机元素
    *
    * @param key
    * @return
    */
    public String sPop(String key) {
    return redisTemplate.opsForSet().pop(key);
    }

    /**
    * 将元素value从一个集合移到另一个集合
    *
    * @param key
    * @param value
    * @param destKey
    * @return
    */
    public Boolean sMove(String key, String value, String destKey) {
    return redisTemplate.opsForSet().move(key, value, destKey);
    }

    /**
    * 获取集合的大小
    *
    * @param key
    * @return
    */
    public Long sSize(String key) {
    return redisTemplate.opsForSet().size(key);
    }

    /**
    * 判断集合是否包含value
    *
    * @param key
    * @param value
    * @return
    */
    public Boolean sIsMember(String key, Object value) {
    return redisTemplate.opsForSet().isMember(key, value);
    }

    /**
    * 获取两个集合的交集
    *
    * @param key
    * @param otherKey
    * @return
    */
    public Set<String> sIntersect(String key, String otherKey) {
    return redisTemplate.opsForSet().intersect(key, otherKey);
    }

    /**
    * 获取key集合与多个集合的交集
    *
    * @param key
    * @param otherKeys
    * @return
    */
    public Set<String> sIntersect(String key, Collection<String> otherKeys) {
    return redisTemplate.opsForSet().intersect(key, otherKeys);
    }

    /**
    * key集合与otherKey集合的交集存储到destKey集合中
    *
    * @param key
    * @param otherKey
    * @param destKey
    * @return
    */
    public Long sIntersectAndStore(String key, String otherKey, String destKey) {
    return redisTemplate.opsForSet().intersectAndStore(key, otherKey,
    destKey);
    }

    /**
    * key集合与多个集合的交集存储到destKey集合中
    *
    * @param key
    * @param otherKeys
    * @param destKey
    * @return
    */
    public Long sIntersectAndStore(String key, Collection<String> otherKeys,
    String destKey) {
    return redisTemplate.opsForSet().intersectAndStore(key, otherKeys,
    destKey);
    }

    /**
    * 获取两个集合的并集
    *
    * @param key
    * @param otherKeys
    * @return
    */
    public Set<String> sUnion(String key, String otherKeys) {
    return redisTemplate.opsForSet().union(key, otherKeys);
    }

    /**
    * 获取key集合与多个集合的并集
    *
    * @param key
    * @param otherKeys
    * @return
    */
    public Set<String> sUnion(String key, Collection<String> otherKeys) {
    return redisTemplate.opsForSet().union(key, otherKeys);
    }

    /**
    * key集合与otherKey集合的并集存储到destKey中
    *
    * @param key
    * @param otherKey
    * @param destKey
    * @return
    */
    public Long sUnionAndStore(String key, String otherKey, String destKey) {
    return redisTemplate.opsForSet().unionAndStore(key, otherKey, destKey);
    }

    /**
    * key集合与多个集合的并集存储到destKey中
    *
    * @param key
    * @param otherKeys
    * @param destKey
    * @return
    */
    public Long sUnionAndStore(String key, Collection<String> otherKeys,
    String destKey) {
    return redisTemplate.opsForSet().unionAndStore(key, otherKeys, destKey);
    }

    /**
    * 获取两个集合的差集
    *
    * @param key
    * @param otherKey
    * @return
    */
    public Set<String> sDifference(String key, String otherKey) {
    return redisTemplate.opsForSet().difference(key, otherKey);
    }

    /**
    * 获取key集合与多个集合的差集
    *
    * @param key
    * @param otherKeys
    * @return
    */
    public Set<String> sDifference(String key, Collection<String> otherKeys) {
    return redisTemplate.opsForSet().difference(key, otherKeys);
    }

    /**
    * key集合与otherKey集合的差集存储到destKey中
    *
    * @param key
    * @param otherKey
    * @param destKey
    * @return
    */
    public Long sDifference(String key, String otherKey, String destKey) {
    return redisTemplate.opsForSet().differenceAndStore(key, otherKey,
    destKey);
    }

    /**
    * key集合与多个集合的差集存储到destKey中
    *
    * @param key
    * @param otherKeys
    * @param destKey
    * @return
    */
    public Long sDifference(String key, Collection<String> otherKeys,
    String destKey) {
    return redisTemplate.opsForSet().differenceAndStore(key, otherKeys,
    destKey);
    }

    /**
    * 获取集合所有元素
    *
    * @param key
    * @return
    */
    public Set<String> setMembers(String key) {
    return redisTemplate.opsForSet().members(key);
    }

    /**
    * 随机获取集合中的一个元素
    *
    * @param key
    * @return
    */
    public String sRandomMember(String key) {
    return redisTemplate.opsForSet().randomMember(key);
    }

    /**
    * 随机获取集合中count个元素
    *
    * @param key
    * @param count
    * @return
    */
    public List<String> sRandomMembers(String key, long count) {
    return redisTemplate.opsForSet().randomMembers(key, count);
    }

    /**
    * 随机获取集合中count个元素并且去除重复的
    *
    * @param key
    * @param count
    * @return
    */
    public Set<String> sDistinctRandomMembers(String key, long count) {
    return redisTemplate.opsForSet().distinctRandomMembers(key, count);
    }

    /**
    *
    * @param key
    * @param options
    * @return
    */
    public Cursor<String> sScan(String key, ScanOptions options) {
    return redisTemplate.opsForSet().scan(key, options);
    }

    /**------------------zSet相关操作--------------------------------*/

    /**
    * 添加元素,有序集合是按照元素的score值由小到大排列
    *
    * @param key
    * @param value
    * @param score
    * @return
    */
    public Boolean zAdd(String key, String value, double score) {
    return redisTemplate.opsForZSet().add(key, value, score);
    }

    /**
    *
    * @param key
    * @param values
    * @return
    */
    public Long zAdd(String key, Set<TypedTuple<String>> values) {
    return redisTemplate.opsForZSet().add(key, values);
    }

    /**
    *
    * @param key
    * @param values
    * @return
    */
    public Long zRemove(String key, Object... values) {
    return redisTemplate.opsForZSet().remove(key, values);
    }

    /**
    * 增加元素的score值,并返回增加后的值
    *
    * @param key
    * @param value
    * @param delta
    * @return
    */
    public Double zIncrementScore(String key, String value, double delta) {
    return redisTemplate.opsForZSet().incrementScore(key, value, delta);
    }

    /**
    * 返回元素在集合的排名,有序集合是按照元素的score值由小到大排列
    *
    * @param key
    * @param value
    * @return 0表示第一位
    */
    public Long zRank(String key, Object value) {
    return redisTemplate.opsForZSet().rank(key, value);
    }

    /**
    * 返回元素在集合的排名,按元素的score值由大到小排列
    *
    * @param key
    * @param value
    * @return
    */
    public Long zReverseRank(String key, Object value) {
    return redisTemplate.opsForZSet().reverseRank(key, value);
    }

    /**
    * 获取集合的元素, 从小到大排序
    *
    * @param key
    * @param start
    * 开始位置
    * @param end
    * 结束位置, -1查询所有
    * @return
    */
    public Set<String> zRange(String key, long start, long end) {
    return redisTemplate.opsForZSet().range(key, start, end);
    }

    /**
    * 获取集合元素, 并且把score值也获取
    *
    * @param key
    * @param start
    * @param end
    * @return
    */
    public Set<TypedTuple<String>> zRangeWithScores(String key, long start,
    long end) {
    return redisTemplate.opsForZSet().rangeWithScores(key, start, end);
    }

    /**
    * 根据Score值查询集合元素
    *
    * @param key
    * @param min
    * 最小值
    * @param max
    * 最大值
    * @return
    */
    public Set<String> zRangeByScore(String key, double min, double max) {
    return redisTemplate.opsForZSet().rangeByScore(key, min, max);
    }

    /**
    * 根据Score值查询集合元素, 从小到大排序
    *
    * @param key
    * @param min
    * 最小值
    * @param max
    * 最大值
    * @return
    */
    public Set<TypedTuple<String>> zRangeByScoreWithScores(String key,
    double min, double max) {
    return redisTemplate.opsForZSet().rangeByScoreWithScores(key, min, max);
    }

    /**
    *
    * @param key
    * @param min
    * @param max
    * @param start
    * @param end
    * @return
    */
    public Set<TypedTuple<String>> zRangeByScoreWithScores(String key,
    double min, double max, long start, long end) {
    return redisTemplate.opsForZSet().rangeByScoreWithScores(key, min, max,
    start, end);
    }

    /**
    * 获取集合的元素, 从大到小排序
    *
    * @param key
    * @param start
    * @param end
    * @return
    */
    public Set<String> zReverseRange(String key, long start, long end) {
    return redisTemplate.opsForZSet().reverseRange(key, start, end);
    }

    /**
    * 获取集合的元素, 从大到小排序, 并返回score值
    *
    * @param key
    * @param start
    * @param end
    * @return
    */
    public Set<TypedTuple<String>> zReverseRangeWithScores(String key,
    long start, long end) {
    return redisTemplate.opsForZSet().reverseRangeWithScores(key, start,
    end);
    }

    /**
    * 根据Score值查询集合元素, 从大到小排序
    *
    * @param key
    * @param min
    * @param max
    * @return
    */
    public Set<String> zReverseRangeByScore(String key, double min,
    double max) {
    return redisTemplate.opsForZSet().reverseRangeByScore(key, min, max);
    }

    /**
    * 根据Score值查询集合元素, 从大到小排序
    *
    * @param key
    * @param min
    * @param max
    * @return
    */
    public Set<TypedTuple<String>> zReverseRangeByScoreWithScores(
    String key, double min, double max) {
    return redisTemplate.opsForZSet().reverseRangeByScoreWithScores(key,
    min, max);
    }

    /**
    *
    * @param key
    * @param min
    * @param max
    * @param start
    * @param end
    * @return
    */
    public Set<String> zReverseRangeByScore(String key, double min,
    double max, long start, long end) {
    return redisTemplate.opsForZSet().reverseRangeByScore(key, min, max,
    start, end);
    }

    /**
    * 根据score值获取集合元素数量
    *
    * @param key
    * @param min
    * @param max
    * @return
    */
    public Long zCount(String key, double min, double max) {
    return redisTemplate.opsForZSet().count(key, min, max);
    }

    /**
    * 获取集合大小
    *
    * @param key
    * @return
    */
    public Long zSize(String key) {
    return redisTemplate.opsForZSet().size(key);
    }

    /**
    * 获取集合大小
    *
    * @param key
    * @return
    */
    public Long zZCard(String key) {
    return redisTemplate.opsForZSet().zCard(key);
    }

    /**
    * 获取集合中value元素的score值
    *
    * @param key
    * @param value
    * @return
    */
    public Double zScore(String key, Object value) {
    return redisTemplate.opsForZSet().score(key, value);
    }

    /**
    * 移除指定索引位置的成员
    *
    * @param key
    * @param start
    * @param end
    * @return
    */
    public Long zRemoveRange(String key, long start, long end) {
    return redisTemplate.opsForZSet().removeRange(key, start, end);
    }

    /**
    * 根据指定的score值的范围来移除成员
    *
    * @param key
    * @param min
    * @param max
    * @return
    */
    public Long zRemoveRangeByScore(String key, double min, double max) {
    return redisTemplate.opsForZSet().removeRangeByScore(key, min, max);
    }

    /**
    * 获取key和otherKey的并集并存储在destKey中
    *
    * @param key
    * @param otherKey
    * @param destKey
    * @return
    */
    public Long zUnionAndStore(String key, String otherKey, String destKey) {
    return redisTemplate.opsForZSet().unionAndStore(key, otherKey, destKey);
    }

    /**
    *
    * @param key
    * @param otherKeys
    * @param destKey
    * @return
    */
    public Long zUnionAndStore(String key, Collection<String> otherKeys,
    String destKey) {
    return redisTemplate.opsForZSet()
    .unionAndStore(key, otherKeys, destKey);
    }

    /**
    * 交集
    *
    * @param key
    * @param otherKey
    * @param destKey
    * @return
    */
    public Long zIntersectAndStore(String key, String otherKey,
    String destKey) {
    return redisTemplate.opsForZSet().intersectAndStore(key, otherKey,
    destKey);
    }

    /**
    * 交集
    *
    * @param key
    * @param otherKeys
    * @param destKey
    * @return
    */
    public Long zIntersectAndStore(String key, Collection<String> otherKeys,
    String destKey) {
    return redisTemplate.opsForZSet().intersectAndStore(key, otherKeys,
    destKey);
    }

    /**
    *
    * @param key
    * @param options
    * @return
    */
    public Cursor<TypedTuple<String>> zScan(String key, ScanOptions options) {
    return redisTemplate.opsForZSet().scan(key, options);
    }
    }

9、Redis.conf


容量单位不区分大小写,G 和 GB 有区别

image-20210519213425110

可以使用 include 组合多个配置问题

image-20210519213443138

网络配置

image-20210519213513686

日志输出级别

image-20210519213546237

日志输出文件

image-20210519213610291

持久化规则

由于 Redis 是基于内存的数据库,需要将数据由内存持久化到文件中

持久化方式:

  • RDB
  • AOF

image-20210519213637622

RDB 文件相关

image-20210519213704099

image-20210519213715249

主从复制

image-20210519213754740

Security 模块中进行密码设置

image-20210519213819778

客户端连接相关

1
2
3
maxclients 10000  最大客户端数量
maxmemory <bytes> 最大内存限制
maxmemory-policy noeviction # 内存达到限制值的处理策略

redis 中的默认的过期策略是 volatile-lru 。

设置方式

1
config set maxmemory-policy volatile-lru

maxmemory-policy 六种方式

1、volatile-lru:只对设置了过期时间的 key 进行 LRU(默认值)

2、allkeys-lru : 删除 lru 算法的 key

3、volatile-random:随机删除即将过期 key

4、allkeys-random:随机删除

5、volatile-ttl : 删除即将过期的

6、noeviction : 永不过期,返回错误

AOF 相关部分

image-20210519214136461

image-20210519214150106

10、持久化—RDB


RDB:Redis Databases

什么是 RDB


在指定时间间隔后,将内存中的数据集快照写入数据库 ;在恢复时候,直接读取快照文件,进行数据的恢复 ;

image-20210519214507636

默认情况下, Redis 将数据库快照保存在名字为 dump.rdb 的二进制文件中。文件名可以在配置文件中进行自定义。

工作原理


在进行 RDB 的时候,redis 的主线程是不会做 io 操作的,主线程会 fork 一个子线程来完成该操作;

  1. Redis 调用 forks。同时拥有父进程和子进程。
  2. 子进程将数据集写入到一个临时 RDB 文件中。
  3. 当子进程完成对新 RDB 文件的写入时,Redis 用新 RDB 文件替换原来的 RDB 文件,并删除旧的 RDB 文件。

这种工作方式使得 Redis 可以从写时复制(copy-on-write)机制中获益(因为是使用子进程进行写操作,而父进程依然可以接收来自客户端的请求。)

image-20210519214731117

触发机制


  1. save 的规则满足的情况下,会自动触发 rdb 原则
  2. 执行 flushall 命令,也会触发我们的 rdb 原则
  3. 退出 redis,也会自动产生 rdb 文件

save

使用 save 命令,会立刻对当前内存中的数据进行持久化 ,但是会阻塞,也就是不接受其他操作了;

由于 save 命令是同步命令,会占用 Redis 的主进程。若 Redis 数据非常多时,save命令执行速度会非常慢,阻塞所有客户端的请求。

image-20210519214827262

flushall 命令

flushall 命令也会触发持久化 ;

触发持久化规则

满足配置条件中的触发条件 ;

可以通过配置文件对 Redis 进行设置, 让它在“ N 秒内数据集至少有 M 个改动”这一条件被满足时, 自动进行数据集保存操作。

image-20210519214902012

image-20210519214912343

bgsave

bgsave 是异步进行,进行持久化的时候,redis 还可以将继续响应客户端请求 ;

image-20210519215422300

bgsave 和 save 对比

命令 save bgsave
IO 类型 同步 异步
阻塞? 是(阻塞发生在 fock(),通常非常快)
复杂度 O(n) O(n)
优点 不会消耗额外的内存 不阻塞客户端命令
缺点 阻塞客户端命令 需要 fock 子进程,消耗内存

优缺点

优点:

  1. 适合大规模的数据恢复
  2. 对数据的完整性要求不高

缺点:

  1. 需要一定的时间间隔进行操作,如果 redis 意外宕机了,这个最后一次修改的数据就没有了。
  2. fork 进程的时候,会占用一定的内容空间。

11、持久化 AOF


Append Only File

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

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

什么是 AOF

快照功能(RDB)并不是非常耐久(durable): 如果 Redis 因为某些原因而造成故障停机, 那么服务器将丢失最近写入、以及未保存到快照中的那些数据。 从 1.1 版本开始, Redis 增加了一种完全耐久的持久化方式: AOF 持久化。

如果要使用 AOF,需要修改配置文件:

image-20210519215758086

appendonly no yes则表示启用 AOF

默认是不开启的,我们需要手动配置,然后重启 redis,就可以生效了!

如果这个 aof 文件有错位,这时候 redis 是启动不起来的,我需要修改这个 aof 文件

redis 给我们提供了一个工具redis-check-aof --fix

优点和缺点

1
2
3
4
5
6
appendonly yes  # 默认是不开启aof模式的,默认是使用rdb方式持久化的,在大部分的情况下,rdb完全够用
appendfilename "appendonly.aof"

# appendfsync always # 每次修改都会sync 消耗性能
appendfsync everysec # 每秒执行一次 sync 可能会丢失这一秒的数据
# appendfsync no # 不执行 sync ,这时候操作系统自己同步数据,速度最快

优点

  1. 每一次修改都会同步,文件的完整性会更加好
  2. 没秒同步一次,可能会丢失一秒的数据
  3. 从不同步,效率最高

缺点

  1. 相对于数据文件来说,aof 远远大于 rdb,修复速度比 rdb 慢!
  2. Aof 运行效率也要比 rdb 慢,所以我们 redis 默认的配置就是 rdb 持久化

12、RDB 和 AOP 选择

RDB 和 AOF 对比

RDB AOF
启动优先级
体积
恢复速度
数据安全性 丢数据 根据策略决定

如何选择使用哪种持久化方式?

一般来说, 如果想达到足以媲美 PostgreSQL 的数据安全性, 你应该同时使用两种持久化功能。

如果你非常关心你的数据, 但仍然可以承受数分钟以内的数据丢失, 那么你可以只使用 RDB 持久化。

有很多用户都只使用 AOF 持久化, 但并不推荐这种方式: 因为定时生成 RDB 快照(snapshot)非常便于进行数据库备份, 并且 RDB 恢复数据集的速度也要比 AOF 恢复的速度要快。

13、Redis 发布与订阅


Redis 发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。

下图展示了频道 channel1 , 以及订阅这个频道的三个客户端 —— client2 、 client5 和 client1 之间的关系:

image-20210519220239569

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

image-20210519220301300

命令

命令 描述
PSUBSCRIBE pattern [pattern..] 订阅一个或多个符合给定模式的频道。
PUNSUBSCRIBE pattern [pattern..] 退订一个或多个符合给定模式的频道。
PUBSUB subcommand [argument[argument]] 查看订阅与发布系统状态。
PUBLISH channel message 向指定频道发布消息
SUBSCRIBE channel [channel..] 订阅给定的一个或多个频道。
SUBSCRIBE channel [channel..] 退订一个或多个频道

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
------------订阅端----------------------
127.0.0.1:6379> SUBSCRIBE sakura # 订阅sakura频道
Reading messages... (press Ctrl-C to quit) # 等待接收消息
1) "subscribe" # 订阅成功的消息
2) "sakura"
3) (integer) 1
1) "message" # 接收到来自sakura频道的消息 "hello world"
2) "sakura"
3) "hello world"
1) "message" # 接收到来自sakura频道的消息 "hello i am sakura"
2) "sakura"
3) "hello i am sakura"

--------------消息发布端-------------------
127.0.0.1:6379> PUBLISH sakura "hello world" # 发布消息到sakura频道
(integer) 1
127.0.0.1:6379> PUBLISH sakura "hello i am sakura" # 发布消息
(integer) 1

-----------------查看活跃的频道------------
127.0.0.1:6379> PUBSUB channels
1) "sakura"

原理

每个 Redis 服务器进程都维持着一个表示服务器状态的 redis.h/redisServer 结构, 结构的 pubsub_channels 属性是一个字典, 这个字典就用于保存订阅频道的信息,其中,字典的键为正在被订阅的频道, 而字典的值则是一个链表, 链表中保存了所有订阅这个频道的客户端。

image-20210519220430971

客户端订阅,就被链接到对应频道的链表的尾部,退订则就是将客户端节点从链表中移除。

缺点

  1. 如果一个客户端订阅了频道,但自己读取消息的速度却不够快的话,那么不断积压的消息会使 redis 输出缓冲区的体积变得越来越大,这可能使得 redis 本身的速度变慢,甚至直接崩溃。
  2. 这和数据传输可靠性有关,如果在订阅方断线,那么他将会丢失所有在短线期间发布者发布的消息。

应用

  1. 消息订阅:公众号订阅,微博关注等等(起始更多是使用消息队列来进行实现)
  2. 多人在线聊天室。

稍微复杂的场景,我们就会使用消息中间件 MQ 处理。

14、Redis 主从复制


概念

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

默认情况下,每台 Redis 服务器都是主节点,一个主节点可以有 0 个或者多个从节点,但每个从节点只能由一个主节点。

作用

  1. 数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余的方式。
  2. 故障恢复:当主节点故障时,从节点可以暂时替代主节点提供服务,是一种服务冗余的方式
  3. 负载均衡:在主从复制的基础上,配合读写分离,由主节点进行写操作,从节点进行读操作,分担服务器的负载;尤其是在多读少写的场景下,通过多个从节点分担负载,提高并发量。
  4. 高可用基石:主从复制还是哨兵和集群能够实施的基础。

为什么使用集群

  1. 单台服务器难以负载大量的请求
  2. 单台服务器故障率高,系统崩坏概率大
  3. 单台服务器内存容量有限。

环境配置

我们在讲解配置文件的时候,注意到有一个replication模块 (见 Redis.conf 中第 8 条)

查看当前库的信息:info replication

1
2
3
4
5
6
7
8
9
10
11
12
127.0.0.1:6379> info replication
# Replication
role:master # 角色
connected_slaves:0 # 从机数量
master_replid:3b54deef5b7b7b7f7dd8acefa23be48879b4fcff
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

既然需要启动多个服务,就需要多个配置文件。每个配置文件对应修改以下信息:

  • 端口号
  • pid 文件名
  • 日志文件名
  • rdb 文件名

启动单机多服务集群:image-20210519220614194

一主二从配置

==默认情况下,每台 Redis 服务器都是主节点;==我们一般情况下只用配置从机就好了!

认老大!一主(79)二从(80,81)

使用SLAVEOF host port就可以为从机配置主机了。

image-20210519220728586

然后主机上也能看到从机的状态:

image-20210519220749402

我们这里是使用命令搭建,是暂时的,==真实开发中应该在从机的配置文件中进行配置,==这样的话是永久的。

image-20210519220807550

使用规则

  1. 从机只能读,不能写,主机可读可写但是多用于写。
1
2
3
4
5
6
7
8
9
10
 127.0.0.1:6381> set name sakura # 从机6381写入失败
(error) READONLY You can't write against a read only replica.

127.0.0.1:6380> set name sakura # 从机6380写入失败
(error) READONLY You can't write against a read only replica.

127.0.0.1:6379> set name sakura
OK
127.0.0.1:6379> get name
"sakura"
  1. 当主机断电宕机后,默认情况下从机的角色不会发生变化 ,集群中只是失去了写操作,当主机恢复以后,又会连接上从机恢复原状。

  2. 当从机断电宕机后,若不是使用配置文件配置的从机,再次启动后作为主机是无法获取之前主机的数据的,若此时重新配置称为从机,又可以获取到主机的所有数据。这里就要提到一个同步原理。

  3. 第二条中提到,默认情况下,主机故障后,不会出现新的主机,有两种方式可以产生新的主机:
    • 从机手动执行命令slaveof no one,这样执行以后从机会独立出来成为一个主机
    • 使用哨兵模式(自动选举)

如果没有老大了,这个时候能不能选择出来一个老大呢?手动!

如果主机断开了连接,我们可以使用SLAVEOF no one让自己变成主机!其他的节点就可以手动连接到最新的主节点(手动)!如果这个时候老大修复了,那么久重新连接!

15、哨兵模式


更多信息参考博客:https://www.jianshu.com/p/06ab9daf921d

主从切换技术的方法是:当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用。这不是一种推荐的方式,更多时候,我们优先考虑哨兵模式

单机单个哨兵

image-20210520105113999

哨兵的作用:

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

多哨兵模式

image-20210520105205914

哨兵的核心配置

1
sentinel monitor mymaster 127.0.0.1 6379 1
  • 数字 1 表示 :当一个哨兵主观认为主机断开,就可以客观认为主机故障,然后开始选举新的主机。

测试

1
redis-sentinel xxx/sentinel.conf

成功启动哨兵模式

image-20210520105238999

此时哨兵监视着我们的主机 6379,当我们断开主机后:

image-20210520105258393

哨兵模式优缺点

优点:

  1. 哨兵集群,基于主从复制模式,所有主从复制的优点,它都有
  2. 主从可以切换,故障可以转移,系统的可用性更好
  3. 哨兵模式是主从模式的升级,手动到自动,更加健壮

缺点:

  1. Redis 不好在线扩容,集群容量一旦达到上限,在线扩容就十分麻烦
  2. 实现哨兵模式的配置其实是很麻烦的,里面有很多配置项

哨兵模式的全部配置

完整的哨兵模式配置文件 sentinel.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# Example sentinel.conf

# 哨兵sentinel实例运行的端口 默认26379
port 26379

# 哨兵sentinel的工作目录
dir /tmp

# 哨兵sentinel监控的redis主节点的 ip port
# master-name 可以自己命名的主节点名字 只能由字母A-z、数字0-9 、这三个字符".-_"组成。
# quorum 当这些quorum个数sentinel哨兵认为master主节点失联 那么这时 客观上认为主节点失联了
# sentinel monitor <master-name> <ip> <redis-port> <quorum>
sentinel monitor mymaster 127.0.0.1 6379 1

# 当在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

16、缓存穿透与雪崩

16.1、缓存穿透(查不到)

概念

在默认情况下,用户请求数据时,会先在缓存(Redis)中查找,若没找到即缓存未命中,再在数据库中进行查找,数量少可能问题不大,可是一旦大量的请求数据(例如秒杀场景)缓存都没有命中的话,就会全部转移到数据库上,造成数据库极大的压力,就有可能导致数据库崩溃。网络安全中也有人恶意使用这种手段进行攻击被称为洪水攻击。

解决方案

布隆过滤器

对所有可能查询的参数以 Hash 的形式存储,以便快速确定是否存在这个值,在控制层先进行拦截校验,校验不通过直接打回,减轻了存储系统的压力。

image-20210520105357970

缓存空对象

一次请求若在缓存和数据库中都没找到,就在缓存中方一个空对象用于处理后续这个请求。

image-20210520105412195

这样做有一个缺陷:存储空对象也需要空间,大量的空对象会耗费一定的空间,存储效率并不高。解决这个缺陷的方式就是设置较短过期时间

即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响。

16.2、缓存击穿(量太大,缓存过期)

概念

相较于缓存穿透,缓存击穿的目的性更强,一个存在的 key,在缓存过期的一刻,同时有大量的请求,这些请求都会击穿到 DB,造成瞬时 DB 请求量大、压力骤增。这就是缓存被击穿,只是针对其中某个 key 的缓存不可用而导致击穿,但是其他的 key 依然可以使用缓存响应。

比如热搜排行上,一个热点新闻被同时大量访问就可能导致缓存击穿。

解决方案

  1. 设置热点数据永不过期

    这样就不会出现热点数据过期的情况,但是当 Redis 内存空间满的时候也会清理部分数据,而且此种方案会占用空间,一旦热点数据多了起来,就会占用部分空间。

  2. 加互斥锁(分布式锁)

    在访问 key 之前,采用 SETNX(set if not exists)来设置另一个短期 key 来锁住当前 key 的访问,访问结束再删除该短期 key。保证同时刻只有一个线程访问。这样对锁的要求就十分高。

16.3、缓存雪崩

概念

大量的 key 设置了相同的过期时间,导致在缓存在同一时刻全部失效,造成瞬时 DB 请求量大、压力骤增,引起雪崩。

image-20210520105514023

解决方案

  • redis 高可用

    这个思想的含义是,既然 redis 有可能挂掉,那我多增设几台 redis,这样一台挂掉之后其他的还可以继续工作,其实就是搭建的集群

  • 限流降级

    这个解决方案的思想是,在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个 key 只允许一个线程查询数据和写缓存,其他线程等待。

  • 数据预热

    数据加热的含义就是在正式部署之前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。在即将发生大并发访问前手动触发加载缓存不同的 key,设置不同的过期时间,让缓存失效的时间点尽量均匀。