由于购物车信息经常变动,所以我们推荐使用Hash 存储。用户id为key,商品id为Field,商品数量为value
那用户购物车信息的维护具体应该怎么操作呢?
使用sorted set 相关的redis命令 ZRANGE, ZREVRANGE(降序),ZREVRANK(指定元素排名)
SPOP key count
: 随机移除并获取指定集合中一个或多个元素,适合不允许重复中奖的场景。SRANDMEMBER key count
: 随机获取指定集合中指定数量的元素,适合允许重复中奖的场景。对于读写命令来说,redis一直是单线程模型,不过在4.0以后,引入了多线程来执行一些较大的键值对的异步删除操作。6.0后引入了多线程来处理网络请求。
redis基于reactor模式开发了一套高效的事件处理模型,也就是文件处理器,由于文件处理器是单线程的,所以一般说redis是单线程模型。
简单来说,就是不再去主动等待事件的到来,而是提前注册好对应事件的处理方法,当事件到来时,直接调用准备好的处理方法。
主要包含4个部分
虽然redis'是单线程,但是4.0之后加入了多线程支持,主要是大键值对的删除。
原因有三个:
主要是为了提高io读写性能,因为这个算是Redis的性能瓶颈,但是主要就是用在网络数据的读写这种消耗时间的操作上,执行命令仍然是单线程。
多线程默认是禁用的,需要手动再redis.conf文件开启
不建议开启多线程(提升不大)
避免oom问题,在Redis中除了字符串可以使用setex命令设置过期时间外,其他数据类型都要使用 expire key 60 同时使用 persist 命令可以移除过期时间。
除了用于避免oom,过期时间还会用于 1. 验证码时效,2.登录状态时效。
用传统的数据库处理需要自己判断过期时间,十分不变
在redis中,数据通过一个叫做过期字典的东西来存储数据的过期时间,过期字典类似哈希表,键指向Redis中的某个key,而value则是过期时间。
typedef struct redisDb {
...
dict *dict; //数据库键空间,保存着数据库中所有键值对
dict *expires // 过期字典,保存着键的过期时间
...
} redisDb;
1.懒汉式: 只有在加载数据的时候才去判断数据是否过期了。对CPU友好
2.定期删除:每隔固定时间就去判断是否有过期的key需要清除。对内存友好
但是这两者都不能保证一定清除全部的过期key,而redis采用的是二者结合的方式。
解决不能删除干净的办法就是内存淘汰机制
4.0 版本后增加以下两种:
两种,RDB(快照) + AOF(追加文件),redis使用两者混合
Redis可以通过创建快照的方式来获得存储在内存里面的数据在某个时间节点上的副本,redis创建快照后,可以对快照进行备份,将其发送到其他节点上(比如主从架构)。
快照持久化是redis默认开启的配置。
Redis提供过了两个方式来创建快照,
save 同步保存,会阻塞
bgsave : 会fork出一个子进程,不会阻塞主线程,默认选项
目前的主流方案就是AOF,默认关闭,手动开启
开启后,会记录redis执行的每一条指令,类似,MySQL的binlog,在执行每条命令后,都会写入到内存缓存中,然后根据appendfsync配置决定何时将其同步到硬盘上。 fsync想起了mysql的redo log 先读数据进入buffer poll 然后对操作记录到 redo log buffer 然后刷到 page cache中
AOF三种方式
always 每次数据同步都写入AOF,严重降低速度
everysec 每秒写一次(推荐)
no 让操作系统决定什么时候同步
日志记录的执行时机: 在每次命令执行结束之后
原因:
缺点:
AOF太大了以后,会在后台重写AOF,在重写期间会创建一个缓冲区,这个缓冲区会记录在重写AOF期间的全部Redis的写操作,在AOF重写结束后,将其追加到AOF末尾。
如果把混合持久化打开,AOF 重写的时候就直接把 RDB 的内容写到 AOF 文件开头。这样做的好处是可以结合 RDB 和 AOF 的优点, 快速加载同时避免丢失过多的数据。当然缺点也是有的, AOF 里面的 RDB 部分是压缩格式不再是 AOF 格式,可读性较差。
通过MULTI,EXEC, DISCARD,WATCH命令来实现
使用了MULTI后,可以输入多个命令,Redis不会立即执行这些命令,而是将他们放到队列里,执行了EXEC后,在执行所有的命令。同样,可以通过WATCH监控某个变量,如果这个变量在其他的线程中被修改了,则事务执行失败。
# 客户端 1
SET PROJECT "RustGuide"
OK
WATCH PROJECT
OK
MULTI
OK
SET PROJECT "JavaGuide"
QUEUED
# 客户端 2
# 在客户端 1 执行 EXEC 命令提交事务之前修改 PROJECT 的值
SET PROJECT "GoGuide"
# 客户端 1
# 修改失败,因为 PROJECT 的值被客户端2修改了
EXEC
(nil)
GET PROJECT
"GoGuide"
如果 WATCH 与 事务 在同一个 Session 里,并且被 WATCH 监视的 Key 被修改的操作发生在事务内部,这个事务是可以被执行成功的
SET PROJECT "JavaGuide"
OK
WATCH PROJECT
OK
MULTI
OK
SET PROJECT "JavaGuide1"
QUEUED
SET PROJECT "JavaGuide2"
QUEUED
SET PROJECT "JavaGuide3"
QUEUED
EXEC
1) OK
2) OK
3) OK
127.0.0.1:6379> GET PROJECT
"JavaGuide3"
不支持,甚至不支持持久性
Redis在事务执行出错的情况下,不会回滚,仅仅出错的命令不会执行,剩余命令全部执行。并且不支持回滚。
Lua脚本
Redis 从 2.6 版本开始支持执行 Lua 脚本,它的功能和事务非常类似。我们可以利用 Lua 脚本来批量执行多条 Redis 命令,这些 Redis 命令会被提交到 Redis 服务器一次性执行完成,大幅减小了网络开销。
一段 Lua 脚本可以视作一条命令执行,一段 Lua 脚本执行过程中不会有其他脚本或 Redis 命令同时执行,保证了操作不会被其他指令插入或打扰。
不过,如果 Lua 脚本运行时出错并中途结束,出错之后的命令是不会被执行的。并且,出错之前执行的命令是无法被撤销的,无法实现类似关系型数据库执行失败可以回滚的那种原子性效果。因此, 严格来说的话,通过 Lua 脚本来批量执行 Redis 命令实际也是不完全满足原子性的。
如果想要让 Lua 脚本中的命令全部执行,必须保证语句语法和命令都是对的。
另外,Redis 7.0 新增了 Redis functionsopen in new window 特性,你可以将 Redis functions 看作是比 Lua 更强大的脚本。
redis命令可以简化为以下4步
发送命令 命令排队 命令执行 返回结果
rtt为发送命令和返回结果所用的时间。主要是减少这个两个的时间。
mget 获取一个或多个key的值
hmget 获取哈希表中的一个或者多个指定字段的值
sadd 想指定集合添加一个或者多个元素
但是在官方提供的分片集群解决方案下,使用这些原生命令,存在一些问题。
比如mget 无法保证所有的key都在一个哈希槽上,mget可能需要经过多次网络传输才能找到数据,这样原子操作也无法保证。但是相比较非批量操作,还是能节省不少的网络传输次数。
整个步骤的简化版如下(通常由 Redis 客户端实现,无需我们自己再手动实现):
mget
请求获取数据;如果想要解决这个多次网络传输的问题,比较常用的办法是自己维护 key 与 slot 的关系。不过这样不太灵活,虽然带来了性能提升,但同样让系统复杂性提升
pipline可以将一组命令封装成一个包内,将这个包传递给redis,只需要一次网络传输,不过要注意传输的命令的数量,比如在500以下,避免网络传输的数据量过大。
与mget, hmget一样,都会存在一些问题,原因类似,无法保证所有的可以都在一个hash slot上,如果使用,需要自己维护客户端和服务端。
同时pipline不支持顺序执行,比如前一个命令的结果要给后一个命令执行,那么是不能用pipline执行的。
Lua 脚本同样支持批量操作多条命令。一段 Lua 脚本可以视作一条命令执行,可以看作是原子操作。一段 Lua 脚本执行过程中不会有其他脚本或 Redis 命令同时执行,保证了操作不会被其他指令插入或打扰,这是 pipeline 所不具备的。
并且,Lua 脚本中支持一些简单的逻辑处理比如使用命令读取值并在 Lua 脚本中进行处理,这同样是 pipeline 所不具备的。
不过, Redis Cluster 下 Lua 脚本的原子操作也无法保证了,原因同样是无法保证所有的 key 都在同一个 hash slot(哈希槽)上
定期删除执行过程中,如果突然遇到大量过期 key 的话,客户端请求必须等待定期清理过期 key 任务线程执行完成,因为这个这个定期任务线程是在 Redis 主线程中执行的。这就导致客户端请求没办法被及时处理,响应速度会比较慢。
如何解决呢?下面是两种常见的方法:
个人建议不管是否开启 lazy-free,我们都尽量给 key 设置随机过期时间
如果一个key对应的内存比较大,那么这个key就可以看成是一个bigkey, string类型 的 vlaue超过10kb,复合类型的value包含的元素超过5000个
危害
消耗更多的存储空间 + 拖慢运行速度
如何发现?
# 什么是缓存穿透?
缓存穿透说简单点就是大量请求的 key 是不合理的,根本不存在于缓存中,也不存在于数据库中 。这就导致这些请求直接到了数据库上,根本没有经过缓存这一层,对数据库造成了巨大的压力,可能直接就被这么多请求弄宕机了。
举个例子:某个黑客故意制造一些非法的 key 发起大量请求,导致大量请求落到数据库,结果数据库上也没有查到对应的数据。也就是说这些请求最终都落到了数据库上,对数据库造成了巨大的压力。
# 有哪些解决办法?
最基本的就是首先做好参数校验,一些不合法的参数请求直接抛出异常信息返回给客户端。比如查询的数据库 id 不能小于 0、传入的邮箱格式不对的时候直接返回错误消息给客户端等等。
1)缓存无效 key
如果缓存和数据库都查不到某个 key 的数据就写一个到 Redis 中去并设置过期时间,具体命令如下: SET key value EX 10086
。这种方式可以解决请求的 key 变化不频繁的情况,如果黑客恶意攻击,每次构建不同的请求 key,会导致 Redis 中缓存大量无效的 key 。很明显,这种方案并不能从根本上解决此问题。如果非要用这种方式来解决穿透问题的话,尽量将无效的 key 的过期时间设置短一点比如 1 分钟。
2)布隆过滤器
布隆过滤器是一个非常神奇的数据结构,通过它我们可以非常方便地判断一个给定数据是否存在于海量数据中。我们需要的就是判断 key 是否合法,有没有感觉布隆过滤器就是我们想要找的那个“人”。
具体是这样做的:把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会走下面的流程。
加入布隆过滤器之后的缓存处理流程图如下。
但是,需要注意的是布隆过滤器可能会存在误判的情况。总结来说就是: 布隆过滤器说某个元素存在,小概率会误判。布隆过滤器说某个元素不在,那么这个元素一定不在。
为什么会出现误判的情况呢? 我们还要从布隆过滤器的原理来说!
我们先来看一下,当一个元素加入布隆过滤器中的时候,会进行哪些操作:
我们再来看一下,当我们需要判断一个元素是否存在于布隆过滤器的时候,会进行哪些操作:
然后,一定会出现这样一种情况:不同的字符串可能哈希出来的位置相同。 (可以适当增加位数组大小或者调整我们的哈希函数来降低概率)
# 什么是缓存击穿?
缓存击穿中,请求的 key 对应的是 热点数据 ,该数据 存在于数据库中,但不存在于缓存中(通常是因为缓存中的那份数据已经过期) 。这就可能会导致瞬时大量的请求直接打到了数据库上,对数据库造成了巨大的压力,可能直接就被这么多请求弄宕机了。
举个例子 :秒杀进行过程中,缓存中的某个秒杀商品的数据突然过期,这就导致瞬时大量对该商品的请求直接落到数据库上,对数据库造成了巨大的压力。
# 有哪些解决办法?
# 缓存穿透和缓存击穿有什么区别?
缓存穿透中,请求的 key 既不存在于缓存中,也不存在于数据库中。
缓存击穿中,请求的 key 对应的是 热点数据 ,该数据 存在于数据库中,但不存在于缓存中(通常是因为缓存中的那份数据已经过期) 。
# 什么是缓存雪崩?
我发现缓存雪崩这名字起的有点意思,哈哈。
实际上,缓存雪崩描述的就是这样一个简单的场景:缓存在同一时间大面积的失效,导致大量的请求都直接落到了数据库上,对数据库造成了巨大的压力。 这就好比雪崩一样,摧枯拉朽之势,数据库的压力可想而知,可能直接就被这么多请求弄宕机了。
另外,缓存服务宕机也会导致缓存雪崩现象,导致所有的请求都落到了数据库上。
举个例子 :数据库中的大量数据在同一时间过期,这个时候突然有大量的请求需要访问这些过期的数据。这就导致大量的请求直接落到数据库上,对数据库造成了巨大的压力。
# 有哪些解决办法?
针对 Redis 服务不可用的情况:
针对热点缓存失效的情况:
# 缓存雪崩和缓存击穿有什么区别?
缓存雪崩和缓存击穿比较像,但缓存雪崩导致的原因是缓存中的大量或者所有数据失效,缓存击穿导致的原因主要是某个热点数据不存在与缓存中(通常是因为缓存中的那份数据已经过期)。
细说的话可以扯很多,但是我觉得其实没太大必要(小声 BB:很多解决方案我也没太弄明白)。我个人觉得引入缓存之后,如果为了短时间的不一致性问题,选择让系统设计变得更加复杂的话,完全没必要。
下面单独对 Cache Aside Pattern(旁路缓存模式) 来聊聊。
Cache Aside Pattern 中遇到写请求是这样的:更新 DB,然后直接删除 cache 。
如果更新数据库成功,而删除缓存这一步失败的情况的话,简单说两个解决方案: