Redis 详解

本文详细介绍了Redis的基础知识,包括互联网架构演变、Redis的特点及优势、与其他NoSQL数据库的对比,以及安装配置流程。此外,深入探讨了Redis的五大数据类型、持久化策略(RDB与AOF)、事务处理、发布订阅机制、主从复制和哨兵模式等内容。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

1. 概述

1.1 互联网架构的演变历程

1.2 Redis 入门介绍

1.3 Redis / Memcache / MongoDB 对比

1.4 分布式数据库 CAP 原理

2. 下载与安装

2.1 下载

2.2 安装

2.3 安装后的操作

2.3.1 后台运行方式

2.3.2 关闭数据库

2.3.3 常用操作

2.3.4 连接 Redis 并测试

2.3.5 HelloWorld

2.3.6 测试性能

2.3.7 默认 16 个数据库

2.3.8 数据库键的数量

2.3.9 清空数据库

2.3.10 模糊查询(keys)

2.3.11 键(key)

3. 使用 Redis

3.1 五大数据类型 

3.1.1 字符串 String

3.1.2 列表 List

3.1.3 集合 Set

3.1.4 哈希 hash

3.1.5 有序集合 Zset

3.3 Redis 持久化

3.3.1 RDB

3.3.2 AOF

3.3.3 总结

3.4 NoSQL 事务

3.4.1 开启事务

3.4.2 放弃操作(回滚)

3.4.3 报错恢复

3.4.4 延迟报错

3.4.5 Watch 监控

3.5 Redis 的发布订阅

3.6 主从复制

3.6.1 一主二从

3.6.2 主从继承

3.6.3 手选主机

3.6.4 复制原理

3.6.5 哨兵模式

3.6.6 总配置 redis.conf 详解


1. 概述

1.1 互联网架构的演变历程

第 1 阶段:

  • 数据访问量不大,简单的架构即可搞定。
  • 适合小型项目。
app -> dao -> mysql

第 2 阶段:

  • 数据访问量大,使用缓存技术来缓解数据库的压力。
  • 不同的业务访问不同的数据库。
  • 适合中型项目。
app -> dao -> cache -> [mysql1, mysql2, mysql3]

第 3 阶段:

  • 主从读写分离。
  • 之前的缓存确实能够缓解数据库的压力,但是写和读都集中在一个数据库上,压力又了。
  • 一个数据库负责写,一个数据库负责读;分工合作。
  • 让 Master(主数据库)来响应事务性(增删改)操作,让 Slave(从数据库)来响应非事务性(查询)操作,然后再采用主从复制来把 Master 上的事务性操作同步到 Slave 数据库中。
  • MySQL 的 Master / Slave 就是网站的标配。
  • 适合大型项目。
app -> dao -> cache -> 主库 -> [从库1, 从库2]

  

第 4 阶段:

  • 在 MySQL 的主从复制,读写分离的基础上,MySQL 的主库开始出现瓶颈。
  • 由于 MyISAM 使用表锁,所以并发性能特别差。
  • 分库分表开始流行,MySQL 也提出了表分区,虽然不稳定,但有了希望。
  • 使用 MySQL 集群。
  • 适合超大型项目。

1.2 Redis 入门介绍

互联网需求的 3 高:高并发,高可扩,高性能。

Redis 是一种运行速度很快,并发性能很强,并且运行在内存上的 NoSQL(Not only SQL)数据库。

NoSQL 非关系型数据库和传统 RDBMS 关系型数据库相比的优势:

  • NoSQL 数据库无需事先为要存储的数据建立字段,随时可以存储自定义的数据格式。
  • 而在关系数据库里,增删字段是一件非常麻烦的事情;如果是非常大数据量的表,增加字段简直就是一个噩梦。

RDBMS

  • 高度组织化结构化数据
  • 结构化查询语言 SQL
  • 数据和关系都存储在单独的表中
  • 数据操纵语言,数据定义语言
  • 严格的一致性
  • 基础事务

NoSQL

  • 代表着不仅仅是 SQL
  • 没有声明性查询语言
  • 没有预定义的模式
  • 键值对存储,列存储,文档存储,图形数据库
  • 最终一致性,而非 ACID 属性
  • 非结构化和不可预知的数据
  • CAP 定理
  • 高性能,高可用性和可伸缩性

Redis 的常用使用场景:

  • 缓存,是 Redis 当今最为人熟知的使用场景;在提升服务器性能方面非常有效;一些频繁被访问的数据,经常被访问的数据如果放在关系型数据库,每次查询的开销都会很大,而放在 Redis 中,因为 Redis 是放在内存中,可以很高效的访问。
  • 排行榜,在使用传统的关系型数据库(MySQL、Oracle 等)来做这个事儿,非常的麻烦,而利用 Redis 的 SortSet(有序集合)数据结构能够简单的搞定。
  • 计算器 / 限速器,利用 Redis 中原子性的自增操作,可以统计类似用户点赞数、用户访问数等,这类操作如果用 MySQL,频繁的读写会带来相当大的压力;限速器比较典型的使用场景是限制某个用户访问某个 API 的频率,常用的有抢购时防止用户疯狂点击带来不必要的压力。
  • 好友关系,利用集合的一些命令,比如求交集、并集、差集等;可以方便搞定一些共同好 友、共同爱好之类的功能。
  • 简单消息队列,除了 Redis 自身的发布 / 订阅模式,也可以利用 List 来实现一个队列机制,比如:到货通知、邮件发送之类的需求,不需要高可靠,但是会带来非常大的 DB 压力,完全可以用 List 来完成异步解耦。
  • Session 共享,以 JSP 为例,默认 Session 是保存在服务器的文件中,如果是集群服务,同一个用户过来可能落在不同机器上,这就会导致用户频繁登陆;采用 Redis 保存 Session 后,无论用户落在那台机器上都能够获取到对应的 Session 信息。

1.3 Redis / Memcache / MongoDB 对比

Redis / Memcache / MongoDB 都是 NoSQL 数据库。

Redis 和 Memcache

  • Redis 和 Memcache 都是内存数据库;不过 Memcache 还可用于缓存其他东西,例如图片、视频等等。
  • Memcache 数据结构单一 key / value;Redis 更丰富一些,还提供 list,set, hash 等数据结构的存储,有效的减少网络 IO 的次数。
  • 虚拟内存 – Redis 当物理内存用完时,可以将一些很久没用到的 value 交换到磁盘。
  • 存储数据安全 – Memcache 挂掉后,数据没了(没有持久化机制);Redis 可以定期保存到磁盘(持久化)。
  • 灾难恢复 – Memcache 挂掉后,数据不可恢复;Redis 数据丢失后可以通过 RBD(将 Redis 在内存中的数据库记录定时 dump 到磁盘上进行持久化)或 AOF(将 Redis 的操作日志以追加的方式写入文件)恢复。

Redis 和 MongoDB

  • Redis 和 MongoDB 并不是竞争关系,更多的是一种协作共存的关系。
  • MongoDB 是一个基于分布式文件存储的数据库,是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的。
  • MongoDB 本质上还是硬盘数据库,在复杂查询时仍然会有大量的资源消耗,而且在处理复杂逻辑时仍然要不可避免地进行多次查询。
  • MongoDB 需要 Redis 或 Memcache 这样的内存数据库来作为中间层进行缓存和加速。
  • 比如在某些复杂页面的场景中,整个页面的内容如果都从 MongoDB 中查询,可能要几十个查询语句,耗时很长;如果需求允许,则可以把整个页面的对象缓存至 Redis 中,定期更新;这样 MongoDB 和 Redis 就能很好地协作起来。

1.4 分布式数据库 CAP 原理

CAP 简介

传统的关系型数据库事务具备 ACID:

  • Atomicity 原子性
  • Consistency 一致性
  • Isolation 独立性
  • Durability 持久性

