[整理] Redis的一些常见问题总结

一、redis 特点

redis(Remote Dictionary Server ),即全称是:远程字典服务。

  • 所有数据存储在内存中,高速读写
  • 提供丰富多样的数据类型:string、 hash、list、 set、 sorted set、bitmap、hyperloglog
  • 提供了 AOF 和 RDB 两种数据的持久化保存方式,保证了 Redis 重启后数据不丢失
  • Redis 的所有操作都是原子性的,还支持对几个操作合并后的原子性操作,支持事务

通常我们都把数据存到关系型数据库中,但为了提升应用的性能,我们应该把访频率高且不会经常变动的数据缓存到内存中。
Redis 没有像 MySQL 这类关系型数据库那样强大的查询功能,需要考虑如何把关系型数据库中的数据,合理的对应到缓存的 key-value 数据结构中。

二、分段设计法设计 Redis Key

使用冒号把 key 中要表达的多种含义分开表示,步骤如下:

  1. 把表名转化为 key 前缀
  2. 主键名(或其他常用于搜索的字段)
  3. 主键值
  4. 要存储的字段

eg. 用户表(user)

id name email
1 zj 156577812@qq.com
2 ai 156577813@qq.com

这个简单的表可能经常会有这个的需求:>根据用户 id 查询用户邮箱地址,可以选择把邮箱地址这个数据存到 redis 中:

set user:id:1:email 156577812@qq.com;
set user:id:2:email 156577812@qq.com;

三、 String数据类型的应用场景

1. 简介

string 类型是 Redis 中最基本的数据类型,最常用的数据类型,甚至被很多玩家当成 redis 唯一的数据类型去使用。string 类型在 redis 中是二进制安全(binary safe)的,这意味着 string 值关心二进制的字符串,不关心具体格式,你可以用它存储 json 格式或 JPEG 图片格式的字符串。
  

2. 应用场景

(1)存储 MySQL 中某个字段的值

把 key 设计为 表名:主键名:主键值:字段名
eg.

set user:id:1:email 156577812@qq.com

(2)存储对象

string 类型支持任何格式的字符串,应用最多的就是存储 json 或其他对象格式化的字符串。(这种场景下推荐使用 hash 数据类型)

set user:id:1 '[{"id":1,"name":"zj","email":"156577812@qq.com"},{"id":1,"name":"zj","email":"156577812@qq.com"}]'

(3)生成自增 id

当 redis 的 string 类型的值为整数形式时,redis 可以把它当做是整数一样进行自增(incr)自减(decr)操作。由于 redis 所有的操作都是原子性的,所以不必担心多客户端连接时可能出现的事务问题。

四、hash 数据类型的应用场景

1. 简介

hash 类型很像一个关系型数据库的数据表,hash 的 Key 是一个唯一值,Value 部分是一个 hashmap 的结构。
  

2. 数据模型

假设有一张数据库表如下:

id name type
1 redis hash

如果要用 redis 的 hash 结构存储,数据模型如下:

hash数据类型在存储上述类型的数据时具有比 string 类型更灵活、更快的优势,具体的说,使用 string 类型存储,必然需要转换和解析 json 格式的字符串,即便不需要转换,在内存开销方面,还是 hash 占优势。

3. 应用场景

hash 类型十分适合存储对象类数据,相对于在 string 中介绍的把对象转化为 json 字符串存储,hash 的结构可以任意添加或删除‘字段名’,更加高效灵活。

hmset user:1 name zj email 156577812@qq.com

HGETALL user:1
输出:
1) "name"
2) "zj"
3) "email"
4) "156577812@qq.com"

五、list 数据类型的应用场景

1. 简介

list 是按照插入顺序排序的字符串链表,可以在头部和尾部插入新的元素(双向链表实现,两端添加元素的时间复杂度为 O(1))。插入元素时,如果 key 不存在,redis 会为该 key 创建一个新的链表,如果链表中所有的元素都被移除,该 key 也会从 redis 中移除。

2. 数据模型

常见操作时用 lpush 命令在 list 头部插入元素, 用 rpop 命令在 list 尾取出数据。

  

3. 应用场景

(1) 消息队列

redis 的 list 数据类型对于大部分使用者来说,是实现队列服务的最经济,最简单的方式。

LPUSH bkey redis
LPUSH bkey mongodb
LPUSH bkey mysql

LRANGE bkey 0 10
输出:
1) "mysql"
2) "mongodb"
3) "redis"

(2) “最新内容”

因为 list 结构的数据查询两端附近的数据性能非常好,所以适合一些需要获取最新数据的场景,比如新闻类应用的 “最近新闻”。
  

4.优化建议

list 是链表结构,所有如果在头部和尾部插入数据,性能会非常高,不受链表长度的影响;但如果在链表中插入数据,性能就会越来越差。

六、set 数据类型的应用场景

1. 简介

  • set 数据类型是一个集合(没有排序,不重复),可以对 set 类型的数据进行添加、删除、判断是否存在等操作(时间复杂度是 O(1) )
  • set 集合不允许数据重复,如果添加的数据在 set 中已经存在,将只保留一份。
  • set 类型提供了多个 set 之间的聚合运算,如求交集、并集、补集,这些操作在 redis 内部完成,效率很高。

2. 数据模型

3. 应用场景

set 类型的特点是——不重复且无序的一组数据,并且具有丰富的计算功能,在一些特定的场景中可以高效的解决一般关系型数据库不方便做的工作。

共同好友列表

社交类应用中,获取两个人或多个人的共同好友,两个人或多个人共同关注的微博这样类似的功能,用 MySQL 的话操作很复杂,可以把每个人的好友 id 存到集合中,获取共同好友的操作就可以简单到一个取交集的命令就搞定。

// 这里为了方便阅读,把 id 替换成姓名
sadd user:wade james melo paul kobe
sadd user:james wade melo paul kobe
sadd user:paul wade james melo kobe
sadd user:melo wade james paul kobe

// 获取 wade 和 james 的共同好友
sinter user:wade user:james
输出:
1) "kobe"
2) "paul"
3) "melo"

// 获取香蕉四兄弟的共同好友
sinter user:wade user:james user:paul user:melo
输出:
1) "kobe"

类似的需求场景

还有很多,比如:

  • 必须把每个标签下的文章 id 存到集合中,可以很容易的求出几个不同标签下的共同文章;
  • 把每个人的爱好存到集合中,可以很容易的求出几个人的共同爱好。

七、sorted set 数据类型的应用场景

1.简介

在 set 的基础上给集合中每个元素关联了一个分数,往有序集合中插入数据时会自动根据这个分数排序。

2.应用场景

在集合类型的场景上加入排序就是有序集合的应用场景了。比如根据好友的“亲密度”排序显示好友列表。

// 用元素的分数(score)表示与好友的亲密度
zadd user:kobe 80 james 90 wade  85 melo  90 paul

// 根据“亲密度”给好友排序
zrevrange user:kobe 0 -1
输出:
1) "wade"
2) "paul"
3) "melo"
4) "james"
 
// 增加好友的亲密度
zincrby user:kobe 15 james

// 再次根据“亲密度”给好友排序
zrevrange user:kobe 0 -1
输出:
1) "james"
2) "wade"
3) "paul"
2) "melo"
//类似的需求还出现在根据文章的阅读量或点赞量对文章列表排序

八、Redis 的 8 大应用场景

1. 缓存

缓存现在几乎是所有中大型网站都在用的必杀技,合理的利用缓存不仅能够提升网站访问速度,还能大大降低数据库的压力。Redis提供了键过期功能,也提供了灵活的键淘汰策略,所以,现在Redis用在缓存的场合非常多。

2. 排行榜

很多网站都有排行榜应用的,如京东的月度销量榜单. 商品按时间的上新排行榜等。Redis提供的有序集合数据类构能实现各种复杂的排行榜应用。

3. 计数器

什么是计数器,如电商网站商品的浏览量. 视频网站视频的播放数等。为了保证数据实时效,每次浏览都得给+1,并发量高时如果每次都请求数据库操作无疑是种挑战和压力。Redis提供的incr命令来实现计数器功能,内存操作,性能非常好,非常适用于这些计数场景。

4. 分布式会话

集群模式下,在应用不多的情况下一般使用容器自带的session复制功能就能满足,当应用增多相对复杂的系统中,一般都会搭建以Redis等内存数据库为中心的session服务,session不再由容器管理,而是由session服务及内存数据库管理。

5. 分布式锁

在很多互联网公司中都使用了分布式技术,分布式技术带来的技术挑战是对同一个资源的并发访问,如全局ID. 减库存. 秒杀等场景,并发量不大的场景可以使用数据库的悲观锁. 乐观锁来实现,但在并发量高的场合中,利用数据库锁来控制资源的并发访问是不太理想的,大大影响了数据库的性能。可以利用Redis的setnx功能来编写分布式的锁,如果设置返回1说明获取锁成功,否则获取锁失败,实际应用中要考虑的细节要更多。

6. 社交网络

点赞. 踩. 关注/被关注. 共同好友等是社交网站的基本功能,社交网站的访问量通常来说比较大,而且传统的关系数据库类型不适合存储这种类型的数据,Redis提供的哈希. 集合等数据结构能很方便的的实现这些功能。

7. 最新列表

Redis列表结构,LPUSH可以在列表头部插入一个内容ID作为关键字,LTRIM可用来限制列表的数量,这样列表永远为N个ID,无需查询最新的列表,直接根据ID去到对应的内容页即可。

8. 消息系统

消息队列是大型网站必用中间件,如ActiveMQ. RabbitMQ. Kafka等流行的消息队列中间件,主要用于业务解耦. 流量削峰及异步处理实时性低的业务。Redis提供了发布/订阅及阻塞队列功能,能实现一个简单的消息队列系统。另外,这个不能和专业的消息中间件相比。

九、SETNX

可用版本: >= 1.0.0
时间复杂度: O(1)

  • 只在键 key 不存在的情况下, 将键 key 的值设置为 value 。
  • 若键 key 已经存在, 则 SETNX 命令不做任何动作。
  • SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写。

返回值:命令在设置成功时返回 1 , 设置失败时返回 0

redis> EXISTS job                # job 不存在
(integer) 0

redis> SETNX job "programmer"    # job 设置成功
(integer) 1

redis> SETNX job "code-farmer"   # 尝试覆盖 job ,失败
(integer) 0

redis> GET job                   # 没有被覆盖
"programmer"

十、HyperLogLog 使用与应用场景

  • HyperLogLog是一种算法,并非redis独有
  • Redis HyperLogLog是用来做基数统计的算法, 优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。
  • 在Redis里面,每个HyperLogLog键只需要花费12KB内存,就可以计算接近2^64个不同元素的基数。和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。
  • 因为HyperLogLog只会根据输入元素来计算基数,而不会储存输入元素本身,所以HyperLogLog不能像集合那样,返回输入的各个元素。
  • 核心是基数估算算法,主要表现为计算时内存的使用和数据合并的处理。最终数值存在一定误差
  • pfadd命令并不会一次性分配12k内存,而是随着基数的增加而逐渐增加内存分配;而pfmerge操作则会将sourcekey合并后存储在12k大小的key中,由hyperloglog合并操作的原理(两个hyperloglog合并时需要单独比较每个桶的值)可以很容易理解。
  • Redis对HyperLogLog的存储进行优化,在计数比较小时,存储空间采用稀疏矩阵存储,空间占用很小,仅仅在计数慢慢变大,稀疏矩阵占用空间渐渐超过了阈值时才会一次性转变成稠密矩阵,才会占用12k的空间

误差说明:基数估计的结果是一个带有0.81%标准错误(standarderror)的近似值。是可接受的范围
延伸阅读:探索HyperLogLog算法(含Java实现)

pfadd 添加

  • 影响基数估值则返回1否则返回0.若key不存在则创建
  • 时间复杂度O(1)
127.0.0.1:6379> pfadd m1 1 2 3 4 1 2 3 2 2 2 2
(integer) 1

pfcount 获得基数值

  • 得到基数值,叫做去重值(1,1,2,2,3)的插入pfcount得到的是3
  • 可一次统计多个key
  • 时间复杂度为O(N),N为key的个数
  • 返回值是一个带有 0.81% 标准错误(standard error)的近似值.
127.0.0.1:6379> pfadd m1 1 2 3 4 1 2 3 2 2 2 2
(integer) 1
127.0.0.1:6379> pfcount m1
(integer) 4

pfmerge 合并多个key

  • 取多个key的并集
  • 命令只会返回 OK.
  • 时间复杂度为O(N)
127.0.0.1:6379> pfadd m1 1 2 3 4 1 2 3 2 2 2 2
(integer) 1
127.0.0.1:6379> pfcount m1
(integer) 4
127.0.0.1:6379> pfadd m2 3 3 3 4 4 4 5 5 5 6 6 6 1
(integer) 1
127.0.0.1:6379> pfcount m2
(integer) 5
127.0.0.1:6379> pfmerge mergeDes m1 m2
OK
127.0.0.1:6379> pfcount mergeDes
(integer) 6

应用场景

基数不大,数据量不大就用不上,会有点大材小用浪费空间,有局限性,就是只能统计基数数量,而没办法去知道具体的内容是什么,和bitmap相比,属于两种特定统计情况,简单来说,HyperLogLog 去重比 bitmap 方便很多,一般可以bitmap和hyperloglog配合使用,bitmap标识哪些用户活跃,hyperloglog计数

一般使用:

  • 统计注册 IP 数
    +统计每日访问 IP 数
    +统计页面实时 UV 数
    +统计在线用户数
    +统计用户每天搜索不同词条的个数

十一、Redis Big Key问题

数据量大的 key ,由于其数据大小远大于其他key,导致经过分片之后,某个具体存储这个 big key 的实例内存使用量远大于其他实例,造成内存不足,拖累整个集群的使用。

