|
|
51CTO旗下网站
|
|
移动端

读完这篇文章,就基本搞定了Redis数据库

简单来说 Redis 就是一个数据库,不过与传统数据库不同的是 Redis 的数据是存在内存中的,所以存写速度非常快,因此 Redis 被广泛应用于缓存方向。

作者:SnailClimb来源:Java 面试通关手册|2018-09-28 09:32

另外,Redis 也经常用来做分布式锁。Redis 提供了多种数据类型来支持不同的业务场景。

除此之外,Redis 支持事务 、持久化、LUA 脚本、LRU 驱动事件、多种集群方案。

本文将从以下几个方面全面解读 Redis:

  • 为什么要用 Redis / 为什么要用缓存
  • 为什么要用 Redis 而不用 map/guava 做缓存
  • Redis 和 Memcached 的区别
  • Redis 常见数据结构以及使用场景分析
  • Redis 设置过期时间
  • Redis 内存淘汰机制
  • Redis 持久化机制(怎么保证 Redis 挂掉之后再重启数据可以进行恢复)
  • Redis 事务
  • 缓存雪崩和缓存穿透问题解决方案
  • 如何解决 Redis 的并发竞争 Key 问题
  • 如何保证缓存与数据库双写时的数据一致性

为什么要用 Redis / 为什么要用缓存?

主要从“高性能”和“高并发”这两点来看待这个问题。

高性能

假如用户第一次访问数据库中的某些数据。这个过程会比较慢,因为是从硬盘上读取的。

将该用户访问的数据存在缓存中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了。

操作缓存就是直接操作内存,所以速度相当快。如果数据库中的对应数据改变了之后,同步改变缓存中相应的数据即可!

高并发

直接操作缓存能够承受的请求是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。

为什么要用 Redis 而不用 map/guava 做缓存

缓存分为本地缓存和分布式缓存。以 Java 为例,使用自带的 map 或者 guava 实现的是本地缓存,最主要的特点是轻量以及快速,生命周期随着 JVM 的销毁而结束。

并且在多实例的情况下,每个实例都需要各自保存一份缓存,缓存不具有一致性。

使用 Redis 或 Memcached 之类的称为分布式缓存,在多实例的情况下,各实例共用一份缓存数据,缓存具有一致性。

缺点是需要保持 Redis 或 Memcached 服务的高可用,整个程序架构上较为复杂。

Redis 和 Memcached 的区别

现在公司一般都是用 Redis 来实现缓存,而且 Redis 自身也越来越强大了!

对于 Redis 和 Memcached 我总结了下面四点:

  • Redis 支持更丰富的数据类型(支持更复杂的应用场景):Redis 不仅仅支持简单的 K/V 类型的数据,同时还提供 list、set、zset、hash 等数据结构的存储。Memcache 支持简单的数据类型 String。
  • Redis 支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而 Memecache 把数据全部存在内存之中。
  • 集群模式:Memcached 没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;但是 Redis 目前是原生支持 Cluster 模式的。
  • Memcached 是多线程,非阻塞 IO 复用的网络模型;Redis 使用单线程的多路 IO 复用模型。

来自网络上的一张对比图,这里分享给大家:

Redis 常见数据结构以及使用场景分析

String

常用命令:set、get、decr、incr、mget 等。

String 数据结构是简单的 Key-Value 类型,Value 其实不仅可以是 String,也可以是数字。常规 Key-Value 缓存应用;常规计数:微博数,粉丝数等。

Hash

常用命令: hget、hset、hgetall 等。

Hash 是一个 String 类型的 Field 和 Value 的映射表,Hash 特别适合用于存储对象。

后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的值。比如我们可以 Hash 数据结构来存储用户信息,商品信息等等。