分布式数据库的 CAP:

  • Consistency - 强一致性
    All nodes see the same data at the same time,更新操作成功并返回客户端后,所有节点在同一时间的数据完全一致,这就是分布式的一致性;一致性的问题在并发系统中不可避免,对于客户端来说,一致性指的是并发访问时更新过的数据如何获取的问题;从服务端来看,则是更新如何复制分布到整个系统,以保证数据最终一致。

  • Availability - 高可用性
    可用性指 Reads and writes always succeed,即服务一直可用,而且要是正常的响应时间;好的可用性主要是指系统能够很好的为用户服务,不出现用户操作失败或者访问超时等用户体验不好的情况。

  • Partition Tolerance - 分区容错性
    即分布式系统在遇到某节点或网络分区故障时,仍然能够对外提供满足一致性或可用性的服务;分区容错性要求能够让应用,虽然是一个分布式系统,但看上去却是一个可以运转正常的整体;比如现在的分布式系统中有某一个或者几个机器宕掉了,其他剩下的机器还能够正常运转满足系统需求,对于用户而言并没有什么体验上的影响。

CAP 理论

CAP 理论提出就是针对分布式数据库环境的,所以,P 这个属性必须容忍它的存在,而且是必须具备的。

因为 P 是必须的,那么需要选择的就是 A 和 C。

在分布式环境下,为了保证系统可用性,通常都采取了复制的方式,避免一个节点损坏,导致系统不可用。那么就出现了每个节点上的数据出现了很多个副本的情况,而数据从一个节点复制到另外的节点时需要时间和要求网络畅通的,所以,当 P 发生时,也就是无法向某个节点复制数据时,这时候你有两个选择:

  • 选择可用性 A,此时,那个失去联系的节点依然可以向系统提供服务,不过它的数据就不能保证是同步的了(失去了 C 属性)。
  • 选择一致性 C,为了保证数据库的一致性,必须等待失去联系的节点恢复过来,在这个过程中,那个节点是不允许对外提供服务的,这时候系统处于不可用状态(失去了 A 属性)。

最常见的例子是读写分离,某个节点负责写入数据,然后将数据同步到其它节点,其它节点提供读取的服务,当两个节点出现通信问题时,就面临着选择 A - 继续提供服务,但是数据不保证准确,C - 用户处于等待状态,一直等到数据同步完成。

CAP 总结

分区是常态,不可避免,三者不可共存。

可用性和一致性:

  • 一致性高,可用性低
  • 一致性低,可用性高

因此,根据 CAP 原理将 NoSQL 数据库分成了满足 CA 原则、满足 CP 原则和满足 AP 原则三 大类:

  • CA - 单点集群(非分布式),满足一致性,可用性的系统,通常在可扩展性上不太强大。
  • CP - 满足一致性,分区容忍性的系统,通常性能不是特别高。
  • AP - 满足可用性,分区容忍性的系统,通常可能对一致性要求低一些。

2. 下载与安装

2.1 下载

Redis:http://www.redis.net.cn

图形工具:https://redisdesktop.com/download或者:https://github.com/uglide/RedisDesktopManager/releases/tag/0.9.3

   

2.2 安装

虽然可以在安装在 windows 操作系统,但是官方不推荐,所以安装在 Linux 系统中。

1)上传 tar.gz 包 到 /opt 目录下,并解压:

tar -zxvf redis-5.0.4.tar.gz

2)安装 gcc(必须有网络):

yum -y install gcc

忘记是否安装过,可以使用 gcc -v 命令查看 gcc 版本,如果没有安装过,会提示命令不存在。

3)进入 Redis 目录,进行编译:

make

4)编译之后,开始安装:

make install

2.3 安装后的操作

2.3.1 后台运行方式

Redis 默认不会使用后台运行,如果需要让它在后台运行,可将配置文件daemonize=no修改为daemonize=yes,当后台服务启动的时候,会写成一个进程文件运行。

打开配置文件:

vim /opt/redis-5.0.4/redis.conf

注释掉 bind,关闭保护模式,并修改为后台启动:

...
# ~~~ WARNING ~~~ If the computer running Redis is directly exposed to the
# internet, binding to all the interfaces is dangerous and will expose the
# instance to everybody on the internet. So by default we uncomment the
# following bind directive, that will force Redis to listen only into
# the IPv4 loopback interface address (this means Redis will be able to
# accept connections only from clients running into the same computer it
# is running).
#
# IF YOU ARE SURE YOU WANT YOUR INSTANCE TO LISTEN TO ALL THE INTERFACES
# JUST COMMENT THE FOLLOWING LINE.
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# bind 127.0.0.1

...

# By default protected mode is enabled. You should disable it only if
# you are sure you want clients from other hosts to connect to Redis
# even if no authentication is configured, nor a specific set of interfaces
# are explicitly listed using the "bind" directive.
protected-mode no

...

# By default Redis does not run as a daemon. Use 'yes' if you need it.
# Note that Redis will write a pid file in /var/run/redis.pid when daemonized.
daemonize yes

...

以配置文件的方式启动:

cd /usr/local/bin
redis-server /opt/redis-5.0.4/redis.conf
29674:C 02 Oct 2021 02:56:47.338 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
29674:C 02 Oct 2021 02:56:47.338 # Redis version=5.0.4, bits=64, commit=00000000, modified=0, pid=29674, just started
29674:C 02 Oct 2021 02:56:47.338 # Configuration loaded

防火墙开放 Redis 的端口号:

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

2.3.2 关闭数据库

  • 单实例关闭
redis-cli shutdown
  • 多实例关闭
redis-cli -p 6379 shutdown

2.3.3 常用操作

  • 检测 6379 端口是否在监听
netstat -lntp | grep 6379
  • 检测后台进程是否存在
ps -ef | grep redis

2.3.4 连接 Redis 并测试

redis-cli
ping

Redis 在 linux 支持命令补全(tab)

2.3.5 HelloWorld

# 保存数据
set k1 china   
# 获取数据
get kl 

2.3.6 测试性能

先 ctrl + c,退出 Redis 客户端

redis-benchmark 

执行命令后,命令不会自动停止,需要手动 ctrl+c 停止测试

====== PING_INLINE ======
  100000 requests completed in 2.52 seconds # 2.52秒处理了10万个请求,性能要看笔记 本的配置高低
  50 parallel clients
  3 bytes payload
  keep alive: 1

62.39% <= 1 milliseconds
99.95% <= 2 milliseconds
99.97% <= 3 milliseconds
100.00% <= 3 milliseconds
39682.54 requests per second # 每秒处理的请求数量

...

2.3.7 默认 16 个数据库

vim /opt/redis-5.0.4/redis.conf
...

# Set the number of databases. The default database is DB 0, you can select
# a different one on a per-connection basis using SELECT <dbid> where
# dbid is a number between 0 and 'databases'-1
databases 16

...
127.0.0.1:6379> get k1                 # 查询 k1
"china"
127.0.0.1:6379> select 16              # 切换 16 号数据库
(error) ERR DB index is out of range   # 数据库的下标超出了范围
127.0.0.1:6379> select 15              # 切换 15 号数据库
OK
127.0.0.1:6379[15]> get k1             # 查询 k1
(nil)
127.0.0.1:6379[15]> select 0           # 切换 0 号数据库
OK
127.0.0.1:6379> get k1                 # 查询 k1
"china"

2.3.8 数据库键的数量

dbsize

2.3.9 清空数据库

  • 清空当前库
flushdb
  • 清空所有(16个)库,慎用
flushall

2.3.10 模糊查询(keys)

模糊查询 keys 命令,有三个通配符:*?[]

* : 通配任意多个字符
       -- 查询所有的键:

keys *

      -- 模糊查询 k 开头,后面随便多少个字符:

keys k*

      -- 模糊查询 e 为最后一位,前面随便多少个字符:

keys *e

       -- 双 * 模式,匹配任意多个字符 - 查询包含 k 的键:

keys *k*

? : 通配单个字符
        -- 模糊查询 k 字头,并且匹配一个字符:

keys k?

        --只记得第一个字母是 k,长度是 3

keys k??

[] :通配括号内的某一个字符
        -- 记得其他字母,第二个字母可能是 a 或 e

keys r[ae]dis

2.3.11 键(key)

  • exists key : 判断某个 key 是否存在
127.0.0.1:6379[15]> EXISTS k1
(integer) 1     # 存在
127.0.0.1:6379[15]> EXISTS y1
(integer) 0     # 不存在
  • move key db :移动(剪切,粘贴)键到几号库
  
                    
127.0.0.1:6379[15]> MOVE x1 8    # 将 x1 移动到 8 号库
(integer) 1                      # 移动成功
127.0.0.1:6379[15]> EXISTS x1    # 查看当前库中是否存在 x1
(integer) 0                      # 不存在(因为已经移走了)
127.0.0.1:6379[15]> SELECT 8     # 切换 8 号库
OK
127.0.0.1:6379[8]> KEYS *        #查看当前库中的所有键
1) "x1"
  • ttl key : 查看键还有多久过期(-1 永不过期,-2 已过期);Time To Live 还能活多久
127.0.0.1:6379[8]> TTL x1
(integer) -1                # 永不过期
  • expire key 秒 :为键设置过期时间(生命倒计时)
127.0.0.1:6379[8]> set k1 v1       # 保存 k1
OK
127.0.0.1:6379[8]> TTL k1          # 查看 k1 的过期时间
(integer) -1                       # 永不过期
127.0.0.1:6379[8]> EXPIRE k1 10    # 设置 k1 的过期时间为 10 秒
(integer) 1                        # 设置成功
127.0.0.1:6379[8]> get k1          # 获取 k1
"v1"
127.0.0.1:6379[8]> TTL k1          # 查看 k1 的过期时间
(integer) 1                        # 还有 1 秒过期
127.0.0.1:6379[8]> get k1
(nil)                              # 从内存中销毁了
  • type key : 查看键的数据类型
127.0.0.1:6379[8]> type k1
string                             # k1 的数据类型是 string 字符串

3. 使用 Redis

3.1 五大数据类型 

操作文档:http://redisdoc.com/

redis无论什么数据类型,在数据库中都是以key-value形式保存,并且所有的key(键)都是字符串,所以讨论基础数据结构都是讨论的value值的数据类型。

3.1.1 字符串 String

String是redis最基本的类型,一个key对应一个value。

  • String类型是二进制安全的,意思是redis的string可以包含任何数据,比如jpg图片或者序列化的对象。
  • String类型是redis最基本的数据类型,一个redis中字符串value最多可以是512M

应用场景:

  • 缓存: 经典使用场景,把常用信息,字符串,图片或者视频等信息放到redis中,redis作为缓存层,mysql做持久化层,降低mysql的读写压力。
  • 计数器:redis是单线程模型,一个命令执行完才会执行下一个,同时数据可以一步落地到其他的数据源。
  • session:常见方案spring session + redis实现session共享。
  • set / get / del / append / strlen
127.0.0.1:6379[8]> set k1 v1     # 保存数据
OK
127.0.0.1:6379[8]> set k2 v2     # 保存数据
OK
127.0.0.1:6379[8]> keys *
1) "k2"
2) "k1"
127.0.0.1:6379[8]> del k2        # 删除数据 k2
(integer) 1
127.0.0.1:6379[8]> keys *
1) "k1"
127.0.0.1:6379[8]> get k1        # 获取数据 k1
"v1"
127.0.0.1:6379[8]> append k1 abc # 往 k1 的值追加数据 abc
(integer) 5                      # 返回值的长度(字符数量)
127.0.0.1:6379[8]> get k1
"v1abc"
127.0.0.1:6379[8]> strlen k1     # 返回 k1 值的长度(字符数量)
(integer) 5
  • incr / decr / incrby / decrby:加减操作,操作的必须是数字类型
    incr - increment(增加)
    decr - decrement(减少)
127.0.0.1:6379[8]> set k1 1       # 初始化 k1 的值为 1
OK
127.0.0.1:6379[8]> incr k1        # k1 自增 1(相当于 ++)
(integer) 2
127.0.0.1:6379[8]> incr k1
(integer) 3
127.0.0.1:6379[8]> get k1
"3"
127.0.0.1:6379[8]> decr k1        # k1 自减 1(相当于 --)
(integer) 2
127.0.0.1:6379[8]> decr k1
(integer) 1
127.0.0.1:6379[8]> get k1
"1"
127.0.0.1:6379[8]> INCRBY k1 3    # k1 自增 3(相当于 +=3)
(integer) 4
127.0.0.1:6379[8]> GET k1
"4"
127.0.0.1:6379[8]> DECRBY k1 2    # k1 自减 2(相当于 -=2)
(integer) 2
127.0.0.1:6379[8]> get k1
"2"
  • getrange / setrange:类似 between ... and ...

range:范围



127.0.0.1:6379[8]> SET k1 abcdef          # 初始化 k1 的值为 abcdef
OK
127.0.0.1:6379[8]> get k1
"abcdef"

127.0.0.1:6379[8]> getrange k1 0 -1       # 查询 k1 全部的值
"abcdef"

127.0.0.1:6379[8]> getrange k1 0 3        # 查询 k1 的值,范围是下标 0 ~ 下标 3(包含 0 和 3,共返回 4 个字符)
"abcd"

127.0.0.1:6379[8]> setrange k1 1 xxx      # 替换 k1 的值,从下标 1 开始替换为 xxx
(integer) 6
127.0.0.1:6379[8]> get k1
"axxxef"
  • setex / setnx

set with expir:添加数据的同时设置生命周期

127.0.0.1:6379[8]> SETEX k1 5 v1           # 添加 k1 v1 数据的同时,设置 5 秒的生命周期
OK
127.0.0.1:6379[8]> get k1
"v1"
127.0.0.1:6379[8]> get k1                   # 已过期,k1 的值 v1 自动销毁
(nil)

set if not exist:添加数据的时候判断是否已经存在,防止已存在的数据被覆盖掉


127.0.0.1:6379[8]> setnx k1 v1              # k1 不存在,添加成功
(integer) 1

127.0.0.1:6379[8]> setnx k1 zm              # 添加失败,因为 k1 已经存在 
(integer) 0
  • mset / mget / msetnx (m:更多)

127.0.0.1:6379[8]> set k1 v1 k2 v2          # set 不支持一次添加多条数据
(error) ERR syntax error

127.0.0.1:6379[8]> mset k1 v1 k2 v2 k3 v3   # mset 可以一次添加多条数据
OK
127.0.0.1:6379[8]> keys *
1) "k2"
2) "k1"
3) "k3"

127.0.0.1:6379[8]> mget k2 k3               # 一次获取多条数据
1) "v2"
2) "v3"

127.0.0.1:6379[8]> msetnx k3 v3 k4 v4       # 一次添加多条数据时,如果添加的数据中有已经存在的,则失败
(integer) 0

127.0.0.1:6379[8]> msetnx k4 v4 k5 v5       # 一次添加多条数据时,如果添加的数据中都不存在的,则成功
(integer) 1
  • getset:先 get 后 set

127.0.0.1:6379[8]> getset k6 v6             # 因为没有 k6,所以 get 为 null,然后将 k6 的值 v6 添加到数据库
(nil)
127.0.0.1:6379[8]> keys *
1) "k6"
2) "k2"
3) "k1"
4) "k4"
5) "k5"
6) "k3"
127.0.0.1:6379[8]> get k6
"v6"

127.0.0.1:6379[8]> getset k6 vv6           # 先获取 k6 的值,然后修改 k6 的值为 vv6
"v6"
127.0.0.1:6379[8]> get k6
"vv6"

3.1.2 列表 List

Redis中的List其实就是双端链表。

