1.为什么使用 Nosql
1.1 单机 MySQL 时代
应用,访问,底层为数据库
90 年代。一个网站的访问量一般不会太大,单个数据库完全够用,那个时候,更多的使用的是静态网页 html,服务器也没有太大的压力!
但随着用户的增加,出现以下问题:
- 数据量太大,一台机器放不下
- 数据的索引(B+树),一台机器内存也放不下
- 访问量过大(读写混合),一台机器也承受不了
只要开始出现以上三种情况之一,那么就必须要晋级!
1.2 Memcached(缓存)+ 垂直拆分(Mysql 读写分离)
因为网站 80%的情况都是在读,每次都要去查询数据库的话就十分的麻烦!所以说我们希望减轻数据库的压力,我们可以使用缓存来保证效率!
优化过程经历了以下几个过程:
1.优化数据库的数据结构和索引(难度大)
2.文件缓存,通过 IO 流获取比每次都访问数据库效率略高,但是流量爆炸式增长时候,IO 流也承受不了
3.MemCache,当时最热门的技术,通过在数据库和数据库访问层之间加上一层缓存,第一次访问时查询数据库,将结果保存到缓存,
后续的查询先检查缓存,若有直接拿去使用,效率显著提升。
读写分离,并应用缓存:
1.3 分库分表 + 水平拆分(MySQL 集群)
1.4 目前基本的互联网项目
为什么要用 NoSQL?
Web2.0 时代产生的数据:用户的个人信息,社交网络,地理位置(定位)。用户自己产生的数据,用户日志等等爆发式增长!
这时候我们就需要使用 NoSQL 数据库,Nosql 可以很好的处理以上的情况!
2.什么是 Nosql
2.1 概述
NoSQL = Not Only SQL(不仅仅是 SQL)
关系型数据库:表:列+行,同一个表下数据的结构是一样的
非关系型数据库:数据存储没有固定的格式,并且可以进行横向扩展
NoSQL 泛指非关系型数据库,随着 web2.0 互联网的诞生,传统的关系型数据库很难对付 web2.0 时代!尤其是超大规模的高并发的社
区,暴露出来很多难以克服的问题,NoSQL 在当今大数据环境下发展的十分迅速,Redis 是发展最快的。
Nosql 特点:解耦!
- 方便扩展(数据之间没有关系,很好扩展)
- 大数据量高性能(Redis 一秒可以写 8 万次,读 11 万次,NoSQL 的缓存记录级,是一种细粒度的缓存,性能会比较高!)
- 数据类型是多样型的!(不需要事先设计数据库,随取随用
传统的 RDBMS 和 NoSQL 对比
传统的 RDBMS(关系型数据库)
- 结构化组织
- SQL
- 数据和关系都存在单独的表中 row col
- 操作,数据定义语言
- 严格的一致性
- 基础的事务
- ...
Nosql
- 不仅仅是数据
- 没有固定的查询语言
- 键值对存储,列存储,文档存储,图形数据库(社交关系)
- 最终一致性
- CAP定理和BASE
- 高性能,高可用,高扩展
- ...
真正在公司中:用 NoSQL + RDBMS(即关系型数据库)
大数据的 3V + 3 高
大数据时代的 3V :主要是描述问题的
- 海量 Velume
- 多样 Variety
- 实时 Velocity
大数据时代的 3 高 : 主要是对程序的要求
- 高并发
- 高可扩
- 高性能
2.2 阿里巴巴网站架构演进分析
阿里巴巴批发网:https://www.1688.com/
推荐阅读:阿里云的这群疯子https://yq.aliyun.com/articles/653511(王坚)
发展历程:
数据架构:
各种数据的存储策略
# 1.商品信息:名称、价格、商家信息
- 一般存放在关系型数据库就可以了:Mysql,但是阿里巴巴使用的Mysql都是经过内部改动的。
# 2.商品描述、评论(文字偏多)
- 放在文档型数据库:MongoDB
# 3.图片
- 放在分布式文件系统
- 比如:
- FastDFS
- 淘宝:TFS
- Google: GFS
- Hadoop: HDFS
- 阿里云: OSS
# 4.商品关键字:用于搜索的
- 放在搜索引擎:比如常见的有:solr,elasticsearch
- 阿里:Isearch(伟大的一个人:多隆,要多去了解一下这些技术大佬!)
所有牛逼的人都有一段苦逼的岁月!但是你要像傻逼一样的坚持,终将牛逼!
# 5.商品热门的波段信息
- 内存数据库:Redis,Tair,Memcache
# 6.商品交易的实现,外部支付接口
- 第三方应用:支付宝,银行,微信
大型互联网应用的问题:
- 数据类型太多了
- 数据源繁多,经常重构
- 数据要改动时,大面积改造
阿里提供的解决方案:UDSL(统一数据服务平台)
2.3 Nosql 的四大分类
2.3.1 键值数据库
K-V 键值对
- 新浪:Redis
- 美团:Redis + Tair
- 阿里、百度:Redis + Memcache
2.3.2 文档数据库
**使用 bson 数据格式!(JSON 的演化版)
文档型数据库也是键值数据库!
- MongoDB(掌握)
- 基于分布式文件存储的数据库。C++编写,用于处理大量文档。
- MongoDB 是 RDBMS 和 NoSQL 的中间产品。MongoDB 是非关系型数据库中功能最丰富的,NoSQL 中最像关系型数据库的数据库。
- ConthDB
2.3.3 列族数据库
- HBase(大数据必学)
- 分布式文件系统
2.3.4 图数据库
用于广告推荐,社交网络
- Neo4j、InfoGrid
表格总结
敬畏之心可以使人进步!(觉得自己会的还太少)看宇宙,科幻,敬畏生命(刘慈欣,埃斯科拉克)
活着的意义?追求幸福(让家人幸福,让自己开心),探索未知(不断地学习)!
3.Redis 入门概述
3.1 介绍 Redis
Redis 是什么?
百度百科:Redis(Remote Dictionary Server ),中文翻译即远程字典服务,是一个开源的使用 ANSI,使用C 语言编写、支持网
络、可基于内存亦可持久化的日志型、Key-Value 数据库,并提供多种语言的 API(JAVA,pythpon,php)
与 memcached 一样,为了保证效率,数据都是缓存在内存中,redis 会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记
录文件,并且在此基础上实现了 master-slave(主从)同步。
官网介绍:Redis 是一个开源的(BSD 许可的),内存存储的数据结构服务器。
可用作数据库,高速缓存和消息队列代理。
它支持字符串、哈希表、列表、集合、有序集合,位图,hyperloglogs 等数据类型。
内置复制、Lua 脚本、LRU 收回、事务以及不同级别磁盘持久化功能。
同时通过 Redis Sentinel 提供高可用,通过 Redis Cluster 提供自动分区。
Redis 能该干什么?
- 内存存储、持久化,内存是断电即失的,所以需要持久化(RDB、AOF)
- 高效率、用于高速缓冲
- 发布订阅系统
- 地图信息分析
- 计时器、计数器(eg:浏览量)
- 。。。
Redis 特性
- 多样的数据类型
- 持久化
- 支持集群
- 支持事务
- 开源
- 单线程
Redis 的单线程特性
Redis 是单线程的!,Redis 是将所有的数据放在内存中的,所以说使用单线程去操作效率就是最高的(如果是多线程,CPU 上下文会
切换:耗时的操作!),对于内存系统来说,如果没有上下文切换效率就是最高的,多次读写都是在一个 CPU 上的,在内存存储数据情
况下,单线程就是最佳的方案。
3.2 下载 Redis
3.2.1 环境搭建
推荐使用 Linux 服务器学习。
windows 版本的 Redis 已经停更很久了
下载:通过官网下载即可,win 版本需要在 github 上面下载
3.2.2 Windows 下安装
地址:https://github.com/MicrosoftArchive/redis/tags
1.解压安装包
2.开启 redis-server.exe,双击即可
默认端口:6379
3.启动 redis-cli.exe 客户端测试
使用 redis-cli 连接 redis 服务!
3.2.3 Linux 下安装并启动服务
下载安装包 redis-6.0.6.tar.gz
上传到 Linux,/opt/module/redis 目录
进入/opt/module/redis 目录,解压 tar -zxvf redis-6.0.6.tar.gz
进入解压文件 cd redis-6.2.6
安装 gcc 环境:yum install gcc-c++
先执行 make 命令:会配置所有的内容
发现有错误,因为我们的 gcc 版本太低了,因为 Redis6.0 版本及以上的需要 gcc 升级到 5.3 及以上!
我们先查看 gcc 版本:gcc -v
发现 gcc 是 4.8.5 版本的,不够
我们来升级 gcc:
升级到 gcc 7.3.1:一共需要四句话! sudo yum install centos-release-scl sudo yum install devtoolset-7-gcc* scl enable devtoolset-7 bash 需要注意的是 scl 命令启用只是临时的,退出 shell 或重启就会恢复原系统 gcc 版本。 如果要长期使用 gcc 7.3.1 的话: echo "source /opt/rh/devtoolset-7/enable" >>/etc/profile 这样退出 shell 重新打开就是新版的 gcc 了:
再次使用 make 命令
发现成功,没有报错!!!
然后执行 make install 命令:确认是否所有都安装了!
redis 的默认安装路径: /usr/local/bin
将我们刚刚安装 redis 的位置/opt/module/redis/redis-6.0.6 的配置文件 redis.conf 复制到/usr/local/bin 目录下的 haoconfig 文件夹内!
bashmkdir haoconfig cp /opt/module/redis/redis-6.0.6/redis.conf hao_config
Redis 默认不是后台启动的,我们要修改配置文件
修改 haoconfig 下的配置文件 redis.conf,修改 daemonize 为 yes
用修改过的配置文件启动 redis 服务
在/usr/local/bin 目录下:
bashredis-server haoconfig/redis.conf
使用客户端连接 server
用 redis-cli 连接 redis 服务
bashredis-cli -p 6379
测试
查看 Redis 的进程是否开启
bashps -ef|grep redis
关闭 Redis 服务
关闭成功
15.后面我们会使用单机多 Redis 启动集群测试
3.3 redis-benchmark 性能测试
redis-benchmark:Redis 官方提供的性能测试工具
参数选项如下:
简单测试:
# 测试:100个并发连接 100000请求
redis-benchmark -h localhost -p 6379 -c 100 -n 100000 #本机100个并发连接,每个10000请求,一共1000000个请求
18ms 处理完成:每秒处理 53050 个请求
3.4 Redis 单线程
Redis 是单线程的,Redis 是基于内存操作的!
所以Redis 的性能瓶颈不是 CPU,而是机器内存和网络带宽,既然可以使用单线程,并且单线程会更快,那么就使用单线程了!
那么为什么 Redis 的速度如此快呢,QPS 可以达到 10W+,为什么性能这么高呢?
Redis 为什么单线程还这么快?
误区 1:高性能的服务器一定是多线程的!
误区 2:多线程(CPU 上下文会切换!)一定比单线程效率高!
速度的对比:CPU>内存>硬盘
核心:Redis 是将所有的数据放在内存中的,所以说使用单线程去操作效率就是最高的,多线程(CPU 上下文会切换:耗时的操作!),对于内存系统来说,如果没有上下文切换效率就是最高的,多次读写都是在一个 CPU 上的,在内存存储数据情况下,单线程就是最佳的方案。
4.Redis 基础命令
4.1 redis 默认有 16 个数据库
默认使用的第 0 个;
16 个数据库为:DB0 ~ DB15 默认使用 DB 0
在 cli 命令行中:可以使用select n
切换到 DB n,dbsize
可以查看当前数据库的大小,大小与 key 的数量相关。
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的个数。
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
4.2 常见基础命令
Redis 是一个开源(BSD 许可),内存存储的数据结构服务器,可用作数据库,高速缓存和消息队列代理。它支持字符串、哈希表、列表、集合、有序集合,位图,hyperloglogs 等数据类型。内置复制、Lua 脚本、LRU 收回、事务以及不同级别磁盘持久化功能,同时通过 Redis Sentinel 提供高可用,通过 Redis Cluster 提供自动分区。
Redis-key
在 redis 中无论什么数据类型,在数据库中都是以 key-value 形式保存,通过进行对 Redis-key 的操作,来完成对数据库中数据的操作。
==注:Redis 的 key 一定是 String 类型的!==
注:命令语句不区分大小写!
切换数据库和查看数据库大小
select 3 //select 切换数据库,Redis默认有16个数据库(编号0-15),默认使用第0个数据库
dbsize //查看当前数据库大小
设置 k-v 和查看 v
set name rainhey //存值,键值对
get name //取值
keys * //查看所有的key值
清空数据库
flushdb //清空当前数据库
flushall //清空全部数据库
判断 k-v 是否存在
exists name //判断键值是否存在
//存在返回1,不存在返回0
移动 k-v
move name 1 //从当前数据库移动key到1号数据库中
k-v 过期时间
expire age 20 //设置key的过期时间(秒),过期后自动移除
ttl age //查看key的剩余时间
查看 v 类型
type name //查看value的类型
删除 k-v
del name #删除键值对
修改 key 名
rename name names #修改key的名称为newkey
renamex name names #仅当newkey不存在时,将key改名为newkey
关于 TTL 命令
Redis 的 key,通过 TTL 命令返回 key 的过期时间,一般来说有 3 种:
当前 key没有设置过期时间,所以会返回-1
当前 key 有设置过期时间,而且key 已经过期,所以会返回-2
当前 key 有设置过期时间,且 key 还没有过期,故会返回 key 的正常剩余时间
关于重命名 rename 和 renamex
rename key newkey 修改 key 的名称为 newkey
renamex key newkey 仅当 newkey 不存在时,将 key 改名为 newkey
更多命令学习:https://www.redis.net.cn/order/
5.Redis 数据类型及高级命令
4.1 String 字符串
相关命令
1.设置字符串 key-value
set test "hello,world"
==注:在设置值的时候,加引号和不加引号是等价的,都会默认被解析为 String 类型!==
==注:即使是数字也是一样的==
2.向指定的 key 的 value 后追加字符串
如果不存在这个 key,就相当于是 set key
append name test //在key name的值后追加字符串“test”,若key不存在相当于添加
3.获取 key 的 value 值的字符串长度
strlen name //获取key对应的值的字符串长度
4.将指定 key 的 value 数值进行+1/-1(仅对于数字值的 value,但是本质也是 String)
注:可以应用于浏览量
set view 0 //设置浏览量为0
incr view //view数值加1
decr view //view数值减1
5.按指定的步长对数值进行加减
incrby view 10 //view数值加10
decrby view 11 //view数值减11
6.按起止位置获取字符串(是一个闭区间,起止位置都取)
注:redis 中的索引也是从 0 开始的
set test "hello,world"
getrange test 0 4 //截取字符串下标[0,4]的字符串,注意两边是闭区间;[0,-1]代表整个字符串
7.从某一个位置开始替换为某个值
从下标 1 开始替换为***
setrange test 1 *** //替换字符串,1代表开始替换的下标,***代表替换为的值
8.设置键值对并设置过期时间(以 s 为单位)
setex key1 20 "hello" // setex( set with expire ) 设置值并设置过期时间
9.设置键值对并设置过期时间(以 ms 为单位)
和 setex 命令相似,但它以毫秒为单位设置 key 的生存时间
psetex key1 2000 "hello"
10.仅当 key 不存在时进行设置(如果存在了就创建失败,不会覆盖!)
setnx key2 "hello" // setnx (set if not exist)如果不存在就设置 ——>在分布式锁中会常常使用
11.批量设置键值对
mset k1 v1 k2 v2 k3 v3 // 批量设置值(key value交替的)
12.批量获取多个 key 保存的值
mget k1 k2 k3 // 批量获取值
13.批量设置键值对,仅当参数中所有的 key 都不存在时执行(如果存在了就创建失败,不会覆盖!原子性的操作,要么一起成功,要么一起失败!)
msetnx k1 v1 k4 v4 // key都不存在时才设置,否则都不设置
14.设置对象的两种方式(本质上也是设置 String)
都是设置一个对象:
==(1)用 user:{id} json + set==
key 为 user:1,value 为{name:zhangsan,age:3} ——> 这是 Json 类型的 String
set user:1 {name:zhangsan,age:3} //设置user:1对象,用json字符串保存其值{name:zhangsan,age:3}
==(2)用 key:{id}:{field}*n + mset== ——>更好一点,可以实现 key 的复用!!!,==key:{id}:{field}整体作为一个大 key==
key 为 user:1:name 和 user:1:age,value 为 zhangsan 和 55
//设置user:1对象的name为zhangsan,age为55
mset user:1:name zhangsan user:1:age 55 //这里的key是个巧妙设计 user:{id}:{field}
注:比如我们设置公众号文章的浏览量,那么可以设置为: 可以修改{id}或者
set article:10000:views 0 ——> 还可以设置其他字段:set article:10000:date 2022-7-16
set article:10001:views 0
set article:10002:views 0
15.如果不存在值,则返回 null,并设置值;如果存在值,获取原来的值,并设置新的值(覆盖)——>组合命令
getset key2 v2 //先获取值再设置值
String 的使用场景
value 除了是普通的 String 字符串还可以是数字的 String 字符串,用途举例:
计数器(incrby):阅读量等
统计多单位的数量(区分 id 区分字段的那种)设计 key,同时结合计数器的实现
比如关注量:uid:123666:follow 0
粉丝数
获赞数
播放数
阅读数
进行对象的存储缓存
4.2 List 列表
简介
Redis 列表是简单的字符串列表,是一个双链结构(类似 Java 的 LinkedList,双向的栈),按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)。
一个列表最多可以包含 2^32^-1 个元素 (4294967295, 即每个列表超过 40 亿个元素)。
列表可以经过规则定义(不同的命令组合)将其变为队列、栈、阻塞队列等
正如图 Redis 中 List 是可以进行双端操作的,所以命令也就分为了 LXXX 和 RLLL 两类,当然有时候 L 也表示 List 例如 LLEN 命令(获取长度)
所有的 List 命令都是以 L 或者 R 开头的 添加:lpush 相当于入栈、rpush 相当于入队列,读的时候先读栈再读队列
数据结构
相当于是两个栈合在一起,一个口在左边,一个口在右边,中间相当于有一条无形的线将两个栈分开!!!
而这个双栈的后边就是指的右边,前边就是指的左边
相关命令
1.添加元素
不存在就创建 list:
lpush list one //往list里面添加元素one,从左边添加
lpush list one two three //添加多个也是没问题的
rpush list four //往list添加元素four,从右边添加
不存在就失败:
lpushx list v1 v2 //往list里面添加元素v1 v2,从左边添加
rpushx list v1 v2 //往list添加元素v1 v2,从右边添加
2.范围取值
lrange list 0 -1 //从list里面取值,注:下标可以超过范围,不会报错
3.左右弹出
lpop list //移除list里的第一个元素
rpop list //移除list里的最后一个元素
双命令:相当于是移动,从一个栈移动到了另一个栈中
rpoplpush list list1 //移除list最后一个元素lpush到list1中
4.下标取值
lindex list 1 //获取list下标1的值
5.获取 List 长度
llen list // 获取list长度
6.移除元素
lrem list 2 four //从list中移除两个为four的值
7.修剪
ltrim list 0 3 //只保留list[0,3]的值
8.替换
lset list 0 one //将指定下标的值替换
9.插值
linsert list before world other // 在world前添加other
10.限时按顺序在一个或者多个列表中弹出第一个元素
blpop newlist mylist 30 # 从newlist中弹出左边第一个值,mylist作为候选(如果newlist里面有,就不从mylist里面弹出了,并且限时为30s,30s如果没有弹出则暂停该方法)
brpop newlist 30 #从newlist中弹出右边第一个值,限时为30s
实战应用
---------------------------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"
---------------------------LREM--------------------------
# 初始 mylist: k2,k2,k2,k2,k2,k2,k4,k2,k2,k2,k2
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作为候选(如果newlist里面有,就不从mylist里面弹出了,并且限时为30s,30s如果没有弹出则暂停该方法)
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 #此时newlist空了,肯定弹不出来
(30.10s) # 超时了
127.0.0.1:6379> BLPOP newlist 30 # 我们连接另一个客户端向newlist中push了test, 阻塞被解决
1) "newlist"
2) "test"
(12.54s)
List 小结
- list 实际上是一个链表,before Node after , left 和 right 都可以插入值
- 如果 key 不存在,则创建新的链表(List 也是由 key-value 组成的,value 是一个链表!)
- 如果 key 存在,新增内容
- 如果移除了所有值,就是空链表,也代表不存在
- 在两边插入或者改动值,效率最高!修改中间元素,效率相对较低
应用场景
消息队列!队列(Lpush Rpop):相当于是改造成了一个队列(一边进另一边出)
消息排队!栈(Lpush Lpop):只使用一边,相当于是一个栈(一边进同一边出)
如果不需要这个消息了直接移除即可!如果需要这个消息的一部分可以截断!
用 MQ 做的东西用 Redis 依旧可以做到!
4.3 Set 无序集合
简介
Redis 的 Set 是 string 类型的无序集合。
集合成员是唯一的,这就意味着集合中不能出现重复的数据。
Redis 中 集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。
集合中最大的成员数为 2^32^-1 (即 4294967295, 每个集合可存储 40 多亿个成员)。
相关命令
sadd myset hello1 hello2 hello3 //添加值
smembers myset //查看成员
sismember myset hello1 //判断hello1是否是myset的成员
scard myset //查看myset元素个数
srem myset hello1 //移除hello1
srandmember myset 2 //随机取出两个
spop myset 1 //随机移除指定数目的元素
smove myset1 myset2 one //将one这个元素从myset1移动到myset2
sdiff set1 set2 //set2在set1中的差集
sinter set1 set2 //set2在set1中的交集
sunion set1 set2 //set2在set1中的并集
实战应用
---------------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"
应用场景
- 可以把一个人关注的所有人放在一个 set 集合里面,因为这里面是不可重复的!还可以把一个人的粉丝也放在一个 set 里面
- A 用户和 B 用户的共同关注,共同爱好
- 二度好友,推荐好友!
4.4 Hash 键值对集合
简介
Redis hash 是一个string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象。
Set 就是一种简化的 Hash,只变动 field,而 value 使用默认值填充。
hash 的 value 值实际上就是 String 类型的 Map 集合,所以由 key-value 变为了 key-map(或者说 key-(key-value)*n),这时候值是 map 的
集合,map 集合又有自己的 key-value,但这个 map 本质和 string 没多大区别(类似 json)。
相关命令
hset myhash field1 rainhey //存键值对
hget myhash field1 //取值
hmset myhash field1 value1 field2 value2 //批量存键值对
hmget myhash field1 field2 //批量取值
hgetall myhash //获取所有的键值对
hdel myhash field1 //删除key,对应的value也没有了
hlen myhash // 查看键值对的对数
hkeys myhash //获取所有键值对的key
hvals myhash //获取所有键值对的value
hincrby myhash field3 4 //field3对应的值加4(仅限数字类的value)
hsetnx myhash field4 hello //key不存在就设置,存在就不设置
实战应用
------------------------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"
应用场景
可以经常变动的数据,尤其是用户信息之类的 user:name age
Hash 更适合于对象的存储,而 String 虽然可以存对象,但是 Sring 更加适合字符串存储!
4.5 Zset 有序集合
简介
Zset 与 Set 不同的是,Zset 每个元素都会关联一个 double 类型的分数(score)
在 set 的基础之上增加了一个值,在 key 和 value 的中间加一个 score(数字):set k v ; zset k score v
redis 正是通过分数来为集合中的成员进行从小到大的排序
如果 score 相同:则按字典(就是英文字典)中的顺序排序
有序集合 Zset 的成员是唯一的,但分数(score)可以重复
注意点:这里面有索引和 score 两个可以进行识别的值,一般都是按照 score 排序,按照索引取值!
相关命令
zadd myzset 1 one // 设置一个值
zadd myzset 3 three 4 four // 设置多个值
zrange myzset 0 -1 // 取值,score从小到大排序(score的排序就对应的是索引排序)
zrange myzset 0 1 //获取索引在 0~1的成员
zincrby myzset 5 m2 //将成员m2的score +5
zscore myzset m1 //获取成员m1的score
zrank myzset m1 # 获取成员m1的索引,score的排序就是索引的排序,score相同则索引值按字典(就是英文字典)顺序增加
zrangebyscore salary -inf +inf // 按照score排序,并返回值(-inf负无穷,+inf正无穷)
zrangebyscore salary 10 20 // score在指定范围内的值,按照score排序,并返回值
zrangebyscore salary -inf +inf withscores //排序,返回值带上score
zrem salary lisi // 移除指定元素
zcard salary // 查看元素个数
zrevrange salary 0 -1 // score从大到小排序,并按索引返回全部数据
zcount myset 1 3 // 获取score在1到3的元素的个数
实战应用(还有更多命令)
-------------------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"
------------------ZRANGEBYLEX--------------------------------- #通过给定的字符串(成员名)区间排序!
#testset=>{abc,add,amaze,apple,back,java,redis} score均为0
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 多了排序:存储班级成绩表(id 不重复,score 作为分数)、工资表等!(需要排序的)
- 排重要性:普通消息设置为 1,重要消息设置为 2,可以带权重
- 排行榜实现:播放量、评分等(定时更新的),可以取 Top N
4.6 Geospatial 地理位置集合
简介
Geospatial 是使用经纬度定位地理坐标并用一个有序集合 zset 保存,所以 zset 的命令也可以使用
Redis 的 Geo 在 Redis3.2 版本就推出了!这个功能可以推算地理位置的信息,两地之间的距离,方圆几里的人!
只有六个命令
经纬度:
- 有效的经度从-180 度到 180 度。
- 有效的纬度从-85.05112878 度到 85.05112878 度
距离:指定单位的参数 unit 必须是以下单位的其中一个
- m 表示单位为米
- km 表示单位为千米
- mi 表示单位为英里。
- ft 表示单位为英尺
相关命令
==注:Geo 的本质就是具有两个 score 的 Zset,这里的两个 score 分别为经度和纬度!==
geoadd china:city 114.878872 30.459422 beijing // 添加地理位置到geo集合中,参数 key 经度 维度 位置名称
geoadd china:city 116.413384 39.910925 guangzhou 79.920212 37.118336 hetian //多地
geopos china:city beijing // 获取某地的经纬度
geopos china:city shanghai beijing // 多地
geodist china:city shanghai beijing //计算距离
geodist china:city shanghai beijing km // km为单位
关于 GEORADIUS 方法的参数
通过georadius
就可以完成 附近的人 功能!
withcoord: 带上坐标
withdist: 带上距离,单位与半径单位相同
COUNT n : 只显示前 n 个(按距离递增排序)
georadius china:city 118 30 500 km // 查找以经纬度118 30 为圆心,500km为半径的城市
georadius china:city 119 30 200 km withdist // 查找并带上距离
georadius china:city 119 30 200 km withcoord //查找并带上经纬度坐标
georadius china:city 119 30 200 km withcoord withdist count 2 //查找并带上坐标距离、限定查找个数
eoradiusbymember china:city city3 100 km // 查找以某以成员为圆心的距离内的其他成员
geohash china:city city1 city2 city3 //该命令返回每个城市的11个字符的geohash字符串,通过算法将二维的经纬度转化为一维的字符串(字符串越接近则距离越接近)
实战应用
----------------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"
注:Geo 的底层实现原理是 Zset,所以也可以 Zset 命令来操作 Geo
查看地图中全部元素:
移除地图中某个元素:
应用场景
- 朋友的定位
- 附近的人
- 打车距离计算
4.7 Hyperloglog 基数统计
简介
Redis HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。
花费 12 KB 内存,就可以计算接近 2^64^个不同元素的基数。
因为 HyperLogLog 只会根据输入元素的个数来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输
入的各个元素。
其底层使用 string 数据类型!
什么是基数?
数据集中不重复的元素的个数
应用场景
应用场景:
网页的访问量(UV):一般要求一个用户多次访问,也只能算作一个人!——>我们的 Hyperloglog 恰好可以解决这个需求!
传统实现:存储用户的 id,然后每次进行比较是否重复,不重复加 1。当用户变多之后这种方式及其浪费空间,而我们的目的只是计数,Hyperloglog 就能帮助我们利用最小的空间完成。
注:
如果允许容错,那么一定可以使用 Hyperloglog !
如果不允许容错,就使用 set 或者自己的数据类型即可 !
相关命令
pfadd myset a b c d e f g h i j //添加
pfadd myset2 i j k c v z h
pfcount myset //估算计数,不重复
pfmerge myset3 myset myset2 // 合并集合 myset和myset2 成为 myset3
实战应用
----------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
4.8 Bitmap 位图
简介
Bitmap 是一串连续的 2 进制数字(0 或 1),每一位所在的位置为偏移(offset),在 bitmap 上可执行 AND,OR,XOR,NOT 以及其它位操作!
Bitmap 使用位存储,信息状态只有 0 和 1!
底层是很多很多个二进制位放在一起,是一串从左到右的二进制串
应用场景
只要某个事务是两个状态的,就可以用位图操作二进制位来记录
签到统计、状态统计
登陆未登录,活跃不活跃!
365 天打卡!(在 mysql 中需要建 user 表:id,status,day ——> 非常麻烦!)
365 天 = 365bit 1 字节=8bit,所以为 46 个字节左右!
统计疫情感染人数,只要把所有的数加起来就可以了
相关命令
setbit 位置号 状态号
setbit sign 2 0 //2代表第二个位置,0代表该位置状态
getbit sign 6 //获取第六个位置的状态
bitcount sign //统计sign中状态为1的个数
实战应用
------------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
6.Redis 事务操作
6.1 概述
Redis 事务本质:一组命令的集合,一个事务的所有命令都会被序列化,在事务执行过程中按照顺序执行
Redis 的单条命令是保证原子性的,但是 redis 事务不能保证原子性
Redis 事务没有隔离级别的概念
所有的命令在事务中都没有被直接执行,只有发起执行命令后才会执行
6.2 Redis 事务操作过程
- 开启事务 multi
- 命令入队 (写入一系列命令,如 get,set 等)
- 执行事务 exec
所以事务中的命令在加入时都没有被执行,直到提交时才会开始执行(Exec)一次性完成。
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"
- discard 放弃事务,队列中的命令都不会被执行
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)
6.3 事务错误
6.3.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> 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) # 其他命令并没有被执行
6.3.2 代码逻辑错误:运行时异常
若事务中某条命令存在运行时异常(如 1/0,报错),则其他命令可以正常执行 ——> 所以 Redis 不保证事务原子性
因为代码是放在一起编译和运行的,如果编译通过了,你并不知道这个代码实际有没有错,只有在运行时才知道,但是一旦运行了,
那么有些命令就已经被执行了,就无法挽回了,后面的命令还会照常执行!
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> 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事务不能保证原子性。
6.4 监控(乐观锁)
悲观锁:
认为什么时候都会出问题,无论做什么都会加锁
乐观锁:
认为什么时候都不会出问题,不会加锁,更新数据的时候去判断一下此期间是否有人修改过数据
- 获取 version
- 更新的时候比较 version
使用 watch key 监控指定数据,就相当于乐观锁加锁
watch key
进行加锁,unwatch
进行解锁。- 监控可以监控 key,也可以监控 value(本质都是监控的 value)!
正常执行
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 的乐观锁操作(相当于 get version,Redis 是获取了原本的值作为 version)
线程 1:
127.0.0.1:6379> watch money # money上乐观锁,相当于get version
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:新开启一个命令窗口执行该命令
127.0.0.1:6379> INCRBY money 500 # 修改了线程一中监视的money
(integer) 600
回到线程 1,执行事务:
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"
如果发现事务执行失败,可以再加锁再进行事务
接着上面失败后的事务进行操作:
127.0.0.1:6379> unwatch
OK
127.0.0.1:6379> watch 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
注意:每次提交执行 exec 后(不管事务是否成功) 或者 放弃事务了 discard 都会自动解锁,所以 unwatch 基本用不到!
但是为了以防万一,可以在再次执行任务之前用 unwatch 解锁一下
应用场景
- 抢票
- 秒杀
7.Jedis
Jedis 是使用 Java 来操作 Redis 的中间件,Jedis 是 Redis 官方推荐使用的 Java 工具来连接 redis 的客户端,是一个 jar 包。
7.1 win 上面的 Redis 用 Jedis 测试
1.新建一个 maven 项目,导入依赖
<dependencies>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.3.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.72</version>
</dependency>
</dependencies>
2.测试本地连接是否成功:
- 打开本地 server:
- 编写 java 代码:TestPing.java
public class TestPing {
public static void main(String[] args) {
Jedis jedis = new Jedis("127.0.0.1", 6379); //先连接一下本地的
//命令就是jedis.我们之前学过的那些命令即可!之前的命令在这边就是java的方法
String response = jedis.ping();
System.out.println(response); // PONG
}
}
- 关闭 server
7.2 Jedis 常用的 API
String
List
Set
Hash
Zset
所有的 api 命令,就是我们对应上面学习的指令,基本都没有变化
7.3 Linux 上面的 Redis 用 Jedis 测试
1.修改配置文件
修改 redis 的配置文件
vim /usr/local/bin/haoconfig/redis.conf
(1)将只绑定本地 bing 127.0.0.1 改为 bind 0.0.0.0
(2)保护模式改为 no
(3)允许后台运行
之前已经设置过了
(4)设置连接密码
2.配置防火墙
- 检查 6379 是否开启
firewall-cmd --query-port=6379/tcp
- 开放端口 6379
firewall-cmd --zone=public --add-port=6379/tcp --permanet
注:这时我们虽然配置了开启,但是依然会显示没有开启,我们需要重启防火墙才可以真正开启!!!
- 重启防火墙服务
systemctl restart firewalld.service
- 再次检查是否开启
firewall-cmd --query-port=6379/tcp
阿里云服务器控制台配置安全组
3.重启 redis-server(非常重要!!!)
我们必须先关闭 server 服务!!!
redis-cli shutdown
- 然后我们再开启服务!
redis-server usr/local/bin/haoconfig/redis.conf
3.测试连接
public class TestPing {
public static void main(String[] args) {
Jedis jedis = new Jedis("124.220.15.95", 6379); //先连接一下本地的
//命令就是jedis.我们之前学过的那些命令即可!
jedis.auth("123456");
String response = jedis.ping();
System.out.println(response); // PONG
}
}
成功!!!
7.4 Jedis 操作事务
public class TestTX {
public static void main(String[] args) {
// 1.new jedis 对象
Jedis jedis = new Jedis("124.220.15.95", 6379);
jedis.auth("123456");
jedis.flushDB(); //清空数据库
JSONObject jsonObject = new JSONObject(); //fastjson中的方法
jsonObject.put("hello", "world");
jsonObject.put("name", "kuangshen");
// 2.开启事务,jedis的所有命令就是之前学的所有命令
Transaction multi = jedis.multi();
String result = jsonObject.toJSONString(); //值就是json字符串!
// jedis.watch(result); //监控result字符串,如果发生了变化那么就会失败
try {
multi.set("user1", result); //配置键值对
multi.set("user2", result);
// int i = 1/0; //代码会抛出异常,任务执行失败!
// 执行事务
multi.exec();
}catch (Exception e){
// 放弃事务
multi.discard();
} finally {
// 得到值
System.out.println(jedis.get("user1"));
System.out.println(jedis.get("user2"));
jedis.close(); // 关闭连接
}
}
}
执行结果:
8.SpringBoot 整合 Redis
8.1 介绍
SpringBoot 操作数据用 SpringData 可以连接 jpa,jdbc,mongondb,redis 等!
Springboot 2.x 后 ,原来使用的 Jedis 被 Lettuce 替换!
Jedis:采用的是直连,多个线程操作的话,是不安全的。如果要避免不安全,要使用 jedis pool 连接池,但是麻烦!更像 BIO 模式(阻塞模式)
Lettuce:采用 netty(高性能网络框架,异步请求),**实例可以在多个线程中共享,不存在线程不安全的情况!**可以减少线程数据了,更像 NIO 模式
8.2 原理解析
我们在学习 SpringBoot 自动配置的原理时,整合一个组件并进行配置一定会有一个自动配置类 xxxAutoConfiguration,并且在
spring.factories 中也一定能找到这个类的完全限定名。Redis 也不例外。
我们在 spring.factories 文件中我们看到了 RedisAutoConfiguration 类
所以还存在一个 RedisProperties 类
之前我们说 SpringBoot2.x 后默认使用 Lettuce 来替换 Jedis,现在我们就能来验证了!
先看 JedisConnectionConfiguration 类:
@ConditionalOnClass 注解中有两个类是默认不存在的,所以 Jedis 是无法生效的!
然后再看 LettuceConnectionConfiguration 类:
完美生效!
现在我们回到 RedisAutoConfiguratio 类:
注:
只有两个简单的 Bean:
- RedisTemplate
默认的 RedisTemplate 没有过多的设置,在 redis 中对象的保存都是需要序列化的!
两个泛型都是 Object 类型,后面我们使用的时候需要强制转换!(我们希望的是<String,Object>)
- StringRedisTemplate
由于 String 类型是 Redis 中最常使用的数据类型,所以单独提出来了一个 bean
当看到 xxTemplate 时可以对比 RestTemplat、SqlSessionTemplate,通过使用这些 Template 来间接操作组件。
那么这俩 bean 也不会例外。分别用于操作 Redis 和 Redis 中的 String 数据类型。
在 RedisTemplate 上也有一个条件注解,不存在 RedisTemplate 这个默认的才生效,说明我们是可以对其进行定制化的!——>我们自
己定义 RedisTemplate 即可!
说完这些,我们需要知道如何编写配置文件然后连接 Redis,就需要阅读 RedisProperties.java 类
我们发现前缀为 spring.redis,所以我们可以在 application.yml 中进行配置了!
这是一些基本的配置属性:
还有一些连接池相关的配置。注意使用时一定要使用 Lettuce 的连接池。
8.3 实战操作
1.导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
或者在创建项目的时候直接勾选即可:
2.配置连接,编写配置文件
spring.redis.host=124.220.15.95
spring.redis.port=6379
spring.redis.password=123456
3.测试及 redisTemplate 方法解释:
一些常用的数据库相关的操作等可以通过 redisTemplate 直接点出来,比如事务 如 exec()等
注:但是某些命令要通过 Redis 的连接对象操作 RedisConnection connection = redisTemplate.getConnectionFactory().getConnection(); connection.flushAll(); //清空数据库 connection.flushDb();
connection.ping();
除了基本的操作,常用的数据类型的操作方法可以通过 redisTemplate.opsForxxx 获取数据类型的方法,再.方法 redisTemplate 的方法: opsForValue(): 操作字符串,其后面的方法和 String 一样 opsForList(): 操作 list opsForSet() opsForZSet() opsForHash() opsForGeo() opsForHyperLogLog()
注意一些连接池相关的配置,使用时一定要使用 Lettuce 的连接池
@SpringBootTest
class RedisSpringApplicationTests {
@Autowired
private RedisTemplate redisTemplate;
@Test
void contextLoads() {
//链式编程!
//设置值,获取值:
redisTemplate.opsForValue().set("mykey", "hao");
System.out.println(redisTemplate.opsForValue().get("mykey"));
}
}
4.测试结果:
8.4 序列化
8.4.1 产生的问题
注:我们在给 Redis 配置文件设置了密码之后,要在命令行输入auth 密码
,才可以接着进行接下来的操作!
此时我们回到 Redis 查看数据时候,惊奇发现全是乱码,可是程序中可以正常输出:
这时候就关系到存储对象的序列化问题,在网络中传输的对象也是一样需要序列化,否者就全是乱码。
8.4.2 原理分析
我们转到看那个默认的 RedisTemplate 内部什么样子:
在最开始我们就能看到几个关于序列化的属性
我们查看 defaultSerializer 属性,发现默认的序列化器是采用 JDK 序列化器:
而默认的 RedisTemplate 中的所有 key-value 序列化器都是使用这个序列化器:也就是 JDK 的——>但是这个序列化器会造成乱码,我们需要修改他!
RedisTemplate 是可以进行定制的,后续我们定制 RedisTemplate 就可以对其进行修改!
8.4.3 存对象时的序列化操作
java 的对象序列化:implements Serializable
Java 提供了一种对象序列化的机制,该机制中,一个对象可以被表示为一个字节序列,该字节序列包括该对象的数据、有关对
象的类型的信息和存储在对象中数据的类型;将序列化对象写入文件之后,可以从文件中读取出来,并且对它进行反序列化,也
就是说,对象的类型信息、对象的数据、还有对象中的数据类型可以用来在内存中新建对象!(把对象转为序列,再根据序列新建对象)
存对象时的序列化方案(存对象时是必须序列化的!)
注:但是不管是存对象还是存 String,我们在 Redis 中都不能得到正常的 key!——> 原因为我们没有设置序列化器,Redis 无法解析我们的序列化内容!
这里只是展示在 java 中传对象到 Redis 中序列化的方案,以及不序列化会产生什么结果!
- 方案一:jackson
创建一个 User 实体类:实体类不 implements Serializable,使用 SpringBoot 自带的 jackson 序列化为 json,然后再存到 redis 中
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private String name;
private int age;
}
@SpringBootTest
class RedisSpringApplicationTests {
@Autowired
private RedisTemplate redisTemplate;
@Test
void test() throws JsonProcessingException {
User user = new User("hao", 3);
//真实的开发一般都使用json字符串来传递对象!
//序列化,转换成json字符串传递
String jsonUser = new ObjectMapper().writeValueAsString(user); //使用SpringBoot自带的jackson进行序列化
redisTemplate.opsForValue().set("user", jsonUser);
System.out.println(redisTemplate.opsForValue().get("user"));
}
}
输出结果:
- 方案二:implements Serializable
直接在实体类上 implements Serializable 序列化,然后直接存到 redis 中
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {
private String name;
private int age;
}
@SpringBootTest
class RedisSpringApplicationTests {
@Autowired
private RedisTemplate redisTemplate;
@Test
void test() throws JsonProcessingException {
User user = new User("hao", 3);
//String s = new ObjectMapper().writeValueAsString(user); //之前的这个就不需要了!
redisTemplate.opsForValue().set("user", user);
System.out.println(redisTemplate.opsForValue().get("user"));
}
}
注:如果我们存对象时没有序列化:也就是说没有 implements Serializable 或者没有序列化为 json 的话就会报错!
- 方案三:就是设置序列化器,并且可以实现 key 的正常显示!
8.4.4 自定义 redisTemplate 类:设置序列化器
我们创建一个 Bean 加入容器,就会触发 RedisTemplate 上的条件注解使默认的 RedisTemplate 失效。
RedisSerializer接口
提供了多种序列化方案:
第一种:自己 new 相应的序列化器实现类,然后 set
新建 config 文件夹,在其下新建 RedisConfig 类
java@Configuration public class RedisConfig { //自己定义的RedisTemplate @Bean @SuppressWarnings("all") //隐藏所有警告 public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { //一般使用<string,Object> RedisTemplate<String, Object> template = new RedisTemplate<String, Object>(); template.setConnectionFactory(factory); //new出Json的序列化器:使用jackson Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); ObjectMapper om = new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jackson2JsonRedisSerializer.setObjectMapper(om); //new出String的序列化器 StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); // key采用String的序列化方式 template.setKeySerializer(stringRedisSerializer); // hash的key也采用String的序列化方式 template.setHashKeySerializer(stringRedisSerializer); // value序列化方式采用jackson template.setValueSerializer(jackson2JsonRedisSerializer); // hash的value序列化方式采用jackson template.setHashValueSerializer(jackson2JsonRedisSerializer); template.afterPropertiesSet(); return template; } }
注:**我们设置了序列化器之后,就不需要在 java 中对对象进行序列化了!**也就是说不需要 implements Serializable 或者序列化为 json 了!!!
所以测试类为:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User{
private String name;
private int age;
}
@SpringBootTest
class RedisSpringApplicationTests {
@Autowired
@Qualifier("redisTemplate") //bean重名了,有歧义,可以使用这个注解,
private RedisTemplate redisTemplate;
@Test
void test() throws JsonProcessingException {
User user = new User("hao", 3);
redisTemplate.opsForValue().set("user", user);
System.out.println(redisTemplate.opsForValue().get("user"));
}
}
测试结果:key 已经不会乱码了!
- 第二种:直接调用 RedisSerializer 的静态方法来返回相应的序列化器,然后 set
@Configuration
public class RedisConfig {
//借鉴源码,写自己的 redisTemplate,覆盖底层的
@Resource
RedisConnectionFactory redisConnectionFactory; // 方法参数注入时错误,使用Resource注入
//这个RedisTemplate覆盖了原来默认的RedisTemplate
@Bean
public RedisTemplate<String, Object> redisTemplate() {
// 为了开发方便,一般将template 泛型设置为 <String, Object>
RedisTemplate<String, Object> template = new RedisTemplate();
// 连接工厂,不必修改
template.setConnectionFactory(redisConnectionFactory);
//序列化设置:调用RedisSerializer的静态方法来返回对应的序列化器
// 普通的key以及hash的key采用 String 序列化方式
template.setKeySerializer(RedisSerializer.string());
template.setHashKeySerializer(RedisSerializer.string());
// 普通的value以及hash的value采用 Jackson—>json 序列化方式
template.setValueSerializer(RedisSerializer.json());
template.setHashValueSerializer(RedisSerializer.json());
template.afterPropertiesSet();
return template;
}
}
测试结果:与上面完全相同!
8.5 自定义 Redis 工具类
RedisTemplate 需要频繁调用.opForxxx
然后才能进行对应的操作,这样使用起来代码效率低下,工作中一般不会使用原生的方式,而
是==将这些常用的公共 API 抽取出来封装成为一个工具类==,然后直接使用工具类来间接操作 Redis,不但效率高并且易用。=
我们先@Autowired 装配工具类,然后直接 redisUtil.方法() 即可!
工具类参考博客:
https://www.cnblogs.com/zeng1994/p/03303c805731afc9aa9c60dbbd32a323.html
https://www.cnblogs.com/zhzhlong/p/11434284.html
9.redis.conf 文件
我们查看 redis.conf 文件进行分析:
1.容量单位不区分大小写,G 和 GB 是有区别的
2.可以使用 include 组合多个配置问题
3.网络配置
4.守护进程(即后台进程)
如果以后台的方式运行,我们就需要指定一个 pid 文件:默认的
daemonize yes //以守护进程方式执行,默认是no
pidfile /var/run/redis_6379.pid //如果以后台守护进程方式执行,需要指定一个PID文件
5.日志输出级别
debug:测试环境
notice:生产环境——>默认
6.日志输出文件
7.默认数据库数量
8.是否显示开启服务的 logo
6.持久化规则:快照
由于 Redis 是基于内存的数据库,需要将数据由内存持久化到文件中
持久化:在规定的时间内执行多少次操作,就会持久化到文件 .rdb .aof
Redis 是内存数据库,如果没有持久化,数据就会断电丢失
持久化方式:
- RDB 文件
- AOF 文件
持久化规则参数:默认的
7.RDB 文件相关
压缩 rdb 文件,需要消耗一些 cpu 的资源!
rdb 文件保存的目录,默认就是当前的目录!(是保存了 redis.serve 的目录,即bin 目录)
8.主从复制相关:多个 Redis 才可以
replicaof <masterip> <masterport> //配置主机ip和端口,相当于命令slaveof
masterauth <master-password> //配置主机密码
9.Security 模块中进行密码设置(连接 Redis 服务需要密码)
默认没有密码:
可以在命令行设置:
config set requirepass "123456"
10.客户端连接相关(一些限制)
maxclients 10000 #最大客户端数量
maxmemory <bytes> #最大内存限制
maxmemory-policy noeviction # 内存达到限制值的处理策略
redis 中的默认的过期策略是 volatile-lru !
的六种方式:
**1、volatile-lru:**只对设置了过期时间的 key 进行 LRU(默认值)
2、allkeys-lru : 删除 lru 算法的 key
**3、volatile-random:**随机删除即将过期 key
**4、allkeys-random:**随机删除
5、volatile-ttl : 删除即将过期的
6、noeviction : 永不过期,返回错误
11.AOF 文件相关
默认是使用 rdb 文件的方式持久化!因为在大部分的情况下,rdb 完全够用了!
每秒执行一次 sync,但是可能会丢失最多 1 秒的数据!
10.持久化——RDB
10.1 什么是 RDB?
RDB:Redis Databases
在指定时间间隔后,将内存中的数据集快照写入磁盘;在恢复时候,直接读取快照文件,进行数据的恢复 ;
rdb 保存的文件是 dump.rdb,可以在配置文件中配置
10.2 RDB 原理
在进行 RDB
的时候,redis
的主线程是不会做 io
操作的,主线程会 fork
一个子线程来完成该操作;
- Redis 调用 forks,同时拥有父进程和子进程
- 子进程将数据写入到一个临时 RDB 文件中
- 当子进程完成对新 RDB 文件的写入时,Redis 用新 RDB 文件替换原来的 RDB 文件,并删除旧的 RDB 文件
这种工作方式使得 Redis 可以从写时复制(copy-on-write)机制中获益(因为是使用子进程进行写操作,而父进程依然可以接收来自客户端的请求。)
10.3 RDB 触发机制及相关命令
触发机制
- 触发持久化的规则满足的情况下,会自动触发持久化
- 执行flushall 命令,也会触发我们的持久化
- 关闭 redis 服务时,也会触发我们的持久化
- 执行 save/bgsave 命令时,也会触发我们的持久化
触发持久化后会自动产生一个 rdb 文件!
保存的文件名默认为dump.rdb
注意点
默认 dir 为./,但是这样有问题,可能会生成两个 dump.rdb 文件,我们将其修改为绝对路径,一般就不会产生问题了!
日志文件也可以设置一下:使用相对路径!
自动恢复机制
如果我们有 rdb 文件,关闭 redis 服务,再开启的话,里面的 key-value 会依然存在!
这是因为 rdb 文件只要在我们的启动目录(bin)下面的时候,redis 启动的时候会自动检查 dump.rdb 并恢复其中的数据!
注:在生产环境时,我们一般要将这个 rdb 文件备份
注:自动恢复是有条件的,我们必须要在 bin 目录下面启动 redis 服务,不能进到 haoconfig 文件夹里面去启动,不然可能导致读取不到 dump.rdb 文件!==使用 redis-server haoconfig/redis.conf 命令==
详细解析
1.save 命令
使用 save
命令,会立刻对当前内存中的数据进行持久化 ,但是会阻塞,也就是服务器就不接受其他操作了;
由于 save
命令是同步命令,会占用 Redis 的主进程。若 Redis数据非常多时,save
命令执行速度会非常慢,阻塞所有客户端的请求。
2.flushall 命令
flushall
命令也会触发持久化 ;
3.触发持久化规则
满足配置条件中的触发条件
可以通过配置文件对 Redis 进行设置, 让它在“ N 秒内数据集至少有 M 个改动”这一条件被满足时, 自动进行数据集保存操作。
4.bgsave 命令
bgsave
是异步进行,进行持久化的时候,redis
还可以将继续响应客户端请求 ;
bgsave 除了可以主动使用,还可以在满足条件的时候自动触发!
bgsave 和 save 对比
命令 | save | bgsave |
---|---|---|
IO 类型 | 同步 | 异步 |
阻塞? | 是 | 是(阻塞发生在 fock(),通常非常快) |
复杂度 | O(n) | O(n) |
优点 | 不会消耗额外的内存 | 不阻塞客户端命令 |
缺点 | 阻塞客户端命令 | 需要 fock 子进程,消耗内存 |
10.4 RDB 优缺点
优点:
- 适合大规模的数据恢复,恢复的效率非常高 ——> 大数据必须要用 RDB
- 对数据的完整性要求不高的场景可以使用
缺点:
- 需要一定的时间间隔进行操作,如果 redis 意外宕机了,这个最后一次修改的数据就没有了(因为RDB 不能及时地把这个数据同步到磁盘里面!)
- fork 进程的时候,会占用一定的内存空间。
11.持久化——AOF
11.1 什么是 AOF?
AOF:Append Only File
将我们所有的命令都记录下来,相当于 history,恢复的时候就把这个文件全部再执行一遍。
以日志的形式来记录每个操作,将 Redis 执行过的所有指令记录下来(读操作不记录),只许追加文件但不可以改写文件,redis
启动之初会读取该文件重新构建数据,换言之,redis 重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工
作。
AOF 对比 RDB:
快照功能(RDB)并不是非常耐久(durable): 如果 Redis 因为某些原因而造成故障停机, 那么服务器将丢失最近写入、以及未
保存到快照中的那些数据。 从 1.1 版本开始, Redis 增加了一种完全耐久的持久化方式: AOF 持久化。
也就是说在 AOF 中:触发机制就一个 ——> 就是 AOF 自己配置的规则(每秒,每次,从不)!
11.2 修改配置文件
如果要使用 AOF,需要修改配置文件:
appendonly no yes 则表示启用 AOF
默认是不开启的,我们需要手动配置,然后重启 redis,就可以生效了!
11.3 AOF 的修复
如果 aof 文件有错位(人为破坏或者发生了意外),这时候 redis 是启动不起来的,我需要修改这个 aof 文件
redis 给我们提供了一个工具 redis-check-aof,修复正常后,redis 就可以正常启动了!
在 bin 目录下的命令行输入:
redis-check-aof --fix appendonly.aof //修复aof文件
11.4 AOF 重写机制
为了解决 AOF 文件体积膨胀的问题,Redis 提供了 AOF 重写功能:Redis 服务器可以创建一个新的 AOF 文件来替代现有的 AOF 文件,新旧
两个文件所保存的数据库状态是相同的,但是新的 AOF 文件不会包含任何浪费空间的冗余命令,通常体积会较旧 AOF 文件小很多。
如果 aof 文件大于 64m,那么会 fork 一个新的进程来将我们的文件进行重写(写到一个新的文件里面)!
11.4 AOF 优缺点
**优点:**保证数据完整性
AOF 的三种触发机制分别的特点:
- 每一次修改都会同步,文件的完整性会更加好
- 每秒同步一次,可能会丢失至多一秒的数据
- 从不同步,操作系统自行决定,相对上面两个效率最高
缺点:
- 相对于数据文件来说,aof 远远大于 rdb,修复速度比 rdb 慢!
- aof 运行效率也要比 rdb 慢,所以我们 redis 默认的配置就是 rdb 持久化
11.5. RDB 与 AOF 的选择
RDB 和 AOF 对比
RDB | AOF | |
---|---|---|
启动优先级 | 低 | 高 |
文件体积 | 小 | 大 |
恢复速度 | 快 | 慢 |
数据安全性 | 丢数据 | 根据策略决定 |
如何选择使用哪种持久化方式?
一般来说,如果想达到足以媲美 PostgreSQL 的数据安全性,你应该同时使用两种持久化功能。
如果你非常关心你的数据,但仍然可以承受数分钟以内的数据丢失,那么你可以只使用 RDB 持久化。
一般定义 RDB 规则:save 900 1 ——> 每 15 分钟如果就变动就持久化!
有很多用户都只使用 AOF 持久化,但并不推荐这种方式:因为定时生成 RDB 快照(snapshot)非常便于进行数据库备份,并且
RDB 恢复数据集的速度也要比 AOF 恢复的速度要快。
12.Redis 发布订阅
12.1 概述
Redis 发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。
下图展示了频道 channel1 , 以及订阅这个频道的三个客户端 —— client2 、 client5 和 client1 之间的关系:
当有新消息通过 PUBLISH 命令发送给频道 channel1 时, 这个消息就会被发送给订阅它的三个客户端:
12.2 深入
相关命令
示例
------------订阅端----------------------
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 属性是一个字
典,这个字典就用于保存订阅频道的信息,其中字典的键为正在被订阅的频道,而==字典的值则是一个链表==,链表中保存了所有订
阅这个频道的客户端。
客户端订阅,就被链接到对应频道的链表的尾部,退订则就是将客户端节点从链表中移除。
总结
所以说,发布订阅系统中:发布=字典,订阅=字典中的 value(即链表),频道=字典中的 key
缺点
- 如果一个客户端订阅了频道,但自己读取消息的速度却不够快的话,那么不断积压的消息会使 redis 输出缓冲区的体积变得越来越大,这可能使得 redis 本身的速度变慢,甚至直接崩溃。
- 这和数据传输可靠性有关,如果在订阅方断线,那么他将会丢失所有在断线期间发布者发布的消息。
应用
- 消息订阅:公众号订阅,微博关注等等(其实更多是使用消息队列 MQ 来进行实现)
- 多人在线聊天室:把聊天室当做频道,将消息回显给所有人即可
- 实时消息系统
13.Redis 主从复制
13.1 概述
概念
主从复制,是指将一台 Redis 服务器的数据,复制到其他的 Redis 服务器。
前者称为主节点(Master/Leader),后者称为从节点(Slave/Follower),==数据的复制是单向的!只能由主节点复制到从节点==
(主节点以写为主、从节点以读为主)。
默认情况下,每台 Redis 服务器都是主节点,一个主节点可以有 0 个或者多个从节点,但每个从节点只能有一个主节点。
作用
数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余的方式
故障恢复:当主节点故障时,从节点可以暂时替代主节点提供服务,是一种服务冗余的方式
负载均衡:在主从复制的基础上,配合读写分离,由主节点进行写操作,从节点进行读操作,分担服务器的负载;尤其是在多读少
写的场景下,通过多个从节点分担负载,提高并发量
高可用基石:主从复制是哨兵和集群能够实施的基础
为什么使用集群?
- 单台服务器难以负载大量的请求
- 单台服务器故障率高,系统崩坏概率大
- 单台服务器内存容量有限。
电商网站上的商品,一般都是一次上传,无数次浏览的,说专业点也就是“多读少写”
对于这种场景,我们可以使用如下的架构:
13.2 环境配置
我们在讲解配置文件的时候,注意到有一个replication
模块 (见 redis.conf 中)
1.查看当前库的信息:info replication
主从复制的信息
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
2.既然需要启动多个服务,就需要多个配置文件。每个配置文件对应修改以下信息:
- 端口号
- pid 文件名
- 日志文件名
- rdb 文件名
一主二从配置
==默认情况下,每台 Redis 服务器都是主节点;==我们一般情况下只用配置从机就好了!
认老大!一主(79)二从(80,81)
使用slaveof host port
命令就可以为从机配置它的主机了!
而从节点变主节点命令为 slaveof no one

1.开启四个端口当做四台主机,准备开启四个 Redis 服务:单机多服务!
2.复制 redis.conf 分别命名为 redis79.conf、redis80.conf、redis81.conf
并修改配置文件
// redis79.conf:修改两个地方
logfile "6379.log"
dbfilename dump6379.rdb
// redis80.conf:修改四个地方
port 6380
pidfile /var/run/redis_6380.pid //pid文件必须改,这是运行在后台会产生的文件!
logfile "6380.log"
dbfilename dump6380.rdb
redis81.conf:
port 6381
pidfile /var/run/redis_6381.pid
logfile "6381.log"
dbfilename dump6381.rdb
3.启动 3 个 Redis 服务,搭建一主二从,只用配置从机,使用 slaveof 命令!
默认每台机器都是这样的:
从机 80:
slaveof 127.0.0.1 6379
从机 81:
slaveof 127.0.0.1 6379
查看主机 replication :如果开启了密码是看不到从机的!
查看从机 replication
注:真正的主从配置是在配置文件中配置的,我们这里是用命令配置的,是暂时的,重启服务就失效了!
在配置文件中配置主从配置:
replicaof <masterip> <masterport> //配置主机ip和端口,相当于命令slaveof
masterauth <master-password> //配置主机密码
13.3 总结
从机只能读,不能写,主机可读可写但是多用于写,主机中的所有数据和信息都会被从机主动保存
bash127.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"
当主机断开连接后,默认情况下从机的角色不会发生变化,集群中只是失去了写操作,当主机恢复以后,又会连接上从机恢复原状。
当从机断开连接后,若是用命令行配置的主从而不是使用配置文件配置的从机,则再次启动后原来的从机将自动恢复为主机是无法
直接获取之前作为从机时连接到的主机的数据的,若此时重新把这个从机配置为之前连接的主机的从机,则又可以获取到主机的
所有数据,这里就要提到一个同步原理。
主从同步原理
只要是 slave 连接了 master,一次全量复制将会被自动执行,之后主机上新增的操作也会增量复制到从机
- **全量复制:**master 启动存盘程序生成 rdb 文件,master 向 salve 发送 rdb 文件,salve 接收到后将其存盘并加载到内存中(实现了数据一致)
- **增量复制:**master 会继续将所有的接收到的用于修改数据集的命令(在全量复制之后发生的写和修改成操作命令)传送给 salve(保持数据的一致)
注:同步原理保证了主从节点保持数据的一致性!
关于第 2 条的补充,默认情况下,主机故障后,不会出现新的主机,有两种方式可以产生新的主机:
方式一:如果没有老大了,这个时候能不能选择出来一个老大呢?手动!莫权篡位
如果主机断开了连接,我们可以使用
slaveof no one
让自己变成主机!这样执行以后从机会独立出来成为一个主机,其他的节点就可以手动连接到最新的主节点(手动)!但是如果这个时候老大修复了,那么就会重新连接!
**缺点:**当主服务器宕机后,如果手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用
方式二:使用哨兵模式(自动选举)——> 后面讲解
除了一主二从还有没有其他的配置方案?
链路模型
第一节传给第二节,第三节传给第四节!
同样只有主节点(最开头的那个节点)才可写入 ——> 来保证数据的一致性
注:下面这个图中,虽然理论上它既是主节点又是从节点,但是实际在显示上它是从节点!!!(所以依然不能够写入)
14.Redis 哨兵模式
简单来说就是自动选举老大!(自动选举 master)
14.1 概述
更多信息参考博客:https://www.jianshu.com/p/06ab9daf921d
由来
主从切换技术的方法是:当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用。这是一种不推荐的方式,更多时候,我们优先考虑哨兵模式。
概念
哨兵模式:自动监控主机是否出故障,当主机出故障后,根据票数自动将从机切换为主机,哨兵是一个独立的进程,通过向 Redis 服务器发送命令并等待响应,从而监控多个运行的 Redis 实例,如果没有响应就认为是出现了故障! (监控主机)
相当于是谋权篡位的自动版,能够后台监控主机是否故障,如果故障了就根据通票数==自动将从库转换为主库==!
哨兵的作用
- 通过发送命令,让 Redis 服务器返回监控其运行状态,包括主服务器和从服务器。
- 当哨兵监测到 master 宕机,会自动将 slave 切换成 master,然后通过发布订阅模式通知其他的从服务器,修改配置文件,让它们切换主机。
单机单个哨兵
4 个进程
一个哨兵对 Redis 服务器进行监控可能会出现问题(比如哨兵宕机了),我们可以使用多个哨兵进行监控,各个哨兵之间还会监控,这样就形成了多哨兵模式 。
多哨兵模式(哨兵的数量必须是奇数)
6 个进程
假设主服务器宕机,哨兵 1 先检测到这个结果,系统并不会马上进行 failover 过程,仅仅是哨兵 1 主观的认为主服务器不可用,这个现象
称为主观下线,当后面的哨兵也检测到主服务器不可用且数量到达一定值时,哨兵之间会进行一次投票,投票结果由一个哨兵发起,进
行 failover 故障转移操作,通过订阅发布模式(哨兵是频道),让每个哨兵对自己监控的从服务器投票,实现切换主机,这个过程称为
客观下线
投票是有投票算法的!
14.2 实战应用
1.首先搭建一主二从模式
2.编写哨兵配置文件 sentinel.conf,与 redis.conf 放在一起
vim sentinel.conf
# 输入以下内容:
sentinel monitor myredis 127.0.0.1 6379 1 #哨兵的核心配置,这里为了防止性能不够,我们只监视一台!
# 参数说明:
# myredis 自己取的监控名称,可以任意
# 监控ip和端口
# 数字1表示 :当一个哨兵主观认为主机断开,就可以客观认为主机故障,然后开始选举新的主机
2.开启哨兵模式:核心命令 redis-sentiel
redis-sentinel sentinel.conf
此时关闭主机 6379 的服务
观察 6380 和 6381,可以发现 81 已经自动变为主机,80 变为其从机

5.哨兵日志:在开启服务的那个窗口中
主机断开后,这时会从从机中选择一个服务器作为主机(投票算法),如果这时再连回之前的主机可以发现,之前的主机已经变为从机
了,无法再夺权回来 ——> 这是与手动谋权篡位的区别!
14.3 优缺点
优点:
- 哨兵集群,基于主从复制模式,所有主从复制的优点,它都有
- 主从可以切换,故障可以转移,系统的可用性更好
- 哨兵模式是主从模式的升级,手动到自动,更加健壮
缺点:
- Redis 不好在线扩容,集群容量一旦达到上限,在线扩容就十分麻烦
- 实现哨兵模式的配置其实是很麻烦的,里面有很多配置项
哨兵模式的全部配置
# 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 123456
# 指定多少毫秒之后 主节点没有应答哨兵sentinel 此时 哨兵主观上认为主节点下线 默认30秒
# sentinel down-after-milliseconds <master-name> <milliseconds>
sentinel down-after-milliseconds mymaster 30000
# 这个配置项指定了在发生failover主备切换时最多可以有多少个slave同时对新的master进行 同步,
这个数字越小,完成failover所需的时间就越长,
但是如果这个数字越大,就意味着越 多的slave因为replication而不可用。
可以通过将这个值设为 1 来保证每次只有一个slave 处于不能处理命令请求的状态。
# sentinel parallel-syncs <master-name> <numslaves>
sentinel parallel-syncs mymaster 1
# 故障转移的超时时间 failover-timeout 可以用在以下这些方面:
#1. 同一个sentinel对同一个master两次failover之间的间隔时间。
#2. 当一个slave从一个错误的master那里同步数据开始计算时间。直到slave被纠正为向正确的master那里同步数据时。
#3.当想要取消一个正在进行的failover所需要的时间。
#4.当进行failover时,配置所有slaves指向新的master所需的最大时间。不过,即使过了这个超时,slaves依然会被正确配置为指向master,但是就不按parallel-syncs所配置的规则来了
# 默认三分钟
# sentinel failover-timeout <master-name> <milliseconds>
sentinel failover-timeout mymaster 180000
# SCRIPTS EXECUTION
#配置当某一事件发生时所需要执行的脚本,可以通过脚本来通知管理员,例如当系统运行不正常时发邮件通知相关人员。
#对于脚本的运行结果有以下规则:
#若脚本执行后返回1,那么该脚本稍后将会被再次执行,重复次数目前默认为10
#若脚本执行后返回2,或者比2更高的一个返回值,脚本将不会重复执行。
#如果脚本在执行过程中由于收到系统中断信号被终止了,则同返回值为1时的行为相同。
#一个脚本的最大执行时间为60s,如果超过这个时间,脚本将会被一个SIGKILL信号终止,之后重新执行。
#通知型脚本:当sentinel有任何警告级别的事件发生时(比如说redis实例的主观失效和客观失效等等),将会去调用这个脚本,
#这时这个脚本应该通过邮件,SMS等方式去通知系统管理员关于系统不正常运行的信息。调用该脚本时,将传给脚本两个参数,
#一个是事件的类型,
#一个是事件的描述。
#如果sentinel.conf配置文件中配置了这个脚本路径,那么必须保证这个脚本存在于这个路径,并且是可执行的,否则sentinel无法正常启动成功。
#通知脚本:可以用shell编程
# sentinel notification-script <master-name> <script-path>
sentinel notification-script mymaster /var/redis/notify.sh
# 客户端重新配置主节点参数脚本
# 当一个master由于failover而发生改变时,这个脚本将会被调用,通知相关的客户端关于master地址已经发生改变的信息。
# 以下参数将会在调用脚本时传给脚本:
# <master-name> <role> <state> <from-ip> <from-port> <to-ip> <to-port>
# 目前<state>总是“failover”,
# <role>是“leader”或者“observer”中的一个。
# 参数 from-ip, from-port, to-ip, to-port是用来和旧的master和新的master(即旧的slave)通信的
# 这个脚本应该是通用的,能被多次调用,不是针对性的。
# sentinel client-reconfig-script <master-name> <script-path>
sentinel client-reconfig-script mymaster /var/redis/reconfig.sh #一般是运维来配置
15.缓存会产生的问题
15.1 缓存穿透(大面积查不到)
概念
在默认情况下,用户请求数据时,会先在缓存(Redis)中查找,若 Redis 内存数据库中没有,也就是缓存没有命中,再在数据库中进
行查找,数量少可能问题不大,可是一旦大量的请求数据(例如秒杀场景)缓存都没有命中的话,就会全部转移到数据库上,造成数据
库极大的压力,就有可能导致数据库崩溃,这就是缓存穿透。网络安全中也有人恶意使用这种手段进行攻击被称为洪水攻击。
解决方案
==1.布隆过滤器==
布隆过滤器是一种数据结构,对所有可能查询的参数以 Hash 的形式存储,以便快速确定是否存在这个值,在控制层先进行拦截校验,校
验不通过直接打回,减轻了存储系统的压力。
2.缓存空对象
一次请求若在缓存和数据库中都没找到,就在缓存中放一个该请求的空对象用于处理后续请求。(后面我们就知道这个东西没有,我们就不会给它查了!)
但是这样做有两个问题:
1.存储空对象也需要空间,大量的空对象会耗费一定的空间,存储效率并不高。解决这个缺陷的方式就是==设置缓存内容的较短过期时间==
2.即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致(缓存里面过期了但是数据库还不知道,
两边数据就不一样了),这对于需要保持一致性的业务会有影响。
15.2 缓存击穿(访问量太大,缓存过期)
概念
相较于缓存穿透,缓存击穿的目的性更强,缓存击穿是指缓存中存在一个热点 key,用户大并发集中对这点进行访问,在缓存过期(这
个 key 失效后)的一刻,持续的大并发请求就直接击穿到数据库,造成瞬时 DB 请求量大、压力骤增,有可能导致数据库崩溃,这就是
缓存击穿。
比如热搜排行上,一个热点新闻被同时被大量访问就可能导致缓存击穿。
解决方案
1.设置热点数据永不过期
这样就不会出现热点数据过期的情况,但是当 Redis内存空间满的时候也会清理部分数据,而且此种方案会占用空间,一旦热点数据多
了起来,就会占用部分空间。
==2.加互斥锁(分布式锁)==
在访问数据库的 key 之前,采用 SETNX(set if not exists)来设置另一个短期 key 来锁住当前 key 的访问,访问结束再删除该短期
key。保证同时刻只有一个线程访问,但是这样对锁的要求就十分高。
15.3 缓存雪崩(缓存同时过期)
概念
在某一个时间段,缓存中大量的 key 集中过期(缓存的过期时间类似)或者 Redis 宕机(缓存直接没了),导致缓存在同一时刻全部失
效,造成瞬时数据库请求量大、压力骤增,引起雪崩,有可能导致数据库崩溃。
解决方案
1.redis 高可用
这个思想的含义是,既然 redis 有可能挂掉,那我多增设几台 redis,这样一台挂掉之后其他的还可以继续工作,其实就是搭建集群
比如双十一:要停掉一些服务,保证主要的服务高可用!
2.限流降级(在 SpringCloud 中讲过)
这个解决方案的思想是,在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个 key 只允许一个线程查询数据
和写缓存,其他线程等待。
3.数据预热
数据预热的含义就是在正式部署之前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。在即将发
生大并发访问前手动触发加载缓存不同的 key,设置不同的过期时间,让缓存失效的时间点尽量均匀。