简单介绍一下 Redis
简介
使用 C 语言开发的数据库,与传统的数据库不同,Redis 的数据存储在内存中,读写速度非常快,因此,Redis 广泛应用于缓存方向
也经常被用来做分布式锁,甚至是消息队列
Redis 提供多种数据类型来支持不同的业务场景
Redis 还支持事务、持久化、Lua 脚本、多种集群方案
Redis 是一个基于键值对的 NoSQL 数据库
特性
速度快
丰富的功能、丰富的数据结构
简单稳定
客户端语言多
持久化机制
主从复制
高可用和分布式
分布式缓存常见的技术选型
分布式缓存主要解决的是单机缓存的容量受服务器限制并且无法保存通用信息的问题,本地缓存只在当前服务有效,服务之间无法共享
使用的比较多的主要是 Memcached 和 Redis,不过,现在基本没有项目使用 Memcached 来做缓存,都是直接用 Redis
为什么要用缓存
简单来说,使用缓存主要是为了提升用户体验以及应对更多的用户请求
高性能
操作缓存相当于直接操作内存,响应速度非常快
高并发
操作缓存能够承受的请求数量远远大于直接访问数据库
项目中如何使用缓存以及缓存使用不当可能会带来什么问题
缓存与数据库双写不一致
缓存穿透和缓存雪崩
缓存并发竞争
聊聊 Redis 的数据结构
全局哈希表
为了实现从键到值的快速访问,Redis 使用了一个哈希表来保存所有键值对
一个哈希表,其实就是一个数组,数组的每个元素称为一个哈希桶,哈希桶中的元素保存的并不是值本身,而是指向具体值的指针
哈希桶中的 entry 元素中保存了*key和*value指针,分别指向了实际的键和值,这样一来,即使值是一个集合,也可以通过*value指针被查找到
O(1) 的时间复杂度来快速查找到键值对,只需要计算键的哈希值,就可以知道它所对应的哈希桶位置,然后就可以访问相应的 entry 元素
哈希冲突问题
往哈希表中写入更多数据时,哈希冲突是不可避免的问题
Redis 解决哈希冲突的方式,就是链式哈希,很容易理解,就是指同一个哈希桶中的多个元素用一个链表来保存,它们之间依次用指针连接
哈希冲突可能也会越来越多,这就会导致某些哈希冲突链过长,进而导致这个链上的元素查找耗时长,效率降低
对于追求“快”的 Redis 来说,不太能接受
rehash的过程
Redis 默认使用了两个全局哈希表:哈希表 1 和哈希表 2,一开始,刚插入数据时,默认使用哈希表 1,此时的哈希表 2 并没有被分配空间
随着数据越来越多,开始rehash过程
第一步,给哈希表 2 分配更大的空间,例如是当前哈希表 1 大小的两倍
第二步,把哈希表 1 中的数据重新映射并拷贝到哈希表 2 中
第三步,释放哈希表 1 的空间
但是第二步涉及大量的数据拷贝,如果一次性把哈希表 1 中的数据都迁移完,会造成 Redis 线程阻塞,无法服务其他请求
渐进式rehash
巧妙地把一次性大量拷贝的开销,分摊到了多次处理请求的过程中,避免了耗时操作,保证了数据的快速访问
第一步,为 ht[1] 分配空间,Redis 同时持有 ht[0] 和 ht[1] 两个哈希表
第二步,Redis 中维持一个索引计数器变量 rehashidx , 并将它的值设置为 0 , 表示 rehash 工作正式开始
第三步,rehash 进行期间,每次执行添加、删除、查找或者更新操作,会将 ht[0] 哈希表在 rehashidx 索引上的所有键值对 rehash 到 ht[1]
第四步,rehash 工作完成之后, 程序将 rehashidx 属性的值加1
第五步,随着字典操作的不断执行, 最终在某个时间点上, ht[0] 的所有键值对都会被 rehash 至 ht[1]
第六步,程序将 rehashidx 属性的值设为 -1, 表示 rehash 操作全部完成
渐进式 rehash 的过程中,字典会同时使用 ht[0] 和 ht[1] 两个哈希表,渐进式 rehash 期间,字典的删除、查找、更新操作会在两个哈希表进行<br>
要在字典里面查找一个键的话, 程序会先在 ht[0] 里面进行查找,如果没找到的话, 就会继续到 ht[1] 里面进行查找, 诸如此类<br>
渐进式 rehash 的过程中,添加操作只会被保存到 ht[1] 哈希表, 而 ht[0] 则不再进行任何添加操作,最终变成空表<br>
rehash 被触发后,Redis 会按一定频率(例如每 100ms/ 次)执行 rehash 操作,即使没有收到新请求,Redis 也会定时执行一次 rehash 操作<br>
聊聊 Redis 的应用场景
string
常用命令, set,get,strlen,exists,decr,incr,setex 等;
一般常用在需要计数的场景,比如用户的访问次数、热点文章的点赞转发数量等等
list
常用命令,rpush,lpop,lpush,rpop,lrange,llen 等
发布与订阅或者说消息队列、慢查询
hash
常用命令,hset,hmset,hexists,hget,hgetall,hkeys,hvals 等
系统中对象数据的存储
set
常用命令,sadd,spop,smembers,sismember,scard,sinterstore,sunion 等
需要存放的数据不能重复以及需要获取多个数据源交集和并集等场景
sorted list<br>
常用命令,zadd,zcard,zscore,zrange,zrevrange,zrem 等
需要对数据根据某个权重进行排序的场景,比如直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜)等信息
Redis 为什么使用单线程
Redis 单线程指的是 Redis 的网络 IO 和键值对读写是由一个线程来完成,这也是 Redis 对外提供键值存储服务的主要流程
持久化、异步删除、集群数据同步等,其实是由额外的线程执行,所以,严格来说,Redis 并不是单线程
单线程编程容易并且更容易维护<br>
Redis 的性能瓶颈不再是 CPU ,主要在内存和网络<br>
多线程就会存在死锁、线程上下文切换等问题,甚至会影响性能<br>
Redis 的单线程为什么这么快
Redis 的大部分操作在内存上完成
Redis 采用高效的数据结构,例如哈希表和跳表
Redis 采用多路复用机制,使其在网络 IO 操作中能并发处理大量的客户端请求,实现高吞吐率
Redis 6.0 为什么引入多线程
为了提高网络 IO 读写性能
Redis 的多线程只是应用在网络数据的读写这类耗时操作,执行命令仍然是单线程顺序执行,不需要担心线程安全问题