使用List结构,我们可以轻松地实现最新消息排队功能(比如新浪微博的TimeLine)。List的另一个应用就是消息队列,可以利用List的 PUSH 操作,将任务存放在List中,然后工作线程再用 POP 操作将任务取出进行执行。

应用场景:

  • 微博TimeLine: 有人发布微博,用lpush加入时间轴,展示新的列表信息。
  • 消息队列。 

使用列表的技巧: 

  • lpush+lpop=Stack(栈)
  • lpush+rpop=Queue(队列)
  • lpush+ltrim=Capped Collection(有限集合)
  • lpush+brpop=Message Queue(消息队列)
  • lpush / rpush / lrange

l:left 自左向右添加 (从上往下添加)
r:right 自右向左添加(从下往上添加)

# 从上往下添加
127.0.0.1:6379[1]> lpush list01 1 2 3 4 5
(integer) 5
127.0.0.1:6379[1]> keys *
1) "list01"

# 查询 list01 中的全部数据 0 表示开始,-1 表示结尾(队列,先进先出)
127.0.0.1:6379[1]> lrange list01 0 -1
1) "5"
2) "4"
3) "3"
4) "2"
5) "1"

# 从下往上添加
127.0.0.1:6379[1]> rpush list02 1 2 3 4 5
(integer) 5
127.0.0.1:6379[1]> lrange list02 0 -1
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
  • lpop / rpop:移除第一个元素(上左下右)
# 从左(上)边移除第一个元素
127.0.0.1:6379[1]> LPOP list02
"1"
# 查看list02里面,1没了
127.0.0.1:6379[1]> lrange list02 0 -1
1) "2"
2) "3"
3) "4"
4) "5"
# 从右(下)边移除第一个元素
127.0.0.1:6379[1]> RPOP list02
"5"
# 查看list02里面,5也没了
127.0.0.1:6379[1]> lrange list02 0 -1
1) "2"
2) "3"
3) "4"
  • lindex:根据下标查询元素(从左向右,自上而下)
127.0.0.1:6379[1]> lrange list01 0 -1
1) "5"
2) "4"
3) "3"
4) "2"
5) "1"
# 从上到下数,下标为 2 的值
127.0.0.1:6379[1]> lindex list01 2
"3"
# 从上到下数,下标为 1 的值
127.0.0.1:6379[1]> lindex list01 1
"4"
  • llen:返回集合的长度
127.0.0.1:6379[1]> llen list01
(integer) 5
  • lrem:删除 n 个 value
127.0.0.1:6379[1]> lpush list01 1 2 2 3 3 3 4 4 4 4
(integer) 10
# 从 list01 中移除 2 个 3
127.0.0.1:6379[1]> lrem list01 2 3
(integer) 2
127.0.0.1:6379[1]> lrange list01 0 -1
 1) "4"
 2) "4"
 3) "4"
 4) "4"
 5) "3"
 6) "2"
 7) "2"
 8) "1"
  • ltrim:截取指定范围的值,别的全扔掉

ltrim key begindex endindex

127.0.0.1:6379[1]> lpush list01 1 2 3 4 5 6 7 8 9
(integer) 9
127.0.0.1:6379[1]> lrange list01 0 -1
1) "9"
2) "8"
3) "7"
4) "6"
5) "5"
6) "4"
7) "3"
8) "2"
9) "1"
# 截取下标 3 ~ 6 的值,别的全扔掉
127.0.0.1:6379[1]> ltrim list01 3 6
OK
127.0.0.1:6379[1]> lrange list01 0 -1
1) "6"
2) "5"
3) "4"
4) "3"
  • rpoplpush:从一个集合搞一个元素到另一个集合中(右出一个,左进一个)
127.0.0.1:6379[1]> rpush list01 1 2 3 4 5
(integer) 5
127.0.0.1:6379[1]> lrange list01 0 -1
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
127.0.0.1:6379[1]> rpush list02 1 2 3 4 5
(integer) 5
127.0.0.1:6379[1]> lrange list02 0 -1
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
# list01 右边(下面)出一个,从左(上面)进入到 list02 的第一个位置
127.0.0.1:6379[1]> RPOPLPUSH list01 list02
"5"
127.0.0.1:6379[1]> LRANGE list01 0 -1
1) "1"
2) "2"
3) "3"
4) "4"
127.0.0.1:6379[1]> LRANGE list02 0 -1
1) "5"
2) "1"
3) "2"
4) "3"
5) "4"
6) "5"
  • lset:改变某个下标的某个值
    lset key index value
127.0.0.1:6379[1]> lrange list02 0 -1
1) "5"
2) "1"
3) "2"
4) "3"
5) "4"
6) "5"
# 将 list02 中下标为 0 的元素修改成 x
127.0.0.1:6379[1]> lset list02 0 x
OK
127.0.0.1:6379[1]> lrange list02 0 -1
1) "x"
2) "1"
3) "2"
4) "3"
5) "4"
6) "5"
  • linsert:插入元素(指定某个元素之前 / 之后)
    linsert key before/after oldvalue newvalue
127.0.0.1:6379[1]> lrange list02 0 -1
1) "x"
2) "1"
3) "2"
4) "3"
5) "4"
6) "5"
# 从左边进入,在 list02 中的 2 元素之前插入 java
127.0.0.1:6379[1]> linsert list02 before 2 java
(integer) 7
127.0.0.1:6379[1]> lrange list02 0 -1
1) "x"
2) "1"
3) "java"
4) "2"
5) "3"
6) "4"
7) "5"
 # 从左边进入,在 list02 中的 2 元素之后插入 redis
127.0.0.1:6379[1]> linsert list02 after 2 redis
(integer) 8
127.0.0.1:6379[1]> lrange list02 0 -1
1) "x"
2) "1"
3) "java"
4) "2"
5) "redis"
6) "3"
7) "4"
8) "5"

列表性能总结:类似添加火车皮一样,头尾操作效率高,中间操作效率惨

3.1.3 集合 Set

与 Java 中的 set 特点类似,无序且不允许重复

同时由于集合与列表相比是无序的,所以不能通过下标索引来操作元素。

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

应用场景:

  • 标签(tag),给用户添加标签,或者用户给消息添加标签,这样有同一标签或者类似标签的可以给推荐关注的事或者关注的人。
  • 点赞,或点踩,收藏等,可以放到set中实现。
  • sadd / smembers / sismember:添加 / 查看 / 判断是否存在
# 添加元素(自动排除重复元素)
127.0.0.1:6379[1]> sadd set01 1 2 2 3 3 3
(integer) 3
# 查询 set01 集合
127.0.0.1:6379[1]> smembers set01
1) "1"
2) "2"
3) "3"
127.0.0.1:6379[1]> sismember set01 2
(integer) 1  # 存在
127.0.0.1:6379[1]> sismember set01 5
(integer) 0  # 不存在

注意:1 和 0 不是下标,而是布尔值;1 - true 存在,0 - false 不存在

  • scard:获得集合中的元素个数
# 集合中有 3 个元素
127.0.0.1:6379[1]> scard set01
(integer) 3
  • srem:删除集合中的元素
    srem key value
# 移除 set01 中的元素 2; 1 表示移除成功
127.0.0.1:6379[1]> srem set01 2
(integer) 1
127.0.0.1:6379[1]> smembers set01
1) "1"
2) "3"
  • srandmember:从集合中随机获取几个元素
    srandmember 整数(个数)
127.0.0.1:6379[1]> sadd set01 1 2 3 4 5 6 7 8 9
(integer) 9
127.0.0.1:6379[1]> smembers set01
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
6) "6"
7) "7"
8) "8"
9) "9"
# 从 set01 中随机获取 3 个元素
127.0.0.1:6379[1]> srandmember set01 3
1) "2"
2) "3"
3) "9"
# 从 set01 中随机获取 5 个元素
127.0.0.1:6379[1]> srandmember set01 5
1) "8"
2) "9"
3) "3"
4) "1"
5) "6"
  • spop:随机出栈(移除)
127.0.0.1:6379[1]> smembers set01
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
6) "6"
7) "7"
8) "8"
9) "9"
# 随机移除一个元素
127.0.0.1:6379[1]> spop set01
"8"
# 随机移除一个元素
127.0.0.1:6379[1]> spop set01
"1"
  • smove:移动元素 - 将 key1 某个值赋值给 key2
127.0.0.1:6379[1]> SADD set01 1 2 3 4 5
(integer) 5
127.0.0.1:6379[1]> SADD set02 x y z
(integer) 3
# 将 set01 中的元素 3 移动到 set02 中
127.0.0.1:6379[1]> smove set01 set02 3
(integer) 1
127.0.0.1:6379[1]> smove set01 set02 5
(integer) 1
127.0.0.1:6379[1]> SMEMBERS set01
1) "1"
2) "2"
3) "4"
  • 数学集合类

       \circ 交集 - sinter

       \circ并集 - sunion

       \circ 差集 - sdiff

127.0.0.1:6379[1]> SADD set01 1 2 3 4 5
(integer) 5
127.0.0.1:6379[1]> SADD set02 2 a 1 b 3
(integer) 5
127.0.0.1:6379[1]> SINTER set01 set02
1) "1"
2) "2"
3) "3"
127.0.0.1:6379[1]> SUNION set01 set02
1) "b"
2) "1"
3) "a"
4) "5"
5) "2"
6) "3"
7) "4"
# 在 set01 中存在,在 set02 中不存在
127.0.0.1:6379[1]> sdiff set01 set02
1) "4"
2) "5"
# 在 set02 中存在,在 set01 中不存在
127.0.0.1:6379[1]> sdiff set02 set01
1) "b"
2) "a"

3.1.4 哈希 hash

类似 java 里面的 Map<String, Object>。

Key / Value(键值对)模式不变,但 Value(值)又是一个键值对。

应用场景:

  • 缓存: 更直观,相比string更节省空间的维护缓存信息,如用户信息,视频信息等。
  • hset / hget / hmset / hmget / hgetall / hdel:添加 / 得到 / 多添加 / 多得到 / 得到全部 / 删除属性
# 添加 user,那user就是key,value值为 id=1001
127.0.0.1:6379[1]> HSET user id 1001
(integer) 1
127.0.0.1:6379[1]> HGET user
(error) ERR wrong number of arguments for 'hget' command

# 查询 user,必须指明具体的字段
127.0.0.1:6379[1]> HGET user id
"1001"

# 添加学生 student,属性一堆
127.0.0.1:6379[1]> HMSET student id 101 name tom age 22
OK

# 获取学生名字
127.0.0.1:6379[1]> HGET student name
"tom"

# 获取学生名字和年龄
127.0.0.1:6379[1]> HMGET student name age
1) "tom"
2) "22"

# 获取学生全部信息
127.0.0.1:6379[1]> HGETALL student
1) "id"
2) "101"
3) "name"
4) "tom"
5) "age"
6) "22"

# 删除学生年龄属性
127.0.0.1:6379[1]> HDEL student age
(integer) 1
127.0.0.1:6379[1]> HGETALL student
1) "id"
2) "101"
3) "name"
4) "tom"
  • hlen:返回元素的属性个数
127.0.0.1:6379[1]> HGETALL student
1) "id"
2) "101"
3) "name"
4) "tom"

# student 属性的数量,id 和 name 共两个属性
127.0.0.1:6379[1]> HLEN student
(integer) 2
  • hexists:判断元素是否存在某个属性
# student 中是否存在 name 属性
127.0.0.1:6379[1]> HEXISTS student name
(integer) 1

# student 中是否存在 age 属性
127.0.0.1:6379[1]> HEXISTS student age
(integer) 0
  • hkeys / hvals:获得属性的所有 key / 获得属性的所有 value
# 获取 student 所有的属性名
127.0.0.1:6379[1]> HKEYS student
1) "id"
2) "name"

# 获取 student 所有属性的值(内容)
127.0.0.1:6379[1]> HVALS student
1) "101"
2) "tom"
  • hincrby / hincrbyfloat:自增(整数)/ 自增(小数)
# 使用flushdb先将库清空,再重新添加数据。
127.0.0.1:6379[1]> hmset student id 101 name tom age 22
OK
# 自增整数 2
127.0.0.1:6379[1]> HINCRBY student age 2
(integer) 24
127.0.0.1:6379[1]> HGET student age
"24"
127.0.0.1:6379[1]> HMSET user id 1001 money 1000
OK

# 自增小数 5.5
127.0.0.1:6379[1]> HINCRBYFLOAT user money 5.5
"1005.5"
127.0.0.1:6379[1]> HGET user money
"1005.5"
  • hsetnx:添加的时候,先判断是否存在
# 添加失败,因为 age 已存在
127.0.0.1:6379[1]> HSETNX student age 18
(integer) 0

# 添加成功,因为 sex 不存在
127.0.0.1:6379[1]> HSETNX student sex male
(integer) 1

127.0.0.1:6379[1]> hgetall student
1) "id"
2) "101"
3) "name"
4) "tom"
5) "age"
6) "24"
7) "sex"
8) "male"

3.1.5 有序集合 Zset

Redis 有序集合和集合一样基本一致。

区别就是:Zset集合每个元素都会关联一个 double 类型的权重参数(score),使得集合中的元素能够按score进行有序排列。

有序集合的成员是唯一的,但分数(score)却可以重复。集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。

应用场景:

  • 排行榜:有序集合经典使用场景。例如小说视频等网站需要对用户上传的小说视频做排行榜,榜单可以按照用户关注数,更新时间,字数等打分,做排行。
  • 成绩排行:比如一个存储全班同学成绩的sorted set,其集合value可以是同学的学号,而score就可以是其考试得分, 形成了按成绩排序。
  • 权重分配:可以用sorted set来做带权重的队列,比如普通消息的score为1,重要消息的score为2,然后工作线程可以选择按score的倒序来获取工作任务。让重要的任务优先执行。

需求:

充 10 元可享 vip1;

充 20 元可享 vip2;

充 30 元可享 vip3;

...

  • zadd / zrange (withscores):添加 / 查询
127.0.0.1:6379> ZADD zset01 10 vip1 20 vip2 30 vip3 40 vip4 50 vip5
(integer) 5
# 查询数据
127.0.0.1:6379> zrange zset01 0 -1
1) "vip1"
2) "vip2"
3) "vip3"
4) "vip4"
5) "vip5"
# 带着分数查询数据
127.0.0.1:6379> zrange zset01 0 -1 withscores
 1) "vip1"
 2) "10"
 3) "vip2"
 4) "20"
 5) "vip3"
 6) "30"
 7) "vip4"
 8) "40"
 9) "vip5"
10) "50"
  • zrangebyscore:模糊查询
    ( :不包含
    limit:跳过几个截取几个
# 20 <= score <= 40
127.0.0.1:6379> ZRANGEBYSCORE zset01 20 40
1) "vip2"
2) "vip3"
3) "vip4"

# 20 <= score < 40
127.0.0.1:6379> ZRANGEBYSCORE zset01 20 (40
1) "vip2"
2) "vip3"

# 20 < score < 40
127.0.0.1:6379> ZRANGEBYSCORE zset01 (20 (40
1) "vip3"

# 10 <= score <= 40,共返回四个,跳过前 2 个,取 2 个
127.0.0.1:6379> ZRANGEBYSCORE zset01 10 40 limit 2 2
1) "vip3"
2) "vip4"

# 20 <= score <= 40,共返回四个,跳过前 2 个,取 1 个
127.0.0.1:6379> ZRANGEBYSCORE zset01 10 40 limit 2 1
1) "vip3"
  • zrem:删除元素
# 移除 vip5
127.0.0.1:6379> ZREM zset01 vip5
(integer) 1
127.0.0.1:6379> zrange zset01 0 -1
1) "vip1"
2) "vip2"
3) "vip3"
4) "vip4"
  • zcard / zcount / zrank / zscore:集合长度 / 范围内元素个数 / 获得元素下标 / 通过值获得分数
# 集合中元素的个数
127.0.0.1:6379> ZCARD zset01
(integer) 4

# 分数在 20 ~ 30之间,共有几个元素
127.0.0.1:6379> ZCOUNT zset01 20 30
(integer) 2

# vip3 在集合中的下标(从上向下)
127.0.0.1:6379> ZRANK zset01 vip3
(integer) 2

# 通过元素获得对应的分数
127.0.0.1:6379> ZSCORE zset01 vip2 
"20"
  • zrevrank:逆序找下标(从下向上)
127.0.0.1:6379> ZRANGE zset01 0 -1
1) "vip1"
2) "vip2"
3) "vip3"
4) "vip4"
127.0.0.1:6379> ZREVRANK zset01 vip3
(integer) 1
  • zrevrange:逆序查询
# 顺序查询
127.0.0.1:6379> ZRANGE zset01 0 -1
1) "vip1"
2) "vip2"
3) "vip3"
4) "vip4"

# 逆序查询
127.0.0.1:6379> ZREVRANGE zset01 0 -1
1) "vip4"
2) "vip3"
3) "vip2"
4) "vip1"
  • zrevrangebyscore:逆序范围查找
# 逆序查询分数在 30 ~ 20 之间的(注意,先写大值,再写小值)
127.0.0.1:6379> ZREVRANGEBYSCORE zset01 30 20
1) "vip3"
2) "vip2"

# 如果小值在前,则结果为 null(empty list or set)
127.0.0.1:6379> ZREVRANGEBYSCORE zset01 20 30
(empty list or set)

3.3 Redis 持久化

3.3.1 RDB

Redis DataBase:

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

默认保存在 /usr/local/bin 中,文件名 dump.rdb。

3.3.1.1 自动备份

redis 是内存数据库,当我们每次用完 redis ,关闭 linux 时,按道理来说,内存释放, redis 中的数
据也会随之消失。为什么我们再次启动redis 的时候,昨天的数据还在,并没有消失呢?
正是因为,每次关机时, redis 会自动将数据备份到一个文件中 : /usr/local/bin/ dump.rdb

1. 默认的自动备份策略不利于测试,所以修改 redis.conf 文件中的自动备份策略

vim /opt/redis-5.0.4/redis.conf

进入 vim 编辑器的底行模式,输入 /SNAPSHOTTING 并点击回车键进行搜索,修改 SNAPSHOTTING 下的备份策略:

save 900 1      # 900 秒内,至少变更 1 次,才会自动备份
save 120 10     # 120 秒内,至少变更 10 次,才会自动备份
save 60 10000   # 60 秒内,至少变更 10000 次,才会自动备份

注:如果只是用 Redis 的缓存功能,不需要持久化,那么可以注释掉所有的 save 行来停用保存功能,可以直接一个空字符串来实现停用:save " "。

重启 Redis-Server

/usr/local/bin/redis-server /opt/redis-5.0.4/redis.conf 

2. 使用 shutdown 关闭 Redis 来模拟关机;关机之前和关机之后,对比 dump.rdb 文件的更新时间

[root@localhost bin]# redis-cli 
127.0.0.1:6379> ping
PONG
127.0.0.1:6379> keys *
1) "zset01"
127.0.0.1:6379> set k2 v2
OK
127.0.0.1:6379> SHUTDOWN
not connected> exit

注意:当使用 shutdown 命令,Redis 会自动将数据库备份,所以 dump.rdb 文件创建时间更新了。

3. 启动 Redis,要在 120 秒内改变 10 条数据,再查看 dump.rdb 文件的更新时间(开两个终端窗口,方便查看)。

cd /usr/local/bin
ll | grep dump

4. 120 秒内改变 10 条数据这一动作触发了备份指令,目前,/usr/local/bin/dump.rdb 文件中保存了 10 条数据,将 dump.rdb 拷贝一份 dump10.rdb,此时两个文件中都保存 10 条数据。

5. 数据已经备份了,这时清空全部数据库 flushall,再次 shutdown 模拟关机。

6. 再次启动 Redis,发现数据消失了,dump.rdb 文件中的内容并没有恢复到 Redis 中。why?

因为,当保存 10 条以上的数据时,数据备份起来了;

然后删除数据库,备份文件中的数据,也没问题;

但是,这个 shutdown 命令一旦执行,就会立刻备份,将删除之后的空数据库生成备份文件,将之前 10 条数据的备份文件覆盖掉了。所以,自动恢复失败。

为了解决这个问题,就要将备份文件再备份。

7. 将 dump.rdb 文件删除,将 dump10.rdb 重命名为 dump.rdb;

8. 重新启动 redis 服务,登录 redis,数据 10 条全部恢复。

3.3.1.2 手动备份

之前自动备份,必须更改好多数据,例如上边,改变了十多条数据,才会自动备份;

如果只保存一条数据想立刻备份,就需要每次操作完成,执行命令 save 就会立刻备份。

127.0.0.1:6379> set k1 x1
OK
127.0.0.1:6379> SAVE
OK

3.3.1.3 与 RDB 相关的配置

查看配置文件:

vim /opt/redis-5.0.4/redis.conf 

快照 SNAPSHOTTING 下的配置解释如下:

\bullet  stop-writes-on-bgsave-error:开启备份后如果发生故障是否继续接受写请求。

          \circ yes - 当后台备份时候反生错误,前台停止写入;

          \circ no - 当后台备份时候反生错误,前台继续写入。

\bullet  rdbcompression:对于存储到磁盘中的快照,是否启动 LZF 压缩算法,一般都会启动;因为这点性能,多买一台电脑,完全搞定 N 个来回了。       

          \circ yes - 启动;                    

          \circ no - 不启动(不想消耗 CPU 资源,可关闭)。

\bullet  rdbchecksum:在存储快照后,是否启动 CRC64 算法进行数据校验。

          \circ 开启后,大约增加 10% 左右的 CPU 消耗; 

          \circ 如果希望获得最大的性能提升,可以选择关闭。

 \bullet dbfilename:快照备份文件名字,默认为 dump.rdb。

 \bullet dir:快照备份文件保存的目录,默认为当前目录。

优:适合大规模数据恢复,对数据完整性和一致性要求不高。

劣:一定间隔备份一次,意外 down 掉,就失去最后一次快照的所有修改。

3.3.2 AOF

Append Only File

以日志的形式记录每个写操作;

将 Redis 执行过的写指令全部记录下来(读操作不记录);

只允许追加文件,不可以改写文件;

Redis 在启动之初会读取该文件从头到尾执行一遍,这样来重新构建数据。

3.3.2.1 开启 AOF

1)为了避免失误,最好将 redis.conf 总配置文件备份一下,然后再修改内容如下:

appendonly yes

# The name of the append only file (default: "appendonly.aof")

appendfilename "appendonly.aof"

2)重新启动 Redis,以新配置文件启动

redis-server /opt/redis-5.0.4/redis.conf

3)连接 Redis,加数据,删库,退出

4)查看当前文件夹 /usr/local/bin 多一个 AOF 文件,看看文件中的内容,保存的都是写操作

           -- 文件中的删库语句要删除,否则数据恢复不了。

           -- 编辑这个文件,如果不是 root 权限则要底行模式输入 wq! 强制执行

5)最后只需要重新连接,数据恢复成功

3.3.2.2 AOF 和 RDB 的优先级

查看 redis.conf 文件,AOF 和 RDB 两种备份策略可以同时开启,那系统会怎样选择?

1)编辑 appendonly.aof,修改为乱码,保存退出;

2) 启动 Redis 失败;