big key 在不同业务上,通常体现为不同的数据,比如:

  • 论坛中的大型持久盖楼活动;
  • 聊天室系统中热门聊天室的消息列表;

1. 带来的问题

bigkey 通常会导致内存空间不平衡,超时阻塞,如果 key 较大,redis 又是单线程,操作 bigkey 比较耗时,那么阻塞 redis 的可能性增大。每次获取 bigKey 的网络流量较大,假设一个 bigkey 为 1MB,每秒访问量为 1000,那么每秒产生 1000MB 的流量,对于普通千兆网卡,按照字节算 128M/S 的服务器来说可能扛不住。而且一般服务器采用单机多实例方式来部署,所以还可能对其他实例造成影响。

如果是集群模式下,无法做到负载均衡,导致请求倾斜到某个实例上,而这个实例的QPS会比较大,内存占用也较多;对于Redis单线程模型又容易出现CPU瓶颈,当内存出现瓶颈时,只能进行纵向库容,使用更牛逼的服务器。

涉及到大key的操作,尤其是使用hgetall、lrange、get、hmget 等操作时,网卡可能会成为瓶颈,也会到导致堵塞其它操作,qps 就有可能出现突降或者突升的情况,趋势上看起来十分不平滑,严重时会导致应用程序连不上,实例或者集群在某些时间段内不可用的状态。

假如这个key需要进行删除操作,如果直接进行DEL 操作,被操作的实例会被Block住,导致无法响应应用的请求,而这个Block的时间会随着key的变大而变长。

2. 什么是 big key

字符串类型:一般认为超过 10k 的就是 bigkey,但是这个值和具体的 OPS 相关。
非字符串类型:体现在哈希,列表,集合类型元素过多。

3. 寻找big key

  • redis-cli自带--bigkeys。
$ redis-cli -p 999 --bigkeys -i 0.1
#Scanning the entire keyspace to find biggest keys as well as average sizes per key type. You can use -i 0.1 to sleep 0.1 sec per 100 SCAN commands (not usually needed).
  • 获取生产Redis的rdb文件,通过rdbtools分析rdb生成csv文件,再导入MySQL或其他数据库中进行分析统计,根据size_in_bytes统计bigkey
$ git clone https://github.com/sripathikrishnan/redis-rdb-tools
$ cd redis-rdb-tools
$ sudo python setup.py install
$ rdb -c memory dump-10030.rdb > memory.csv
  • 通过python脚本,迭代scan key,每次scan 1000,对扫描出来的key进行类型判断,例如:string长度大于10K,list长度大于10240认为是big bigkeys

  • 其他第三方工具,例如:redis-rdb-cli

4. 优化big key

优化big key的原则就是string减少字符串长度,list、hash、set、zset等减少成员数。

  • string类型的big key,建议不要存入redis

用文档型数据库MongoDB代替或者直接缓存到CDN上等方式优化。有些 key 不只是访问量大,数据量也很大,这个时候就要考虑这个 key 使用的场景,存储在redis集群中是否是合理的,是否使用其他组件来存储更合适;如果坚持要用 redis 来存储,可能考虑迁移出集群,采用一主一备(或1主多备)的架构来存储。

  • 单个简单的key存储的value很大

该对象需要每次都整存整取: 可以尝试将对象分拆成几个key-value, 使用multiGet获取值,这样分拆的意义在于分拆单次操作的压力,将操作压力平摊到多个redis实例中,降低对单个redis的IO影响;
该对象每次只需要存取部分数据: 可以像第一种做法一样,分拆成几个key-value,也可以将这个存储在一个hash中,每个field代表一个具体的属性,使用hget,hmget来获取部分的value,使用hset,hmset来更新部分属性。

  • hash, set,zset,list 中存储过多的元素

可以将这些元素分拆。以hash为例,原先的正常存取流程是 hget(hashKey, field) ; hset(hashKey, field, value)
现在,固定一个桶的数量,比如 10000, 每次存取的时候,先在本地计算field的hash值,模除 10000,确定了该field落在哪个key上。

newHashKey  =  hashKey + (hash(field) % 10000);   
hset(newHashKey, field, value) ;  
hget(newHashKey, field)

set, zset, list 也可以类似上述做法。但有些不适合的场景。比如,要保证 lpop 的数据的确是最早push到list中去的,这个就需要一些附加的属性,或者是在 key的拼接上做一些工作(比如list按照时间来分拆)。

十二、Redis 发布订阅

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

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

// 订阅
SUBSCRIBE bbChat

// 发布
PUBLISH bbChat "Redis PUBLISH test"

// 退阅
UNSUBSCRIBE bbChat
posted @ 2020-11-23 12:56  哆啦梦乐园  阅读(447)  评论(5编辑  收藏  举报