文章目录
什么是 Redis?
Redis 代表REmote DIctionary Server。它由 Salvatore Sanfilippo 在 2006 年用 C 语言编写。它是一个 NoSQL 高级键值数据存储。Redis 中的读写操作非常快,因为它将所有数据存储在内存中。数据也可以存储在磁盘上,也可以写回内存。
它通常被称为数据结构服务器,因为键可以包含字符串、散列、列表、集合、排序集合、位图和超日志。
NoSQL 意味着not only SQL。NoSQL 数据库不定义像表这样的数据库结构,也不支持查询,比如 SQL SELECT。数据主要存储为 JSON 对象或键值对。
Redis 有什么作用?
由于 Redis 将其数据存储在内存中,因此它最常用作缓存;
访问数据库的效率比较低,在请求与数据库之间加上 redis 缓存,由于缓存里面的数据在内存中,请求在一定程度上避免了和磁盘直接打交道,提升了数据返回的运行效率;
应用场景:
配合关系型数据库做高速缓存
1、高频次,热门数据的访问,降低数据库的 IO;
2、分布式架构,当做 session 共享;
3、多样化的数据结构支持持久化数据;
web 请求访问缓存数据库主数据库正常的流程
缓存穿透、缓存击透以及缓存雪崩
虽然使用缓存可以大大的提升系统的访问效率,但是同时也伴随着其他的问题需要解决,那就是缓存穿透、缓存击穿以及缓冲雪崩的情况是存在的;
存在的问题就是,为了提升访问效率添加的缓存在某些特定的场景之下,缓存失效了(缓存中有需要访问数据缓存才不会失效,其余的都相当于是缓存失效了,虽然片面但是可以这样理解)
,使得整个系统的性能遭受到了一定的影响;
缓存穿透:缓存以及数据库中都没有某一个数据,使得每次的 web 请求都是直接访问到数据库的,缓存失去了原本的提升效率的作用;
缓存击穿:缓存中的某个 key 在某一瞬间失效了,但是数据库还存在这个 key ,突然大量的请求前来,直接访问了数据库,导致缓存失去了原本提升效率的作用;
缓存雪崩:缓存中大量的 key 都失效了,导致请求直接访问到了数据库,使得数据库的压力增大了;
所有的所有所有导致的各种缓存问题,根本原因就是,缓存中没有用户要访问的数据或者,缓存中用户需要的数据过期了
,使得,请求与底层的数据库直接打交道,缓存失去了作用,导致的问题;
缓存穿透
缓存穿透解决方式:
1、对空值缓存:如果一个查询返回的数据为空(不管是数据是否不存在),我们仍然把这个空结果(null)进行缓存,设置空结果的过期时间会很短,最长不超过五分钟,这样可以防止攻击用户反复用同一个id暴力攻击。
2、设置可访问的名单(白名单):
使用bitmaps类型定义一个可以访问的名单,名单id作为bitmaps的偏移量,每次访问和bitmap里面的id进行比较,如果访问id不在bitmaps里面,进行拦截,不允许访问。
3、采用布隆过滤器:布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。将所有可能存在的数据哈希到一个足够大的bitmaps中,一个一定不存在的数据会被 这个bitmaps拦截掉,从而避免了对底层存储系统的查询压力。
4、进行实时监控:当发现Redis的命中率开始急速降低,需要排查访问对象和访问的数据,和运维人员配合,可以设置黑名单限制服务。
缓存击穿
解决办法:
1、预先设置热门数据:在redis高峰访问之前,把一些热门数据提前存入到redis里面,加大这些热门数据key的时长。
2、实时调整:现场监控哪些数据热门,实时调整key的过期时长。
3、加互斥锁:流程如下图所示
缓存雪崩
缓存击穿指在某一极小时间段内,对同一过期数据的大量访问;
(大量请求都访问某一个过期的key)
缓存雪崩指在某一极小时间段内,对很多已过期的不同数据的大量访问;
(大量请求都访问某一批过期的key)
与缓存击穿的图示基本一致,只是同一时间访问的 key 失效的数量的多少不一样而已
缓存中大量的 key 都失效了,导致了缓存雪崩;
Redis 常用命令
与 Redis 有关的命令都是与操作 key 与 value 的;
与 key 相关的命令
|命令 |命令的作用 |
|--|--|
| <key> * | 查看当前库的所有 <key> |
| exist <key> | 判断某个 <key> 是不是存在 |
| type <key> | 判断 <key> 是什么类型 |
| del <key> | 删除某个指定的 <key> |
| unlink <key> | 异步的删除 <key> |
| expire <key> 10 | 为给定的<key>设置过期时间 |
| ttl <key> | 查看 <key> 的状态 -1 表示永不过期 ,-2 表示已经过期了|
| select | 切换数据库 |
| dbsize | 查看数据库的容量 |
| flushdb | 清空当前库 |
| flushall | 清空所有库 |
Redis - 底层的数据结构 - 图解Redis 对象
关于数据结构与数据结构的图示
在查看源代码的时候,在 C 语言中存在结构体,一个结构体就可以看作是一个结构图,多个结构体之间相互嵌套。形成了最终的某种数据结构,从而绘制最终的结构图;
Redis 基本的数据类型有如下几种,在后序的版本中,可能添加新的数据类型,同一个数据类型实现的数据结构也是不一样的,比如 String 类型实现它的数据类型有 embstr、raw、int 三种(这三种实现方式也即是三种不同的encoding)
,Redis 底层根据不同的情况选用不同的数据结构(选择不同的 encoding ,不同的 encoding 为了内存的优化是可以相互转换的)
,减少内存空间的浪费;
Redis 中所有的数据首先组织到不同的编码方式对应的数据结构中,相关的数据结构又被包装在了下面表中的各个对象中;每种对象就是一种 Redis 底层保存数据的一种方式;
Redis - String
String类型是二进制安全的。意味着Redis的string可以包含任何数据。比如jpg图片或者序列化的对象;
什么是二进制安全
当一个数据转换为二进制,并且可以从二进制转换为最原始的数据,原始的数据没有发生变化,那么就是二进制安全的;
String 常用的命令
| set <key> <value> | 给具体的 <key> 设置 <value> |
| get <key> | 查询对应的键值 |
| strlen <key> | 获取键对应的值的长度 |
| setnx <key> <value> | 当键<key>不存在的时候,设置 <value> |
| incr <key> | 将 key 中存储的数值加一;只能对数字值操作,如果为空,新增值为 1 ;|
| decr <key> | 将 key 存储的数字减一 ,只能对数字操作,如果为空新增加的数字为 -1|
| incrby <key> <步长> |将 key 中存储的数字值按照步长进行增加,自定义步长|
| mset <key1><value1><key2><value2> |同时设置一个或多个 key-value 对|
| mget <key1><key2><key3> | 同时获取多个 key 对应的 value |
| msetnx <k1> <value1> <k2> <value2> | 同时设置一个或者多个 key - value 对,要进行赋值 |的键值对是不存在的,存在的话不能设置成功;|
| getrange <key> <起始位置> <结束位置> | 获取起始结束位置的 key 对应的 value
| setex <key> <过期时间> <value> | 设置键值对的过期时间|
| getset <key> <value> | 以新换旧,设置了新值同时获得旧值 |
字符串对象 encoding - embstr raw SDS
底层的 C 的数据结构
struct sdshdr {
// buf 中已占用空间的长度
int len;
// buf 中剩余可用空间的长度
int free;
// 数据空间
char buf[];
};
数据结构使用图示:
由于 redis 底层没有直接使用数据结构存储数据,而是将数据结构放在了 redis 对象中,每个对象里面有自己确定的存储数据的数据结构;
下面展示的是 REDIS_STRING 对象 使用raw 的编码方式进行的数据存储的底层存储数据的数据结构以及其他的相关属性;
底层是 C++ 实现的一种简单动态字符串的数据结构,类似于 Java 的ArrayList ,采用分配冗余空间的形式,减少内存的频繁分配;
capacity 的长度一般长于字符串长度 len ,字符串小于 1 M的时候,扩容加倍现有的空间,超过 1 M,每次扩容只会扩容 1M 的空间;
最大的字符串长度是 512M;
embstr 与 raw 之间的区别:
raw:存储的字符串长度短;
embstr :存储的字符串的长度长
字符串对象 encoding - int
Redis - List
单键多数值;
常用命令
// 在每个命令前面加上 l 表示的是关于 list 的操作
lpush/rpush <key> <value1> <value2> <value3> 从左边/右边插入一个或多个值。
lpop/rpop <key> 从左边/右边吐出一个值。值在键在,值光键亡。
rpoplpush <key1> <key2>从<key1> 列表右边吐出一个值,插到<key2>列表左边。
lrange <key> <start> <stop> 按照索引下标获得元素(从左到右)
lrange mylist 0 -1 0左边第一个,-1右边第一个,(0-1表示获取所有)
lindex <key> <index> 按照索引下标获得元素(从左到右)
llen <key> 获得列表长度
linsert <key> before <value> <newvalue> 在<value>的后面插入<newvalue>插入值
lrem <key> <n> <value> 从左边删除n个value(从左到右)
lset<key> <index> <value> 将列表key下标为index的值替换成value
列表对象 encoding - ziplist
列表对象 encoding - linkedlist
上图中的字符串对象的完整样子:
Redis - Set
与 List 类似的功能,但是Set 是可以自动的去重的,就是存储的元素需要是唯一的;在希望存储数据,但是存储的数据必须唯一的时候,使用 Set 这种数据类型是合适的;
常用命令
// 在每一个命令前面都加上一个 s 表示关于 Set 类型元素的操作
sadd <key> <value1> <value2> ... <key> 中添加多个元素
smembers <key> 取出该集合的所有值。
sismember <key> <value> 判断集合<key>是否为含有该<value>值,有1,没有0
scard<key> 返回该集合的元素个数。
srem <key> <value1> <value2> .... 删除集合中的某个元素。
spop <key> 随机从该集合中吐出一个值。
srandmember <key> <n> 随机从该集合中取出n个值。不会从集合中删除 。
smove <source> <destination> value 把集合中一个值从一个集合移动到另一个集合
sinter <key1> <key2> 返回两个集合的交集元素。
sunion <key1> <key2> 返回两个集合的并集元素。
sdiff <key1> <key2> 返回两个集合的差集元素(key1中的,不包含key2中的)
集合对象 encoding set - intset
集合对象 encoding set - ht(hashtable)
Redis -Hash
常用命令
hset <key> <field> <value> 给<key>集合中的 <field>键赋值<value>
hget <key1> <field> 从<key1>集合<field>取出 value
hmset <key1> <field1> <value1> <field2> <value2>... 批量设置hash的值
hexists<key1> <field> 查看哈希表 key 中,给定域 field 是否存在。
hkeys <key> 列出该hash集合的所有field
hvals <key> 列出该hash集合的所有value
hincrby <key> <field> <increment> 为哈希表 key 中的域 field 的值加上增量 1 -1
hsetnx <key> <field> <value> 将哈希表 key 中的域 field 的值设置为 value ,当且仅当域 field 不存在
哈希对象 encoding - ziplist
哈希对象 encoding - ht(hashtable)
哈希对象的底层是字典;字典的底层是哈希表,所以哈希对象的底层就是哈希表,哈希表中有很多的哈希节点,哈希节点可以存储 key 可以存储 value;
Redis - ZSet (有序集合)
有序集合对象 encoding zset - ziplist
有序集合对象 zset - skiplist
参考
黄健宏 《Redis 设计与实现》 (网络大部分博客参与这个书写的,十分优秀的一本书)