比如下面我就用 Hash 类型存放了我本人的一些信息:

  1. key=JavaUser293847 
  2. value={ 
  3.   “id”: 1, 
  4.   “name”: “SnailClimb”, 
  5.   “age”: 22, 
  6.   “location”: “Wuhan, Hubei” 

List

常用命令:lpush、rpush、lpop、rpop、lrange 等。

List 就是链表,Redis List 的应用场景非常多,也是 Redis 最重要的数据结构之一。

比如微博的关注列表,粉丝列表,消息列表等功能都可以用 Redis 的 List 结构来实现。

Redis List 的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。

另外可以通过 lrange 命令,就是从某个元素开始读取多少个元素,可以基于 List 实现分页查询。

这是很棒的一个功能,基于 Redis 实现简单的高性能分页,可以做类似微博那种下拉不断分页的东西(一页一页的往下走),性能高。

Set

常用命令:sadd、spop、smembers、sunion 等。

Set 对外提供的功能与 List 类似是一个列表的功能,特殊之处在于 Set 是可以自动排重的。

当你需要存储一个列表数据,又不希望出现重复数据时,Set 是一个很好的选择。

并且 Set 提供了判断某个成员是否在一个 Set 集合内的重要接口,这个也是 List 所不能提供的。你可以基于 Set 轻易实现交集、并集、差集的操作。

比如:在微博应用中,可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis 可以非常方便的实现如共同关注、共同粉丝、共同喜好等功能。

这个过程也就是求交集的过程,具体命令如下:

  1. sinterstore key1 key2 key3 将交集存在key1内 

Sorted Set

常用命令:zadd、zrange、zrem、zcard 等。

和 Set 相比,Sorted Set 增加了一个权重参数 Score,使得集合中的元素能够按 Score 进行有序排列。

举例:在直播白菜送彩金大全中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,弹幕消息(可以理解为按消息维度的消息排行榜)等信息,适合使用 Redis 中的 Sorted Set 结构进行存储。

Redis 设置过期时间

Redis 中有个设置过期时间的功能,即对存储在 Redis 数据库中的值可以设置一个过期时间。作为一个缓存数据库,这是非常实用的。

如我们一般项目中的 Token 或者一些登录信息,尤其是短信验证码都是有时间限制的,按照传统的数据库处理方式,一般都是自己判断过期,这样无疑会严重影响项目性能。

我们 Set Key 的时候,都可以给一个 Expire Time,就是过期时间,通过过期时间我们可以指定这个 Key 可以存活的时间。

如果你设置了一批 Key 只能存活 1 个小时,那么接下来 1 小时后,Redis 是怎么对这批 Key 进行删除的?

答案是:定期删除+惰性删除。通过名字大概就能猜出这两个删除方式的意思了:

  • 定期删除:Redis 默认是每隔 100ms 就随机抽取一些设置了过期时间的 Key,检查其是否过期,如果过期就删除。

注意这里是随机抽取的。为什么要随机呢?你想一想假如 Redis 存了几十万个 Key ,每隔 100ms 就遍历所有的设置过期时间的 Key 的话,就会给 CPU 带来很大的负载!

  • 惰性删除 :定期删除可能会导致很多过期 Key 到了时间并没有被删除掉。所以就有了惰性删除。

假如你的过期 Key,靠定期删除没有被删除掉,还停留在内存里,除非你的白菜送彩金大全去查一下那个 Key,才会被 Redis 给删除掉。这就是所谓的惰性删除,也是够懒的哈!

但是仅仅通过设置过期时间还是有问题的。我们想一下:如果定期删除漏掉了很多过期 Key,然后你也没及时去查,也就没走惰性删除,此时会怎么样?

如果大量过期 Key 堆积在内存里,导致 Redis 内存块耗尽了。怎么解决这个问题呢?

Redis 内存淘汰机制

MySQL 里有 2000w 数据,Redis 中只存 20w 的数据,如何保证 Redis 中的数据都是热点数据?

Redis 配置文件 redis.conf 中有相关注释,我这里就不贴了,大家可以自行查阅或者通过这个网址查看:http://download.redis.io/redis-stable/redis.conf

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-enviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。这个应该没人使用吧!

Redis 持久化机制

怎么保证 Redis 挂掉之后再重启数据可以进行恢复?很多时候我们需要持久化数据也就是将内存中的数据写入到硬盘里面。

大部分原因是为了之后重用数据(比如重启机器、机器故障之后恢复数据),或者是为了防止白菜送彩金大全故障而将数据备份到一个远程位置。

Redis 不同于 Memcached 的