一、缓存发展史&缓存分类
1.1、大型网站中缓存的使用
访问量越大,响应力越差,用户体验越差。
引入缓存、示意图如下:
读写策略:
- Cache Aside Pattern(旁路缓存模式)
- Read/Write Through Pattern(读写穿透模式)
- Write Behind Pattern(异步缓存写入)
高性能 :
假如用户第一次访问数据库中的某些数据的话,这个过程是比较慢,毕竟是从硬盘中读取的。但是,如果说,用户访问的数据属于高频数据并且不会经常改变的话,那么我们就可以很放心地将该用户访问的数据存在缓存中。
这样有什么好处呢? 那就是保证用户下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。
不过,要保持数据库和缓存中的数据的一致性。 如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据即可!
高并发:
一般像 MySQL 这类的数据库的 QPS 大概都在 1w 左右(4 核 8g) ,但是使用 Redis 缓存之后很容易达到 10w+,甚至最高能达到
30w+(就单机 redis 的情况,redis 集群的话会更高)。
QPS(Query Per Second):服务器每秒可以执行的查询次数;
所以,直接操作缓存能够承受的数据库请求数量是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。进而,我们也就提高的系统整体的并发。
1.2、常见缓存的分类
分布式缓存
分布式缓存主要解决的是单机缓存的容量受服务器限制并且无法保存通用的信息。因为,本地缓存只在当前服务里有效,比如如果你部署了两个相同的服务,他们两者之间的缓存数据是无法共同的。
具有缓存功能的中间件:Redis、Memcache、Tair(阿里 、美团)等等。
1.3、分布式缓存选型方案对比
Memcache和Redis区别
共同点
- 都是基于内存的数据库,一般都用来当做缓存使用。
- 都有过期策略。
- 两者的性能都非常高。
区别
- Redis 支持更丰富的数据类型(支持更复杂的应用场景)。Redis 不仅仅支持简单的 k/v 类型的数据,同时还提供 list,set,zset,hash 等数据结构的存储。Memcached 只支持最简单的 k/v 数据类型。
- Redis 支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而 Memecache 把数据全部存在内存之中。
- Redis 有灾难恢复机制。 因为可以把缓存中的数据持久化到磁盘上。
- Memcached 没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;但是 Redis目前是原生支持 cluster 模式的.
- Memcached 是多线程,非阻塞 IO 复用的网络模型;Redis 使用单线程的多路 IO 复用模型。(Redis 6.0 引入了多线程 IO)
相信看了上面的对比之后,我们已经没有什么理由可以选择使用 Memcached 来作为自己项目的分布式缓存了。
二、Redis概述&安装配置
2.1、概述
官网:https://redis.io
中文官网地址:http://www.redis.cn
简单来说 Redis 就是一个使用 C 语言开发的数据库 ,不过与传统数据库不同的是 Redis 的数据是存在内存中的
,也就是它是内存数据库,所以读写速度非常快,因此 Redis 被广泛应用于缓存方向。
另外, Redis 除了做缓存之外,Redis 也经常用来做分布式锁,甚至是消息队列 。
Redis 提供了多种数据类型来支持不同的业务场景。Redis 还支持事务 、持久化、Lua 脚本、多种集群方案。
Redis应用场景
- 缓存使用,减轻DB压力
- DB使用,用于临时存储数据(字典表,购买记录)
- 解决分布式场景下Session分离问题(登录信息)
- 任务队列(秒杀、抢红包等等) 乐观锁
- 应用排行榜 zset
- 签到 bitmap
- 分布式锁
- 冷热数据交换
2.2、安装&配置
- 参考一:https://blog.csdn.net/qq_37242720/article/details/115001586
- 参考二:https://blog.csdn.net/qq_37242720/article/details/113654545
三、数据类型选择&应用场景
3.1、Redis的Key的设计规范
3.1.1、key名设计
可读性和可管理性
以业务名(或数据库名)为前缀(防止key冲突),用冒号分隔,比如 业务名:表名:id
ugc:video:1
简洁性
保证语义的前提下,控制key的长度,当key较多时,内存占用也不容忽视,例如:
user:{uid}:friends:messages:{mid} 简化为 u:{uid}:fr:m:{mid}
字符串大小(字节) | SDS结构元数据大小(字节) |
---|---|
1-25-1 | 1 |
25 - 28 - 1 | 3 |
28 - 216 - 1 | 5 |
216 - 232 - 1 | 9 |
232 - 264 - 1 | 17 |
不要包含特殊字符
反例:包含空格、换行、单双引号以及其他转义字符
3.1.2、避免bigkey
- 键值对的值大小本身就很大,例如value为1MB的String数据类型。为了避免String类型的bigKey,在业务层,我们要尽量把String类型的大小控制在10KB以下。
- 键值对的值是集合类型,集合元素个数非常多,例如包含100万个元素的Hash集合类型数据。为了避免集合类型的bigkey,对应的设计规范是,尽量把集合类型的元素个数控制在1万以下。
3.2、string字符串类型
string 数据结构是简单的 key-value 类型。虽然 Redis 是用 C 语言写的,但是 Redis 并没有使用 C
的字符串表示,而是自己构建了一种简单动态字符串(simple dynamic string,SDS)
。相比于 C 的原生字符串,Redis 的 SDS
不光可以保存文本数据还可以保存二进制数据,并且获取字符串长度复杂度为 O(1)(C 字符串为 O(N)),除此之外,Redis 的 SDS API
是安全的,不会造成缓冲区溢出。
常用命令:set
、get
、strlen
、exists
、decr
、incr
、setex
等等。
应用场景 :一般常用在需要计数的场景,比如用户的访问次数、热点文章的点赞转发数量等等。
## 单值缓存
SETkey value
GET key
## 对象缓存
MSET user:1:name zimu user:1:balance 1888
MGET user:1:name user:1:balance
## 分布式锁(「SET if Not eXists」)
SETNX product:10001 true # 返回1代表获取锁成功
SETNX product:10001 false # 返回0代表获取锁失败
#.......执行业务操作
DEL product:10001 # 执行完业务 释放锁
SET product:10001 true ex 10 nx # 防止程序意外终止导致死锁
## 计数器
INCR article:readcount:101
3.3、hash类型(散列表)
hash 类似于 JDK1.8 前的 HashMap,内部实现也差不多(数组 + 链表)。不过,Redis 的hash 做了更多优化。另外,hash 是一个
string 类型的 field 和 value 的映射表,特别适合用于存 储对象,后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的值。
比如我们可以 hash 数据结构来存储用户信息,商品信息等等。
常用命令:hset
、hmset
、hexists
、hget
、hgetall
、hkeys
、hvals
等。
应用场景: 系统中对象数据的存储。
## 对象缓存
HMSET user {userId}:username zhangfei {userId}:password 123456
HMSET user 1:username zhangfei 1:password 123456
HMGET user 1:username 1:password
## 电商购物
# 以用户id为key
# 商品id为field
# 商品数量为key
## 购物车操作
# 1)添加商品 ---> hset cart:1001 10088 1
# 2) 增加数量 ---> hincrby cart:1001 10088 1
# 3) 商品总数 ---> hlen cart:1001
# 4) 删除商品---> hdel cart:1001 10088
# 5)获取购物车所有商品---> hgetall cart:1001
优点
- 同类数据归类整合储存,方便数据管理。
- 相比String操作消耗内存和cpu更小。
- 相比String储存 更节省空间。
缺点
- 过期功能不能使用在field上,只能用在key上。
- Redis集群架构下不适合大规模使用。
3.4、list列表类型
list 即是 链表。链表是一种非常常见的数据结构,特点是易于数据元素的插入和删除并且
且可以灵活调整链表长度,但是链表的随机访问困难。许多高级编程语言都内置了链表的实现比如 Java 中的 LinkedList,但是 C
语言并没有实现链表,所以 Redis 实现了自己的链表数据结构。
Redis 的 list 的实现为一个 双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。
常用命令:rpush
、lpop
、lpush
、rpop
、lrange
、llen
等。
应用场景: 发布与订阅或者说消息队列、慢查询。
常用数据结构
- Stack(栈)= LPUSH(左边放) + LPOP(左边取) --> FILO
- Quece(队列)= LPUSH(左边放) + RPOP右边取)
- BLocking MQ(阻塞队列)= LPUSH(左边放) + BRPOP(右边阻塞取:没有数据就阻塞!)
微博、朋友圈、公众号等,关注的文章列表展示
3.5、set集合类型
set 类似于 Java 中的 HashSet 。Redis 中的 set
类型是一种无序集合,集合中的元素没有先后顺序。当你需要存储一个列表数据,又不希望出现重复数据时,set 是一个很好的选择,并且set
提供了判断某个成员是否在一个 set 集合内的重要接口,这个也是 list 所不能提供的。可以基于 set
轻易实现交集、并集、差集的操作。比如:你可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis
可以非常方便的实现如共同关注、共同粉丝、共同喜好等功能。这个过程也就是求交集的过程。
常用命令:sadd
、spop
、smembers
、sismember
、scard
、sinterstore
、sunion
等。
应用场景:需要存放的数据不能重复以及需要获取多个数据源交集和并集等场景。
微信抽奖小程序
1、点击 参与抽奖 加入集合
SADD key {userID}
2、查看排行榜
SMEMBERS key
3、抽取count名中奖者
SRANDMEMBER key [count] / SPOP key [count]
集合操作实现微博、微信关注模型
首先了解一下set的集合操作,假如有三个集合
- 交集为:SINTER set1 set2 set3 ==> { c }
- 并集为:SUNION set1 set2 set3 ==> { a,b,c,d,e }
- 差集为:SDIFF set1 set2 set3 ==> { a }
- 差集计算方式:set1 - (set2并set3) = {a、b、c} - {b、c、d、e} = {a} 只保留a中单独存在的元素
共同关注A的人 :可以用交集来实现
我可能认识的人 :可以使用差集来实现,把我关注的人求差集
3.6、sortedset有序集合类型
和 set 相比,sorted set 增加了一个权重参数 score,使得集合中的元素能够按 score 进行有序排列,还可以通过 score
的范围来获取元素的列表。有点像是 Java 中 HashMap 和 TreeSet的结合体。
常用命令:zadd
、zcard
、zscore
、zrange
、zrevrange
、zrem
等。
应用场景:需要对数据根据某个权重进行排序的场景。比如在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,弹幕消息(可以理解为按消息维度的消息排行榜)等信息。
Zset集合操作实现排行榜
1、点击新闻,为其分值+1
ZINCRBY hotNews:20210707 1 iphone13或有日落金玫瑰金
2、展示当日排行前10
ZREVRANGE hotNews:20210707 0 ,9 WITHSCORES
3.7、bitmap位图 类型
bitmap 存储的是连续的二进制数字(0 和 1),通过 bitmap, 只需要一个 bit 位来表示某个元素对应的值或者状态,key 就是对应元素本身
。我们知道 8 个 bit 可以组成一个 byte,所以bitmap 本身会极大的节省储存空间。
常用命令:setbit
、getbit
、bitcount
、bitop
。
应用场景:适合需要保存状态信息(比如是否签到、是否登录…)并需要进一步对这些信息进行分析的场景。比如用户签到情况、活跃用户情况、用户行为统计(比如是否点赞过某个视频)。
例:存储对比:
有1亿用户,5千万登陆用户,那么统计每日用户的登录数。每一位标识一个用户ID,当某个用户访问我们的网站就在Bitmap中把标识此用户的位设置为1。
这里做了一个使用set集合和BitMap存储的对比。
数据类型 | 每个 userid 占用空间 | 需要存储的用户量 | 全部占用内存量 |
---|---|---|---|
set(集 合) | 32位也就是4个字节(假设userid用的是整型, 实际很多网站用的是长整型) | 50,000,000 | 32位 * |
50,000,000 = 200 MB | |||
BitMap | 1 位(bit) | 100,000,000 | 1 位 * 100,000,000 = 12.5 MB |
时间在拉长一点
set(集合) 200M 6G 72G BitMap 12.5M 375M 4.5G
类型 | 一天 | 一月 | 一年 |
---|---|---|---|
set(集合) | 200M | 6G | 72G |
BitMap | 12.5M | 375M | 4.5G |
# SETBIT 会返回之前位的值(默认是 0)这里会生成 7 个位
127.0.0.1:6379> setbit mykey 7 1
(integer) 0
127.0.0.1:6379> setbit mykey 7 0
(integer) 1
127.0.0.1:6379> getbit mykey 7
(integer) 0
127.0.0.1:6379> setbit mykey 6 1
(integer) 0
127.0.0.1:6379> setbit mykey 8 1
(integer) 0
# 通过 bitcount 统计被被设置为 1 的位的数量。
127.0.0.1:6379> bitcount mykey
(integer) 2Copy to clipboardErrorCopied
针对上面提到的一些场景,这里进行进一步说明。
使用场景一: 用户行为分析 很多网站为了分析你的喜好,需要研究你点赞过的内容。
面试题:现在系统有亿级的活跃用户,为了增强用户粘性,该如何实现签到、日活统计?
使用时间作为 key,然后用户 ID 为 offset,如果当日活跃过就设置为1。
那么我该如果计算某几天/月/年的活跃用户呢(暂且约定,统计时间内只有有一天在线就称为活跃),有请下一个 redis 的命令。
# 对一个或多个保存二进制位的字符串 key 进行位元操作,并将结果保存到 destkey 上。
# BITOP 命令支持 AND 、 OR 、 NOT 、 XOR 这四种操作中的任意一种参数
BITOP operation destkey key [key ...]
## 初始化数据:
127.0.0.1:6379> setbit 20210308 1 1
(integer) 0
127.0.0.1:6379> setbit 20210308 2 1
(integer) 0
127.0.0.1:6379> setbit 20210309 1 1
(integer) 0
## 统计 20210308~20210309 总活跃用户数: 1
127.0.0.1:6379> bitop and desk1 20210308 20210309
(integer) 1
127.0.0.1:6379> bitcount desk1
(integer) 1
## 统计 20210308~20210309 在线活跃用户数: 2
127.0.0.1:6379> bitop or desk2 20210308 20210309
(integer) 1
127.0.0.1:6379> bitcount desk2
(integer) 2
3.8、geo地理位置类型
3.8.1、概述
Redis 3.2
中增加了对GEO类型的支持。GEO,Geographic,地理信息的缩写。该类型,就是元素的2维坐标,在地图上就是经纬度。redis基于该类型,提供了经纬度设置,查询,范围查询,距离查询,经纬度Hash等常见操作
应用场景:附近的人、摇一摇、附近的车、附近银行站点查询
3.8.2、环境要求
- redis版本需要3.2及以上。
- 如果使用jedis操作redis,需要jedis版本为2.9及以上。
- 如果使用spring data redis操作redis,需要spring data redis版本为1.8.0及以上。
3.8.3、redis GEO常用命令
在学习geo命令时会使用到经纬度坐标信息,可以在百度地图的拾取坐标系统中获取测试坐标信息,网址:http://api.map.baidu.com/lbsapi/getpoint/index.html
1、geoadd命令
为了进行地理位置相关操作, 我们首先需要将具体的地理位置记录起来, 这一点可以通过执行 geoadd 命令来完成, 该命令的基本格式如下:
GEOADD location-set longitude latitude name [longitude latitude name ...]
此命令用于添加位置信息到集合中
以下代码展示了如何通过 GEOADD 命令, 将武汉、襄阳、宜昌、枝江、咸宁等数个湖北省的市添加到位置集合 hubeiCities 集合里面
此处添加武汉的坐标信息到hubeiCities集合中
geoadd hubeiCities 114.32538 30.534535 wuhan
此处添加襄阳、枝江、咸宁的坐标信息到hubeiCities集合中
geoadd hubeiCities 112.161882 32.064505 xiangyang 111.305197 30.708127 yichang
111.583717 30.463363 zhijiang 114.295174 29.885892 xianning
2、geopos命令
此命令用于根据输入的位置名称获取位置的坐标信息,基本语法如下:
GEOPOS location-set name [name ...]
案例:查询襄阳市的位置信息
geopos hubeiCities xiangyang
# 结果如下【1为经度 2为纬度】
1) "112.16188341379165649"
2) "32.06450528704699821"
## 也可以一次查询多个位置的经纬度
geopos hubeiCities xiangyang wuhan
# 襄阳的经纬度
1) 1) "112.16188341379165649"
2) "32.06450528704699821"
# 武汉的经纬度
2) 1) "114.32538002729415894"
2) "30.53453492166421057"
3、geodist命令
此命令用于计算两个位置之间的距离,基本语法如下:
GEODIST location-set location-x location-y [unit]
可选参数 unit 用于指定计算距离时的单位, 它的值可以是以下单位的其中一个:
- m 表示单位为米。
- km 表示单位为千米。
- mi 表示单位为英里。
- ft 表示单位为英尺。
案例:分别以默认距离单位和指定距离单位计算襄阳和武汉的距离
## 不指定距离单位
127.0.0.1:6381> geodist hubeiCities xiangyang wuhan
"266889.7642"
## 指定距离单位km
127.0.0.1:6381> geodist hubeiCities xiangyang wuhan km
"266.8898"
4、georadius命令和georadiusbymember命令
这两个命令都可以用于获取指定范围内的元素,也即查找特定范围之内的其他存在的地点。比如找出地点A范围200米之内的所有地点,找出地点B范围50公里之内的所有地点等等。
这两个命令的作用一样, 只是指定中心点的方式不同: georadius 使用用户给定的经纬度作为计算范围时的中心点, 而 georadiusbymember
则使用储存在位置集合里面的某个地点作为中心点。
以下是这两个命令的基本语法
GEORADIUS location-set longitude latitude radius m|km|ft|mi [WITHCOORD]
[WITHDIST] [ASC|DESC] [COUNT count]
GEORADIUSBYMEMBER location-set location radius m|km|ft|mi [WITHCOORD] [WITHDIST]
[ASC|DESC] [COUNT count]
这两个命令的各个参数的意义如下:
m|km|ft|mi 指定的是计算范围时的单位;
如果给定了WITHCOORD,那么在返回匹配的位置时会将位置的经纬度一并返回;
如果给定了WITHDIST , 那么在返回匹配的位置时会将位置与中心点之间的距离一并返回;
在默认情况下, GEORADIUS 和 GEORADIUSBYMEMBER 的结果是未排序的, ASC 可以让查找结果根据距离从近到远排序, 而 DESC
则可以让查找结果根据从远到近排序;
COUNT参数用于指定要返回的结果数量
下面通过案例分别演示georadius命令和georadiusbymember命令
GEORADIUS案例:
在hubeiCities位置集合中查找距离经纬度为112.927076
28.235653(长沙)500km以内的位置信息,查找结果中应包含不超过5个位置的坐标信息,距离信息,并按距离由近到远排序。
127.0.0.1:6381> georadius hubeiCities 112.927076 28.235653 500 km withcoord
withdist asc count 5
-- 咸宁 距离目标位置226.67公里
1) 1) "xianning"
2) "226.6716"
3) 1) "114.29517298936843872"
2) "29.88589217282589772"
-- 枝江 距离目标位置279.91公里
2) 1) "zhijiang"
2) "279.9154"
3) 1) "111.58371716737747192"
2) "30.46336248623112652"
-- 武汉 距离目标位置289.38公里
3) 1) "wuhan"
2) "289.3798"
3) 1) "114.32538002729415894"
2) "30.53453492166421057"
-- 宜昌 距离目标位置316.68公里
4) 1) "yichang"
2) "316.6777"
3) 1) "111.30519658327102661"
2) "30.70812783498269738"
-- 襄阳 距离目标位置432.18公里
5) 1) "xiangyang"
2) "432.1767"
3) 1) "112.16188341379165649"
2) "32.06450528704699821"
GEORADIUSBYMEMBER案例:
在hubeiCities位置集合中查找距离襄阳200km以内的位置信息【这里指定的目标位置只能是hubeiCities中存在的位置,而不能指定位置坐标】,查找结果中应包含不超过2个位置的坐标信息,距离信息,并按距离由远到近排序。
127.0.0.1:6381> georadiusbymember hubeiCities xiangyang 200 km withcoord
withdist desc count 2
-- 枝江 距襄阳186.38km
1) 1) "zhijiang"
2) "186.3784"
3) 1) "111.58371716737747192"
2) "30.46336248623112652"
-- 宜昌 距襄阳171.40km
2) 1) "yichang"
2) "171.3950"
3) 1) "111.30519658327102661"
2) "30.70812783498269738"
四、Redis高级应用&拓展功能
4.1、发布订阅
Redis提供了发布订阅功能,可以用于消息的传输。
Redis的发布订阅机制包括三个部分,publisher
,subscriber
和Channel
发布者和订阅者都是Redis客户端,Channel则为Redis服务器端。
发布者将消息发送到某个的频道,订阅了这个频道的订阅者就能接收到这条消息。
指令详情
- SUBSCRIBE / PSUBSCRIBE : 订阅,精确、或者按匹配符
- UNSUBSCRIBE / PUNSUBSCRIBE :退订,精确、或者按匹配符
- PUBLISH : 发送
- PUBSUB :查看消息列表
频道/模式的订阅与退订
subscribe :订阅 subscribe channel1 channel2 …
Redis客户端1订阅频道1和频道2
127.0.0.1:6379> subscribe ch1 ch2
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "ch1"
3) (integer) 1
1) "subscribe"
2) "ch2"
3) (integer) 2
publish :发布消息 publish channel message
Redis客户端2将消息发布在频道1和频道2上
127.0.0.1:6379> publish ch1 hello
(integer) 1
127.0.0.1:6379> publish ch2 world
(integer) 1
Redis客户端1接收到频道1和频道2的消息
1) "message"
2) "ch1"
3) "hello"
1) "message"
2) "ch2"
3) "world"
unsubscribe :退订 channel
Redis客户端1退订频道1
127.0.0.1:6379> unsubscribe ch1
1) "unsubscribe"
2) "ch1"
3) (integer) 0
psubscribe :模式匹配 psubscribe +模式
Redis客户端1订阅所有以ch开头的频道
127.0.0.1:6379> psubscribe ch*
Reading messages... (press Ctrl-C to quit)
1) "psubscribe"
2) "ch*"
3) (integer) 1
Redis客户端2发布信息在频道5上
127.0.0.1:6379> publish ch5 helloworld
(integer) 1
Redis客户端1收到频道5的信息
1) "pmessage"
2) "ch*"
3) "ch5"
4) "helloworld"
punsubscribe 退订模式
127.0.0.1:6379> punsubscribe ch*
1) "punsubscribe"
2) "ch*"
3) (integer) 0
使用场景
- 在Redis哨兵模式中,哨兵通过发布与订阅的方式与Redis主服务器和Redis从服务器进行通信。
- Redisson是一个分布式锁框架,在Redisson分布式锁释放的时候,是使用发布与订阅的方式通知的。
注:重业务的消息,推荐用消息队列
4.2、事务
所谓事务(Transaction) ,是指作为单个逻辑工作单元执行的一系列操作
ACID回顾
- Atomicity(原子性):构成事务的的所有操作必须是一个逻辑单元,要么全部执行,要么全部不执行。
- Consistency(一致性):数据库在事务执行前后状态都必须是稳定的或者是一致的。
- Isolation(隔离性):事务之间不会相互影响。
- Durability(持久性):事务执行成功后必须全部写入磁盘
Redis事务
Redis 事务的本质是一组命令的集合
- Redis的事务是通过multi、exec、discard和watch这四个命令来完成的。
- Redis的单个命令都是原子性的,所以这里需要确保事务性的对象是命令集合。
- Redis将命令集合序列化并确保处于同一事务的命令集合连续且不被打断的执行
- Redis不能保障失败回滚
注意事项
redis的事务远远弱于mysql,严格意义上,它不能叫做事务,只是一个命令打包的批处理,不能保障失败回滚。
这是官方文档的原话:
It’s important to note that even when a command fails, all the other commands in the queue are processed – Redis will not stop the processing of commands.
原理分析
- 调用multi指令后,redis其实是开启了一个命令队列,后续的命令被提交到队列(还没有执行)
- 期间出现问题了(比如down机),终止操作,队列清空
- 到exec命令后,批量提交,事务完成
操作演示
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set s1 1
QUEUED
127.0.0.1:6379(TX)> get s1
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
2) "1"
关于回滚
注意!回滚要看两种情况:
-
直接语法错误,redis完全无法执行,Redis 2.6.5之前的版本不会回滚,之后版本整个事务回滚
-
执行期的错误,redis不会回滚,其他正确的指令会照样执行
验证:错误的命令,导致回滚(版本:6.0)
#旧value是a
127.0.0.1:9010> set a a
OK
127.0.0.1:9010> get a
"a"
#开启事务
127.0.0.1:9010> multi
OK
#设置成b,语法没问题,进入队列
127.0.0.1:9010> set a b
QUEUED
#语法错误!
127.0.0.1:9010> set a
(error) ERR wrong number of arguments for 'set' command
#提交事务:失败,操作被回滚
127.0.0.1:9010> exec
(error) EXECABORT Transaction discarded because of previous errors.
#最终结果:a没有被修改
127.0.0.1:9010> get a
"a"
验证:命令语法对,但是数据类型不对,执行期间才会被发现!
#旧值a
127.0.0.1:9010> get a
"a"
#开启事务
127.0.0.1:9010> multi
OK
#正确的语法,没毛病!
127.0.0.1:9010> set a b
QUEUED
#语法也对,但是类型肯定是不对的,这不是一个list!
#会进入队列,执行期才会发现这个问题
127.0.0.1:9010> lpush a 1
QUEUED
#提交事务!
#发现正确的1号命令执行ok,2号错误
127.0.0.1:9010> exec
1) OK
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
#最终结果,a被修改,事务没有回滚!
127.0.0.1:9010> get a
"b"
watch
Redis Watch 命令用于监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。
关于上面的操作,如果遇到各种错误,multi可以自动帮你回滚。
而watch命令提供了另一种机制,它通过监控某个key的变动,来决定是不是回滚。
主要应用于高并发的正常业务场景下,处理并发协调。
1、使用语法
watch key
...
multi
...do somethings...
exec
2、操作演练
key无变动时
#初始化,a=a , b=1
127.0.0.1:9010> set balance 80
OK
127.0.0.1:9010> set name zimu
OK
#监控a的变动
127.0.0.1:9010> watch balance
OK
#开启事务,内部对b进行操作
127.0.0.1:9010> multi
OK
127.0.0.1:9010> set name zimulaoshi
QUEUED
127.0.0.1:9010> exec
1) OK
#提交事务后,b正常被改写
127.0.0.1:9010> get name
"zimulaoshi"
如果watch的key发生了变化,注意有意思的事情来了……
#开启两个终端 T1, T2
#T1执行过程与上面一致
#以下是T1的操作过程:
#初始化,a=a , b=1
127.0.0.1:9010> set balance 80
OK
127.0.0.1:9010> set name zimu
OK
#监控a的变动
127.0.0.1:9010> watch balance
OK
#开启事务,内部对b进行操作
127.0.0.1:9010> multi
OK
127.0.0.1:9010> set name zimu
QUEUED
# !!!这一步注意切换到T2:
#在T1的watch和exec之间执行一个 set a 123,a的值被别的终端修改了!!!
#再切回T1,注意!exec得不到ok,得到了一个nil,说明队列被清空了!
127.0.0.1:9010> exec
(nil)
#来查看b的值,没有被改为2,事务回滚了!
127.0.0.1:9010> get b
"1"
3、原理剖析
在exec执行事务的一瞬间,判断监控的key是否变动。
变动则取消事务队列,直接不执行。
无变动则执行,提交事务,参考流程图:
4.3、Lua脚本
lua是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。
Lua应用场景:
- 游戏开发
- 独立应用脚本
- Web应用脚本
- 扩展和数据库插件
- Nginx + lua开发高性能web应用,限流、防止Sql注入…
Redis使用lua脚本
版本:自2.6.0起可用,通过内置的lua编译/解释器,可以使用EVAL命令对lua脚本进行求值。
时间复杂度:取决于执行的脚本。
使用Lua脚本的好处:
- 减少网络开销。可以将多个请求通过脚本的形式一次发送,减少网络时延。
- 原子操作。redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。因此在编写脚本的 过程中无需担心会出现竞态条件,无需使用事务。
- 复用。客户端发送的脚本会永久存在redis中,这样,其他客户端可以复用这一脚本而不需要使用代码完成相同的逻辑。
如何使用 EVAL命令
命令格式:
EVAL script numkeys key [key ...] arg [arg ...]
命令说明:
- script :参数是一段 Lua 5.1 脚本程序。脚本不必(也不应该)定义为一个 Lua 函数
- numkeys : 用于指定键名参数的个数。
- key [key …] ,是要操作的键,可以指定多个,在lua脚本中通过 KEYS[1] , KEYS[2] 获取
- arg [arg …] ,附加参数,在lua脚本中通过 ARGV[1] , ARGV[2] 获取。
实例:
eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
lua脚本中调用Redis命令
-
redis.call():
返回值就是redis命令执行的返回值。
如果出错,则返回错误信息,不继续执行。 -
redis.pcall():
返回值就是redis命令执行的返回值。
如果出错,则记录错误信息,继续执行。 -
注意事项
在脚本中,使用return语句将返回值返回给客户端,如果没有return,则返回nil。eval “return redis.call(‘set’,KEYS[1],ARGV[1])” 1 n1 zhaoyun
命令行里使用
如果直接使用 redis-cli 命令,格式会有点不一样:
redis-cli --eval lua_file key1 key2 , arg1 arg2 arg3
注意的地方:
-
eval 后面参数是lua脚本文件, .lua 后缀
-
不用写 numkeys ,而是使用 , 隔开。注意 , 前后有空格。
示例:
incrbymul.lua
local num = redis.call('GET', KEYS[1]);
if not num then
return 0;
else
local res = num * ARGV[1];
redis.call('SET',KEYS[1], res);
return res;
end
命令行运行:
$ redis-cli --eval incrbymul.lua lua:incrbymul , 8
(integer) 0
$ redis-cli incr lua:incrbymul
(integer) 1
$ redis-cli --eval incrbymul.lua lua:incrbymul , 8
(integer) 8
$ redis-cli --eval incrbymul.lua lua:incrbymul , 8
(integer) 64
$ redis-cli --eval incrbymul.lua lua:incrbymul , 2
(integer) 128
由于redis没有提供命令可以实现将一个数原子性的乘以N倍,这里我们就用Lua脚本实现了,运行过程中确保不会被其它客户端打断。
4.4、慢查询日志
问:日常在使用redis的时候为什么要用慢查询日志?
客户端请求的生命周期的完整生命周期,4个阶段
注意:慢查询只统计步骤3的时间,所以没有慢查询并不代表客户端没有超时问题。换句话说。redis的慢查询记录时间指的是不包括像客户端响应、发送回复等IO操作,而单单是执行一个查询命令所耗费的时间。
第一个问题:
慢查询日志是为了记录执行时间超过给定时长的redis命令请求。
第二个问题:
让使用者更好地监视和找出在业务中一些慢redis操作,找到更好的优化方法。
设置和查看SLOWLOG
慢查询配置相关的参数
-
slowlog-log-slower-than :选项指定执行时间超过多少微秒(默认1秒=1,000,000微秒)的命令请求会被记录到日志上。
例:如果这个选项的值为100,那么执行时间超过100微秒的命令就会被记录到慢查询日志; 如果这个选项的值为500 ,
那么执行时间超过500微秒的命令就会被记录到慢查询日志; -
slowlog-max-len :选项指定服务器最多保存多少条慢查询日志。服务器使用先进先出的方式保存多条慢查询日志: 当服务器储存的慢查询日志数量等于slowlog-max-len选项的值时,服务器在添加一条新的慢查询日志之前,会先将最旧的一条慢查询日志删除。
例:如果服务器slowlog-max-len的值为100,并且假设服务器已经储存了100条慢查询日志,
那么如果服务器打算添加一条新日志的话,它就必须先删除目前保存的最旧的那条日志, 然后再添加新日志。
在Redis中有两种修改配置的方法,一种是修改配置文件,另一种是使用config set命令动态修改;
慢查询配置相关的命令
- config set slowlog-log-slower-than 20000
- config set slowlog-max-len 1024
- showlog get # 查看慢查询日志
慢查询日志的访问和管理
- 获取[n条]慢查询队列 slowlog get [n]
- 获取慢查询队列的当前长度 slowlog len
- 清空慢查询队列 slowlog reset
慢查询日志的使用案例
- 设置慢查询时长: config set slowlog-log-slower-than 0 # 0表示将所有命令都记录为慢查询
- 设置最多保存多少条慢查询日志: config set slowlog-max-len 3
- 获得慢查询日志: slowlog get
慢查询日志的组成由以下六个属性组成:
在生产环境中,慢查询功能可以有效地帮助我们找到Redis可能存在的瓶颈,但在实际使用过程中要注意以下几点:
1、slowlog-max-len:线上建议调大慢查询列表,记录慢查询时Redis会对长命令做阶段操作,并不会占用大量内存.增大慢查询列表可以减缓慢查询被剔除的可能,例如线上可设置为1000以上。
2、slowlog-log-slower-than:默认值超过10毫秒判定为慢查询,需要根据Redis并发量调整该值。
3、慢查询只记录命令的执行时间,并不包括命令排队和网络传输时间,因此客户端执行命令的时间会大于命令的实际执行时间。因为命令执行排队机制,慢查询会导致其他命令级联阻塞,因此客户端出现请求超时时,需要检查该时间点是否有对应的慢查询,从而分析是否为慢查询导致的命令级联阻塞。
4、由于慢查询日志是一个先进先出的队列,也就是说如果慢查询比较多的情况下,可能会丢失部分慢查询命令,为了防止这种情况发生,可以定期执行slowlog
get命令将慢查询日志持久化到其他存储中(例如:MySQL等),然后可以通过可视化工具进行查询。