Redis 的数据类型及使用
什么是 Redis
Redis 本质上是一个 Key-Value 类型的内存数据库。因为是纯内存操作,Redis 的性能非常出色,每秒可以处理超过 10 万次读写操作。Redis 还有一个优势就是是支持保存多种数据结构,例如 String、List、Set、Sorted Set、hashes 等。
常见数据结构以及使用场景分析
String
常用命令: set, get, decr, incr, incrby, decrby, mget 等。
String 数据结构是简单的 key-value 类型,value 可以是 String,也可以是数字。 除了常规 key-value 缓存应用;decr incr 可以实现原子性的递增递减,可应用于高并发的秒杀活动、访问量统计、分布式序列号生成等场景。例如短信验证码服务就可以使用它来计数实现一分钟内只发送一次。
Redis Incr 命令将 key 中储存的数字值增一。如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCR 操作。如果值包含错误的类型,或字符串类型的值不能表示为数字,那么返回一个错误。本操作的值限制在 64 位(bit)有符号数字表示之内。
hash
常用命令: hget, hset, hgetall 等。
hash 是一个 string 类型的 field 和 value 的映射表,可以理解为 <key,map>
这样的结构。特别适合用于存储对象,后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的值。 比如我们可以 hash 数据结构来存储用户会话信息,商品信息等等。
list
常用命令: lpush, rpush, lpop, rpop, lrange 等
Redis list 的实现为一个双向链表,即可以支持反向查找和遍历。是Redis最重要的数据结构之一,应用场景非常多,比如微博的关注列表、粉丝列表、消息列表等功能都可以用 Redis 的 list 结构来实现。
另外可以通过 lrange 命令实现分页查询,类似微博那种下拉不断分页的效果,商品的评价列表也是类似的实现方式。
set
常用命令: sadd, spop, smembers, sunion 等
功能与 list 类似,也是一个列表的结构,特殊之处在于 set 是可以自动排重的。底层的数据结构查资料说是 intset(内部是一个数组) 和 hashtable 两种数据结构存储的(后续有待研究)。set 提供了判断是否包含某个元素的接口,这是 list 所不能提供的。可以基于 set 轻易实现交集、并集、差集的操作。
例如微博应用中,可以将一个用户关注人存在一个集合中,将其粉丝存在一个集合。通过求交集可以非常方便的实现如共同关注、共同粉丝、共同喜好等功能。
Sorted Set
常用命令: zadd, zrange, zrem, zcard 等
和 set 相比,sorted set 增加了一个权重参数 score,使得集合中的元素能够按 score 进行有序排列。非常适合各种实时的排行榜,例如商品评价中的推荐评价列表,并且同时还支持分页查询。
Redis Key 过期时间
Redis 支持对存储在数据库中的值可以设置一个过期时间。作为一个缓存数据库,这是非常实用的。如我们一般项目中的 token 或者一些登录信息,尤其是短信验证码都是有时间限制的,按照传统的数据库处理方式,一般都是自己判断过期,这样无疑会严重影响项目性能。
那么问题来了,Redis 是怎么删除过期 key 的呢?
- 定期删除:redis 默认是每隔 100ms 就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就删除。注意这里是随机抽取的。为什么要随机呢?你想一想假如 redis 存了几十万个 key ,每隔 100ms 就遍历所有的设置过期时间的 key 的话,就会给 CPU 带来很大的负载!
- 惰性删除 :定期删除可能会导致很多过期 key 到了时间并没有被删除掉。所以就有了惰性删除。假如你的过期 key,靠定期删除没有被删除掉,还停留在内存里,除非你的系统去查一下那个 key,才会被redis给删除掉。这就是所谓的惰性删除!
又有一个问题来了,如果定期删除漏掉了很多过期 key,然后你也没及时去查,也就没走惰性删除,此时会怎么样?如果大量过期 key 堆积在内存里,导致 redis 内存块耗尽了。怎么解决这个问题呢?答案是 redis 内存淘汰机制。
Redis 数据淘汰策略
Redis 提供 6 种数据淘汰策略
- volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
- volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
- volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
- allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key(这个是最常用的)
- allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
- no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。这个应该没人使用吧!
Redis 持久化机制
Redis 支持持久化,而且支持两种不同的持久化操作。一种持久化方式叫快照(snapshotting,RDB),另一种方式是只追加文件(append-only file, AOF)。
快照(snapshotting)持久化(RDB)
Redis 可以通过创建快照来获得存储在内存里面的数据在某个时间点上的副本。Redis创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本,还可以将快照留在原地以便重启服务器的时候使用。
快照持久化是 Redis 默认采用的持久化方式,在 redis.conf 配置文件中默认有此下配置:
save 900 1 #在900秒(15分钟)之后,如果至少有1个key发生变化,Redis就会自动触发BGSAVE命令创建快照。
save 300 10 #在300秒(5分钟)之后,如果至少有10个key发生变化,Redis就会自动触发BGSAVE命令创建快照。
save 60 10000 #在60秒(1分钟)之后,如果至少有10000个key发生变化,Redis就会自动触发BGSAVE命令创建快照。
AOF(append-only file)持久化
与快照持久化相比,AOF持久化 的实时性更好,因此已成为主流的持久化方案。默认情况下Redis没有开启AOF(append only file)方式的持久化,可以通过appendonly参数开启:
appendonly yes
开启 AOF 持久化后每执行一条会更改 Redis 中的数据的命令,Redis 就会将该命令写入硬盘中的 AOF 文件。AOF 文件的保存位置和 RDB 文件的位置相同,都是通过 dir 参数设置的,默认的文件名是 appendonly.aof。
在 Redis 的配置文件中存在一下三种不同的 AOF 持久化方式:
appendfsync always #每次有数据修改发生时都会写入 AOF 文件,这样会严重降低 Redis 的速度
appendfsync everysec #每秒钟同步一次,显示地将多个写命令同步到硬盘
appendfsync no #让操作系统决定何时进行同步
一般选用第二种策略即每秒同步一次 AOF 文件,Redis 性能几乎没受到任何影响。而且这样即使出现系统崩溃,用户最多只会丢失一秒之内产生的数据。而在 Redis 4.0 开始支持 RDB 和 AOF 的混合持久化。这样做的好处是可以结合 RDB 和 AOF 的优点, 快速加载同时避免丢失过多的数据。
Redis 事务
Redis 通过 MULTI、EXEC、WATCH 等命令来实现事务(transaction)功能。事务提供了一种将多个命令请求打包,然后一次性、按顺序地执行多个命令的机制,并且在事务执行期间,服务器不会中断事务而改去执行其他客户端的命令请求,它会将事务中的所有命令都执行完毕,然后才去处理其他客户端的命令请求。
在传统的关系式数据库中,常常用 ACID 性质来检验事务功能的可靠性和安全性。在 Redis 中,事务总是具有原子性(Atomicity)、一致性(Consistency)和隔离性(Isolation),并且当 Redis 运行在某种特定的持久化模式下时,事务也具有持久性(Durability)。
缓存雪崩 & 缓存穿透 & 缓存预热
缓存雪崩
产生原因:缓存同一时间大面积的失效,导致后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。
解决方法:
- 设置缓存超时时间的时候加上一个随机的时间长度,可一定程度上避免雪崩问题;
- 添加本地缓存实现多级缓存
缓存穿透
产生原因:恶意用户模拟请求很多缓存中不存在的数据,由于缓存中都没有,导致这些请求短时间内直接落在了数据库上,导致数据库异常。
解决方法:
- 采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的 bitmap 中,过滤掉一定不存在的数据从而避免了对底层存储系统的查询压力。
- 对空值也进行缓存,过期时间设置的短一些
缓存预热
缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!
解决思路:
- 数据量小的情况下启动时即可启动刷新
- 定时异步刷新
使用 Redis 实现秒杀系统方案
秒杀系统的特点是瞬时流量非常高,有大量的非法脚本请求,针对正常流量实现上一般是通过操作缓存、异步处理来应对。这些都可以通过 Redis 来实现。
使用 list 结构通过 RPUSH key value 插入秒杀请求,当插入的秒杀请求数达到上限时,后续请求直接返回秒杀失败。后台启动多个工作线程,使用 LPOP key (或者 LRANGE key start end)读取秒杀成功者的用户 id,进行后续下单处理。每完成一条秒杀记录的处理,就执行 INCR key_num。一旦所有库存处理完毕,就结束该商品的本次秒杀,关闭工作线程,也不再接收秒杀请求。
当然还有一些其他的注意事项,例如前端抢购按钮做防重复点击处理、对脚本请求做 ip 限制等等,这些不在这里的讨论范围。