所以是 AOF 优先载入来恢复原始数据;因为 AOF 比 RDB 数据保存的完整性更高。

3)修复 AOF 文件,删除不符合 Redis 语法规范的代码;

reids-check-aof  --fix appendonly.aof

3.3.2.3 与 AOF 相关的配置

查看配置文件:

vim /opt/redis-5.0.4/redis.conf 

文件追写模式 APPEND ONLY MODE 下的配置解释如下:

 \bullet appendonly:开启 AOF 模式 。

 \bullet appendfilename:AOF 的文件名字,建议不修改。

 \bullet appendfsync:追写策略。                                       

         \circ always - 每次数据变更,就会立即记录到磁盘,性能较差,但数据完整性好;  

         \circ everysec - 默认设置,异步操作,每秒记录,如果一秒内宕机,会有数据丢失; 

         \circ no - 不追写。

 \bullet no-appendfsync-on-rewrite:当后台某个子线程在重写或保存时,主程序不执行追写策略,防止出现响应延迟问题。但是,为保证数据的安全性,默认 no 即可。     

         \circ AOF 采用文件追加的方式,文件会越来越大,为了解决这个问题,增加了重写机制,Redis 会自动记录上一次 AOF 文件的大小,当 AOF 文件大小达到预先设定的 大小时,Redis 就会启动 AOF 文件进行内容压缩,只保留可以恢复数据的最小指令集合。
 

 \bullet auto-aof-rewrite-percentage:一般设置为 AOF 文件大小已经超过原来的 100%,也就是一倍,才重写压缩。

 \bullet auto-aof-rewrite-min-size:一般设置为 AOF 文件已经超过了 64 MB,才重写压缩。这样可以防止出现文件比较小的时候出现无必要的重写。

3.3.3 总结

RDB:只用作后备用途,建议 15 分钟备份一次就好。

AOF:

  • 在最恶劣的情况下,也只丢失不超过 2 秒的数据,数据完整性比较高,但代价太大,会带来持续的 IO;
  • 对硬盘的大小要求也高,默认 64 MB 太小了,企业级最少都是 5 G 以上;
  • Master / Slave(主/从)方案是更优的选择
     

3.4 NoSQL 事务

可以一次执行多个命令,是一个命令组,一个事务中,所有命令都会序列化(排队),不会被插队;一个队列中,一次性,顺序性,排他性的执行一系列命令。

三特性:

  • 隔离性 - 所有命令都会按照顺序执行,事务在执行的过程中,不会被其他客户端的命令打断。
  • 没有隔离级别 - 队列中的命令没有提交之前都不会被实际的执行,不存在类似“事务中查询要看到事务里的更新,事务外查询不能看到”的这些问题。
  • 不保证原子性 - 如果一个命令失败,但是别的命令可能会执行成功,没有回滚。

三步走:

  • 开启 multi
  • 入队 queued
  • 执行 exec

与关系型数据库事务相比:

  • multi - 可以理解成关系型事务中的 Begin
  • exec - 可以理解成关系型事务中的 Commit
  • discard - 可以理解成关系型事务中的 Rollback

3.4.1 开启事务

开启事务,加入队列,一起执行,并成功

# 开启事务
127.0.0.1:6379> multi
OK

# 加入队列
127.0.0.1:6379> set k1 v1
QUEUED
# 加入队列
127.0.0.1:6379> set k2 v2
QUEUED
# 加入队列
127.0.0.1:6379> get k2
QUEUED
# 加入队列
127.0.0.1:6379> set k3 v3
QUEUED

# 执行,一起成功
127.0.0.1:6379> exec
1) OK
2) OK
3) "v2"
4) OK

3.4.2 放弃操作(回滚)

放弃之前的操作,恢复到原来的值

# 开启事务
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET k1 v1111
QUEUED
127.0.0.1:6379> SET k2 v2222
QUEUED

# 放弃操作
127.0.0.1:6379> DISCARD
OK

# 还是原来的值
127.0.0.1:6379> get k1
"v1"

3.4.3 报错恢复

一句报错,全部取消,恢复到原来的值

127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k4 v4
QUEUED

# 一句报错
127.0.0.1:6379> setvcdnkaj
(error) ERR unknown command `setvcdnkaj`, with args beginning with: 
127.0.0.1:6379> set k5 v5
QUEUED

# 队列中命令全部取消
127.0.0.1:6379> exec
(error) EXECABORT Transaction discarded because of previous errors.

# 还是原来的值
127.0.0.1:6379> keys *
1) "k1"
2) "k3"
3) "k2"

3.4.4 延迟报错

追究责任,谁报的错,找谁去

127.0.0.1:6379> MULTI
OK
# 虽然 v1 不能 ++,但是加入队列并没有报错,类似 java 中的通过编译
127.0.0.1:6379> INCR k1
QUEUED
127.0.0.1:6379> set k4 v4
QUEUED
127.0.0.1:6379> set k5 v5
QUEUED

# 真正执行的时候,报错
127.0.0.1:6379> EXEC
1) (error) ERR value is not an integer or out of range
2) OK
3) OK
127.0.0.1:6379> KEYS *
1) "k2"
2) "k1"
3) "k3"
4) "k4"
5) "k5"

3.4.5 Watch 监控

Watch 命令用于监视一个 (或多个) key ,如果在事务执行之前这个 (或这些) key 被其他命令所改动,那么事务将被打断。

模拟收入与支出。

  • 正常情况下
# 收入 100 元
127.0.0.1:6379> set in 100 
OK
# 支出 0 元
127.0.0.1:6379> set out 0
OK
127.0.0.1:6379> multi
OK
# 收入 -20
127.0.0.1:6379> decrby in 20
QUEUED
# 支出 +20
127.0.0.1:6379> incrby out 20
QUEUED
# 结果,没问题
127.0.0.1:6379> exec
1) (integer) 80
2) (integer) 20
  • 特殊情况下

在开启事务之前,先执行 watch 监控收入 in:

127.0.0.1:6379> watch in
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> decrby in 20
QUEUED
127.0.0.1:6379> incrby out 20
QUEUED

在 exec 之前,开启了另一个窗口(线程),对监控的 in 做了修改:

127.0.0.1:6379> set in 1000
OK
127.0.0.1:6379> get in
"1000"

第一个窗口的事务将被打断(失效),类似于“乐观锁”:

127.0.0.1:6379> exec
(nil)
  • unwatch:取消 watch 命令对所有 key 的操作。

注:如果加了unwatch,那么一旦执行了 exec 命令,之前加的所有监控将会自动失效。

3.5 Redis 的发布订阅

进程间的一种消息通信模式:发送者 publish 发送消息,订阅者 subscribe 接收消息。

例如:微信订阅号。

订阅一个或多个频道。

一个窗口作为订阅者 Subscriber

127.0.0.1:6379> clear
# 1.订阅三个频道
127.0.0.1:6379> SUBSCRIBE cctv1 cctv2 cctv3
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "cctv1"
3) (integer) 1
1) "subscribe"
2) "cctv2"
3) (integer) 2
1) "subscribe"
2) "cctv3"
3) (integer) 3

另一个窗口作为发送者 Publisher

# 2.发送消息
127.0.0.1:6379> PUBLISH cctv3 NBA
(integer) 1
127.0.0.1:6379> PUBLISH cctv1 good
(integer) 1

第一个窗口的订阅者收到消息:

# 3.接收到推送过来的信息
1) "message"
2) "cctv3"
3) "NBA"
1) "message"
2) "cctv1"
3) "good"

3.6 主从复制

就是 Redis 集群的策略。

配从(库)不配主(库):从库可以选择谁是主库,但主库不能选择从库。

读写分离:主机写,从机读。

3.6.1 一主二从

1)准备三台服务器(192.168.186.128,192.168.186.129,192.168.186.130),并修改 redis.conf 配置文件。(默认ip为;127.0.0.1,也就是默认只允许本机访问,但这里配置了三台为了让三台服务器可以互相访问,要将其修改成如下配置)

