Redis是一个使用C语言编写的、基于内存的且可持久化的key-value数据库。
Redis有以下特点:
1、性能高。 Redis 读数据速度能达到 110000 次/s,写数据速度是 81000 次/s ;而MySQL的读数据速度为5k/s,写数据速度为3k/s。
2、数据结构丰富。Redis支持string、 list,set,zset,hash等数据结构的存储。
3、原子性。Redis的所有操作是支持原子性的,要么成功,要么失败。
4、持久化存储。Redis支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候再次加载进行使用。
Redis使用C语言开发,但C没有提供字符串类型,只能使用指针或数组的形式表示,所以redis设计了一个简单的动态字符串SDS(Simple Dynamic String)作为底层实现。
定义一个SDS结构体,如下:
struct sdshdr{
int len; //buf中已经占有字符串的长度(表示字符串的实际长度)。
int free; //buf中未使用的字符串的长度。
char buf[]; //buf缓冲区。
};
Redis的SDS与C语言字符串的区别:
1、字符串长度计数方式不同。C语言对字符串长度的统计,采用从头遍历到末尾的方式,直到发现空字符,时间复杂度为O(n)。而SDS本身就保存字符串长度的信息,获取长度的时间复杂度是O(1)。
2、缓冲区溢出。C语言不记录字符串长度,在字符串拼接时,如果没有提前计算好内存,会造成内存溢出。而SDS存有free,在字符串拼接时,若长度够就直接执行,若不够就进行扩容,不会出现内存泄露。
3、修改字符串时内存重分配次数不同。C语言每次对字符串拼接都会重新分配内存,开销大;而SDS会分配多余的free空间,避免连续添加字符串带来的内存分配开销。
4、二进制安全性。C语言通过字符串结尾的空字符’\0’来判断字符串的长度,但图片、压缩文件等二进制数据,经常会穿插’\0’在字符串中间,就会长度识别错误。SDS保存了字符串的长度,不会出现这个问题。
应用场景:redis对于key-value的操作效率很高,可以直接用作计数器。
参考链接:Redis为什么快?。
字符串列表,按照插入顺序排序,可以在列表的头部和尾部添加元素。底层实现是一个双向链表,支持反向查找和遍历,但带来了额外的内存开销。删除效率高。
应用场景:如微博的关注列表、粉丝列表。
string类型的无序集合,不允许重复的元素,底层实现是哈希表,添加、删除、查找的时间复杂度均为O(1)。
set与list类似,都可以存储多个字符串,而set通过哈希表来保证存储的string是各不相同的。
应用场景:存储不能重复的元素,如:用户名不能重复。
与set类似,也是string类型的集合,有序的且不允许重复的元素。
底层实现:内部使用HashMap和跳跃表(SkipList)来保证数据的存储和有序,HashMap里放的是成员到score(优先级)的映射,而跳跃表里存放的是所有的成员,排序依据是HashMap里存的score,使用跳跃表的结构可以获得比较高的查找效率,并且在实现上比较简单。
应用场景:范围查找、排行榜等。
key对应的值是一个hashmap,每个hash中存储field及对应的value,其关系如下图所示:
hash的优点就是根据key+field直接操作其中一个value,而不用重复存储全部数据。
应用场景:存储部分变更的数据,如:用户数据等。
参考链接:
redis五种数据类型及其常见操作。
redis的五种数据结构原理分析。
key对应的数据源并不存在,每次针对此key的请求在缓存中获取不到,请求都会打数据源,这样的请求过多就会压垮数据库。
解决方案:采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据就会被bitmap拦截掉,从而减小了对底层存储系统的查询压力。
key对应的数据源存在,但在redis中过期,若有针对该key的大量请求并发过来,就会从后端DB加载数据并回设到缓存,从而造成数据库压力过大而崩溃。
解决方案:
①设置key永不过期。最安全可靠,但会一直占有内存且无法获取最新数据。
②加互斥锁。在读取数据为NULL的时候,将尝试获取锁,若成功就重新从数据库加载数据并回设缓存,若失败就睡眠一定时间(保证数据回设缓存已经完成)重新从缓存中获取数据。代码参考:缓存击穿解决代码。
当缓存服务器重启或大量缓存集中在某一个时间段失效,这样在失效的时候也会给数据库带来很大的压力。缓存雪崩是针对很多个key,而缓存击穿是针对某一个key来说的。
解决方案:
①将缓存过期时间设置为一个随机值,避免缓存在同一时间过期;
②使用双缓存策略,设置原始缓存和备用缓存,原缓存失效时,访问备用缓存,备用缓存的失效时间设置得长一些。直到原始缓存在后台更新完成后,才会返回新缓存。
redis是内存数据库,一旦重启后所有数据都会丢失,因此,需要将redis中的数据做持久化存储到磁盘。redis的持久化存储有以下两种方式:
按照一定的时间周期执行快照,将内存中的数据保存到硬盘中。RDB是redis默认的持久化方式。
RDB的优点:
①适合冷备份。RDB是按时间周期存储,可以快速恢复之前某个时间段的数据。
②性能最大化。在同步数据的时候fork一个子进程去持久化,主进程继续处理命令。
③RDB数据恢复时的速度比AOF更高。
缺点:
①数据安全性低。RDB是间隔一段时间做持久化,如果这段时间redis发生故障,会发生数据丢失。
②RDB在生成数据快照时,若文件很大,客户端会暂停几毫米甚至几秒,若此时正在进行秒杀操作,性能会受到影响。
将redis执行的每次写命令记录到单独的日志文件中,当redis重启时会从持久化的日志文件中恢复数据。
AOF的优点:
①数据安全性高。AOF每进行一次命令操作就会异步将数据记录到AOF中一次,若数据丢失,也只会丢失这一次的数据。
②rewrite模式。如用flushall命令误操作清空了所有数据,在后台没开始重写AOF文件之前,可以拷贝AOF日志文件,并删除其中的flushall命令。
缺点:
①同样的数据,AOF文件比RDB文件更大,恢复速度更慢;
若单独使用RDB,可能会丢失很多数据;若单独使用AOF,数据恢复没有RDB快。因此,Redis4.0之后提供了混合持久化方式,也就是:采用RDB快照以一定频率执行保存数据,而在两次快照之间,使用AOF日志记录这期间的所有命令操作。
①如果你的业务场景需要很高的性能,或者宕机之后能够尽快的恢复,而对数据完整性的要求不是那么高,那么可以采用RDB持久化的方式。
②如果你的业务场景对数据完整性的要求很高,那么可以采用AOF持久化方式,而至于采用哪种回写策略,则取决于你对数据完整性的要求程度。
③如果你的业务场景既要兼顾性能,又注重数据完整性,那么可以采用混合持久化的方式。
④如果你对数据丢失无所谓,追求性能最大化的情况下,甚至可以禁用持久化。
此处参考:如何选择合适的持久化方式?。
Redis是key-value型数据库,可以设置key的过期时间。在Redis中的过期策略有以下三种:
①定时删除: 每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除。
优点:保证内存被尽快释放;
缺点:若过期key很多,会需要占用大量CPU资源去处理过期数据,从而影响读取缓存的响应时间。
②惰性删除: 只有当访问这个key的时候,才会判断key是否过期,过期就删除。
优点:节省CPU资源;
缺点:若出现大量过期的key没有被访问,会占用大量内存;
③定期删除: 每隔一段时间,扫描一定数量设置了过期时间的key,并清除其中过期的key。
定期删除的性能介于定时删除和惰性删除之间。在内存占用方面,定期删除不如定时删除好;在CPU占用方面,定期删除不如惰性删除好。
为啥不扫描全部设置了过期时间的key呢?
假如Redis里面所有的key都有过期时间,都扫描一遍?那太恐怖了,而且我们线上基本上也都是会设置一定的过期时间的。全扫描跟你去查数据库不带where条件不走索引全表扫描一样,100s一次,Redis累都累死了。
Redis采用的key过期策略:惰性删除+定期删除。
redis内存数据集大小上升到一定大小的时候,导致用于缓存的内存不足,就会施行数据淘汰策略。惰性删除+定期删除可能会残留大量的过期key,从而导致内存数据太大。
全局的键空间选择性移除
noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。
allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。(这个是最常用的)
allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。
设置过期时间的键空间选择性移除
volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。
volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。
volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。