bind 0.0.0.0

2)启动三台 Redis,并查看每台机器的角色,都是 Master。

[root@localhost redis-5.0.4]# /usr/local/bin/redis-cli 
127.0.0.1:6379> INFO replication
# Replication
role:master
connected_slaves:0
master_replid:68eda0fe7309a2dca0a1d4f7ac8ca53682c42142
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

3)测试开始

首先,将三个机器的 Redis 数据库全都清空,第一台(198.168.186.128)添加值:

127.0.0.1:6379> MSET k1 v1 k2 v2
OK

其余两台机器,复制(找主库),下面的命令就相当于是“我”要去认198.168.186.128这台服务器做"大哥"(主库),而自己变成了“小弟”(从库):

127.0.0.1:6379> SLAVEOF 192.168.186.128 6379
OK

第一台再添加值:

127.0.0.1:6379> set k3 v3
OK

通过这个测试思考几个问题:

思考1slave之前的k1k2是否能拿到?

     \circ 可以获得,只要跟了大哥(主库),之前的数据也会立刻同步。

思考 2 slave 之后的 k3 是否能拿到?
     \circ 可以获得,只要跟了大哥(主库 ),数据会立刻同步。 
思考 3 同时添加 k4 ,结果如何?
     \circ 主机(128master )可以添加成功,从机( 129 130 slave )失败,从机只负责读取
         数据,无权写入数据,这就是 读写分离 ”。
思考 4 主机 shutdown ,从机如何?
     \circ 129和 130 仍然是 slave ,并显示他们的 master 已离线。
思考 5 主机重启,从机又如何?
      \circ 129和 130 仍然是 slave ,并显示他们的 master 已上线。
思考 6 从机死了,主机如何?从机归来身份是否变化?
      \circ 关闭一个从机 129,主机 128 没有变化,只是显示少了一个 slave。
       \circ 主机和从机没有变化,而重启归来的从机自立门户成为了master ,不和原来的集群在              一 起了。

3.6.2 主从继承

一个主机理论上可以多个从机,但是这样的话,这个主机会很累。

可以使用 java 面向对象继承中的传递性来解决这个问题,减轻主机的负担。

129 跟随 128:

127.0.0.1:6379> SLAVEOF 192.168.186.128 6379
OK

130 跟随 129:

127.0.0.1:6379> SLAVEOF 192.168.186.129 6379
OK

128:

127.0.0.1:6379> INFO replication
# Replication
role:master
connected_slaves:1
slave0:ip=192.168.186.129,port=6379,state=online,offset=1120,lag=0
master_replid:dc6fc5fcaf449d33dad91ecabe3ba28d07dd1926
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:1120
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:1120

129:

127.0.0.1:6379> INFO replication
# Replication
role:slave
master_host:192.168.186.128
master_port:6379
master_link_status:up
master_last_io_seconds_ago:7
master_sync_in_progress:0
slave_repl_offset:1106
slave_priority:100
slave_read_only:1
connected_slaves:1
slave0:ip=192.168.186.130,port=6379,state=online,offset=1106,lag=0
master_replid:dc6fc5fcaf449d33dad91ecabe3ba28d07dd1926
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:1106
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:953
repl_backlog_histlen:154

130:

127.0.0.1:6379> INFO replication
# Replication
role:slave
master_host:192.168.186.129
master_port:6379
master_link_status:up
master_last_io_seconds_ago:8
master_sync_in_progress:0
slave_repl_offset:1092
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:dc6fc5fcaf449d33dad91ecabe3ba28d07dd1926
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:1092
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:1092

3.6.3 手选主机

1 个主机,2 个从机,当 1 个主机挂掉了,只能从 2 个从机中再次选 1 个主机。

手动选主机。

模拟测试:1 为 master,2 和 3 为 slave,当 1 挂掉后,2 选为 master,3 跟 2。

关闭主机 128,从机 129 执行命令成为主机:

slaveof no one

从机 130 执行命令跟随新主机 129:

SLAVEOF 192.168.186.129 6379

当 128 再次回归,128 和 129 已经形成新的集群,和 128 没有任何的关系了,所以 128 成为了光杆司令。

3.6.4 复制原理

1)Slave 服务器链接 Master 服务器,并发送同步请求。

2)Master 服务器启动后台的存盘进程,同时会收集所有写(Write)的命令集。

3)Master 服务器发送快照到 Slave 服务器。

4)Slave 服务器载入快照。

5)Master 服务器发送缓存写(Write)命令。

6)Slave 服务器执行命令。

完成上面几个步骤后就完成了 Slave 服务器数据初始化的所有操作,Slave 服务器此时可以接收来自用户的读请求。

  • 全量复制:Slave 初始化阶段,这时 Slave 需要将 Master 上的所有数据都复制一份,Slave 接收到数据文件后,存盘,并加载到内存中(步骤 1、2、3、4)。
  • 增量复制:Slave 初始化后,开始正常工作时主服务器发生的写操作同步到从服务器的过程(步骤 5、6)。

但只要是重新连接 Master,一次性(全量复制)同步将自动执行。

Redis 主从同步策略:主从刚刚连接的时候,进行全量同步;全量同步结束后,进行增量同步。

如果有需要,Slave 在任何时候都可以发起全量同步。

Redis 的策略是,无论如何,首先会尝试进行增量同步;如不成功,则要求 Slave 进行全量同步。

3.6.5 哨兵模式

哨兵 Sentinel 是 Redis 的高可用性解决方案:由一个或多个 Sentinel 实例组成的 Sentinel 系统可以监视任意多个主服务器,以及所有从服务器,并在被监视的主服务器进入下线状态时,自动将下线主服务器属下的某个从服务器升级为新的主服务器,然后由新的主服务器代替已下线的主服务器继续处理命令请求。
模拟测试:

1)128 为主服务器,129 和 130 为从服务器。

2)每一台服务器中在 /usr/local/bin 目录下创建一个配置文件 sentinel.conf,名字要正确,并编辑 sentinel.conf。

128 服务器 sentinel.conf:

#sentinel monitor 被监控主机名(自定义) ip port 票数
sentinel monitor redis128 192.168.186.128 6379 1

129 服务器 sentinel.conf:

sentinel monitor redis129 192.168.186.129 6379 1

130 服务器 sentinel.conf:

sentinel monitor redis130 192.168.186.130 6379 1

3)启动服务的顺序:启动 128 服务器为 Master(主) -> 启动 129 和 130 服务器为 Slave (从)-> 打开新的三个窗口分别启动 Redis 的哨兵 Sentinel 128、129、130。

/usr/local/bin/redis-sentinel sentinel.conf

4)将 128 挂掉,后台自动发起投票,选出新的主服务器。

127.0.0.1:6379> shutdown
not connected> exit

5)查看最后的分配:129 成为了新的主服务器,130 还是从服务器。

6)如果之前的主服务器再次归来:128 再次归来,自己成为了 master,和 129 平等;过了几秒之后,被哨兵检测到了 128 号机的归来,128 将变为 Slave。

42486:X 04 Oct 2021 10:56:08.098 # +sdown slave 192.168.186.128:6379 192.168.186.128 6379 @ redis128 192.168.186.129 6379
42486:X 04 Oct 2021 10:57:41.459 # -sdown slave 192.168.186.128:6379 192.168.186.128 6379 @ redis128 192.168.186.129 6379
42486:X 04 Oct 2021 10:57:51.434 * +convert-to-slave slave 192.168.186.128:6379 192.168.186.

缺点

所有的写操作都是在 master 上完成的,然后再同步到 slave 上,所以两台机器之间通信会有延迟。
当系统很繁忙的时候,延迟问题会加重。
Slave 机器数量增加,问题也会加重。

3.6.6 总配置 redis.conf 详解

执行 vim /opt/redis-5.0.4/redis.conf 直接查看所有配置信息的详解。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值