Redis基本问题及拓展

Redis

1.介绍

redis是用C语言编写的,高性能非关系型的键值对数据库。与传统数据库不同,它是存在于内存中的,读写速度很快,被广泛用于缓存方向。

问题1:为什么使用redis而不用本地缓存?

本地缓存优势是低依赖,比较轻量,且比使用分布式缓存更加简单。但是优缺点:1.本地缓存对分布式架构不友好,数据无法共享
2.容量跟随服务器限制明显

分布式缓存:
1.缓存单独部署在一台服务器上
2.缓存数据共享,其实就一种数据库,单独放在了一个服务器上,然后其他的服务都去这个缓存数据库拿数据(个人理解)。
3.单独分布式缓存服务的性能,容量和功能都比较强大

因此,分布式缓存redis的优点?

1.基于内存操作,读写速度快
2.单线程,避免切换开销和多线程竞争大问题。单线程是指在处理网络请求(一个或多个客户端连接)的时候只有一个线程处理。redis运行不止一个线程,数据持久化或向slave同步会另起线程
3.支持多种数据类型,string,hash,list,set,zset
4.支持持久化,RDB和AOF,避免数据丢失
5.支持事务,redis的操作是原子性的
6.支持主从复制,主节点会自动将数据同步到从节点,可以进行读写分离。

缺点:
1.对join或其他结构化查询的支持比较差
2.数据库容量受到物理内存的限制,不适合做海量数据的高性能读写,适合的场景主要局限在较小数据量的操作。

因此,为什么要使用redis?
从“高性能”:将一些高频且不经常改变的数据存入缓存中。先访问缓存,没有则访问数据库。可以保证用户下一次访问某些数据的时候,能够直接从缓存中取,直接操作内存,速度很快、
从“高并发”:与访问数据库例如Mysql相比,mysql的QPS在1w左右,redis能够达到10w~30w,直接操作缓存能够承受的请求数量远远大于直接访问数据库。

此外:除了用做缓存,还可以来做分布式锁,限流(redis+LUA脚本),消息队列(list结构,支持消息持久化和ack机制),复杂业务场景(排行榜,社交网络中的共同关注等)

问题2: redis为什么快?

1.内存存储,数据在内存中,读写速度快
2.单线程实现(6.0以前),避免线程切换和锁资源的争用。单线程即一个线程处理所有网络请求。
3.IO多路复用:多个socket请求连接,复用一个线程。redis使用epoll模型。将数据库的开关读写都转化为事件。
4.数据结构简单:redis数据结构专门优化过。

为什么采取IO多路复用的模型?
1.redis是单线程的,所有操作按顺序线性执行,但是读写等IO操作是阻塞的,这会导致期间整个进程无法为其他客户提供服务,因此需要采用IO多路复用

IO多路复用的模型:
select:单个进程能够监视的文件描述符存在最大限制,1024(基于数组结构)。并且采用轮询的方式扫描文件描述符,来查询哪些发生了事件。用户空间和内核空间的复制非常消耗资源(需要复制大量的句柄数据结构)
poll:调用过程和select类似,时间复杂度:O(n)
其和select不同的地方:采用链表的方式替换原有fd_set

设想一下如下场景:有100万个客户端同时与一个服务器进程保持着TCP连接。而每一时刻,通常只有几百上千个TCP连接是活跃的(事实上大部分场景都是这种情况)。如何实现这样的高并发?

在select/poll时代,服务器进程每次都把这100万个连接告诉操作系统(从用户态复制句柄数据结构到内核态),让操作系统内核去查询这些套接字上是否有事件发生,轮询完后,再将句柄数据复制到用户态,让服务器应用程序轮询处理已发生的网络事件,这一过程资源消耗较大,因此,select/poll一般只能处理几千的并发连接。

epoll:在Linux内核中申请一个简易的文件系统(B+树),
(1)调用epoll_create()建立一个epoll对象,在文件系统中分配资源:建立eventpoll结构体:红黑树,存储添加到epoll(第二步)中需要监控的事件。双链表存放通过epoll_wait返回给用户的满足条件的事件。
(2)epoll_ctl中向对象添加100万个连接的套接字,所有添加到epoll中的事件都会与设备驱动程序建立回调关系,当发生相应的事件时会调用这个回调方法(ep_poll_callback,将发生的事件添加到双链表中)
(3)调用epoll_wait收集发生的事件的连接,即检查双链表中是否有epitem元素,不为空则将发生的事件复制到用户态。

问题3:redis之前是单线程,为什么又引入多线程?
选择单线程:1.避免过多的上下文切换开销。2.避免同步机制开销(多线程要考虑同步问题)。3.简单可维护(多线程底层也要线程安全).4.即使是单线程模型也能够并发的处理多个客户端的请求(IO多路复用)

(6.0)引入多线程:1.多线程可以分摊redis同步IO读写的负荷,因为IO读写本身是阻塞的(将数据从内核态拷贝到用户态)。redis多线程只是在数据的读写上,内部任然是单线程的。也就是将主线程的IO读写给多个线程去执行,使得多个socket的读写可以并行化。2.可以充分利用cpu资源,单线程模型的主线程只能利用一个cpu。

基本数据结构:
1.string:常规的key-value缓存应用,string类型的值可以是字符串,数字或二进制。
2.hash:存储对象,键值对的集合,string类型的field和value的集合。内部:数组+链表。(用户信息,阅读数,关注数,粉丝数【被关注的,关注的】这样就不用每次去表中进行count统计)
与hashmap的不同是:链表不会转化为红黑树
3.set:无序去重的集合,散列表实现,提供交集并集操作
有序的:
4.list:有序可重复的集合,双链表实现,两端访问快,中间慢
5.sortedset:增加了一个score权重参数,使得集合中的元素能够有序排列,还可以通过score范围来获取元素列表。基于散列表和跳跃表实现,能够调整元素的位置,但是更耗内存。

特殊:
1.bitmap:位图,连续的二进制数字。通过一个bit位来表示某个元素对应的值或状态。场景:用户签到,活跃用户,用户行为。
例如活跃用户:一个key,用户ID为offset,在线则设置为1

问题4:redis事务相关?

开启事务:MULTI
指令插入队列
提交事务:EXEC
WATCH:监视一个或者多个键,一旦其中有一个键被修改则整个事务都不会执行。
当 Redis 使用 exec 命令执行事务的时候,它首先会去比对被 watch 命令所监控的键值对,如果没有发生变化,那么它会执行事务队列中的命令,提交事务;如果发生变化,那么它不会执行任何事务中的命令,而去事务回滚。无论事务是否回滚,Redis 都会去取消执行事务前的 watch 命令。

redis的事务如何实现?

本质是一个数组,较早的入队命令,放到数组前面,较后的放入到队列后面。

问题5:redis的持久化

将内存中的数据写到磁盘上,防止服务宕机内存数据丢失
RDB:根据指定的规则定时将内存数据存储在硬盘上,在指定目录下生成dump.rdb文件,重启时会自动加载
过程:
1.创建一个子进程
2.父进程继续接受请求,子进程将内存中的数据写入临时文件
3.子进程完成所有数据后用临时文件替换旧的RDB文件

触发:
1.手动:用户save(阻塞用户段请求)或者bgsave(异步执行)
2.被动:默认执行shutdown,没有开启AOF会执行执行bgsave
根绝规则进行配置,save 300 10,300秒内至少有10个键被修改则进行快照。
debug reload重新加载redis,会自动触发save

优点:恢复数据快
缺点:不能实时持久化,新旧版本兼容问题

AOF:以独立日志的方式记录每次写的命令,重启时重新执行AOF文件:appendonly yes 开启。
开启AOF方式持久化后每执行一条写命令,Redis就会将该命令写进aof_buf缓冲区,AOF缓冲区根据对应的策略向硬盘做同步操作。

默认情况下系统每30秒会执行一次同步操作。
优点:
1.AOF可以更好的保护数据不丢失,一般AOF会每秒去执行一次fsync操作,如果redis进程挂掉,最多丢失1秒的数据。
2.AOF以appen-only的模式写入,所以没有任何磁盘寻址的开销,写入性能非常高。
缺点:
1.对于同一份文件AOF文件比RDB数据快照要大。
2.不适合写多读少场景。
3.数据恢复比较慢。

如何选择?

通常来说,应该同时使用两种持久化方案,以保证数据安全。

1.如果数据不敏感,且可以从其他地方重新生成,可以关闭持久化。
2.如果数据比较重要,且能够承受几分钟的数据丢失,比如缓存等,只需要使用RDB即可。
3.如果是用做内存数据,要使用Redis的持久化,建议是RDB和AOF都开启。
4.如果只用AOF,优先使用everysec的配置选择,因为它在可靠性和性能之间取了一个平衡。
当RDB与AOF两种方式都开启时,Redis会优先使用AOF恢复数据,因为AOF保存的文件比RDB文件更完整。

redis常见使用和部署方式?

1.主从模式:master 节点挂掉后,需要手动指定新的 master,可用性不高,基本不用。
2.
哨兵模式:master 节点挂掉后,哨兵进程会主动选举新的 master,可用性高,但是每个节点存储的数据是一样的,浪费内存空间。数据量不是很多,集群规模不是很大,需要自动容错容灾的时候使用。
3.Redis cluster:主要是针对海量数据+高并发+高可用的场景,如果是海量数据,如果你的数据量很大,那么建议就用Redis cluster,所有主节点的容量总和就是Redis cluster可缓存的数据容量

redis的主从复制

主数据库可以进行读写操作,当主数据库的数据发生变化时,会自动将数据同步到从数据库。从数据库一般只读,接受同步过来的数据。

当你需要几个redis就建立几个redis文件夹:
从数据库配置salveof来监听主数据库,然后启动主,再启动从

redis-server //启动Redis实例作为主数据库 
redis-server --port 6380 --slaveof  127.0.0.1 6379  //启动另一个实例作为从数据库 
slaveof 127.0.0.1 6379
SLAVEOF NO ONE //停止接收其他数据库的同步并转化为主数据库。

流程
1.保存主节点信息。
2.主从建立socket连接。
3.从节点发送ping命令进行首次通信,主要用于检测网络状态。
4.权限认证。如果主节点设置了requirepass参数,则需要密码认证。从节点必须配置masterauth参数保证与主节点相同的密码才能通过验证。
5.同步数据集。第一次同步的时候,从数据库启动后会向主数据库发送SYNC命令。主数据库接收到命令后开始在后台保存快照(RDB持久化过程),并将保存快照过程接收到的命令缓存起来。当快照完成后,Redis会将快照文件和缓存的命令发送到从数据库。从数据库接收到后,会载入快照文件并执行缓存的命令。以上过程称为复制初始化。
6.复制初始化完成后,主数据库每次收到写命令就会将命令同步给从数据库,从而实现主从数据库数据的一致性。
在这里插入图片描述
在这里插入图片描述

redis在2.8以上的版本中使用psync命令完成主从数据的同步:全量复制(用于初次复制的场景,主节点把全部数据一次性发给从节点)和部分复制(复制过程中出现网络问题造成数据丢失的场景,主节点补发丢失数据给从节点)

哨兵模式

主从复制存在不能自动故障转移,达不到高可用的问题。而哨兵机制可以自动切换主从节点,哨兵是一个独立的进程,用于监控redis实例能否正常运行。

客户端连接redis的时候,先连接哨兵,哨兵会告诉客户端redis主节点的地址,然后客户端连接上redis并进行后续的操作。当主节点宕机的时候,哨兵监测到主节点宕机,会重新推选出某个表现良好的从节点成为新的主节点,然后通过发布订阅模式通知其他的从服务器,让它们切换主机。

工作原理:

1.每个Sentinel以每秒钟一次的频率向它所知的Master,Slave以及其他 Sentinel 实例发送一个 PING 命令。
2.如果一个实例距离最后一次有效回复 PING 命令的时间超过指定的值, 则这个实例会被 Sentinel 标记为主观下线
3.如果一个Master被标记为主观下线,则正在监视这个Master的所有 Sentinel 要以每秒一次的频率确认Master是否真正进入主观下线状态
4.当有足够数量的 Sentinel(大于等于配置文件指定的值)在指定的时间范围内确认Master的确进入了主观下线状态, 则Master会被标记为客观下线 。若没有足够数量的 Sentinel 同意 Master 已经下线, Master 的客观下线状态就会被移除。 若 Master 重新向 Sentinel 的 PING 命令返回有效回复, Master 的主观下线状态就会被移除。

5.哨兵节点会选举出哨兵领导者,负责故障转移的工作。
6.哨兵领导者会推选出某个表现良好的从节点成为新的主节点,然后通知其他从节点更新主节点。

cluster模式

哨兵模式还存在主节点的写能力,容量受单机配置的问题

cluster模式实现了redis的分布式存储,每个节点存储不同的内容

Redis Cluster集群节点最小配置6个节点以上(3主3从),其中主节点提供读写操作,从节点作为备用节点,不提供请求,只作为故障转移使用。

Redis Cluster采用虚拟槽分区,所有的键根据哈希函数映射到0~16383个整数槽内,每个节点负责维护一部分槽以及槽所印映射的键值数据。

分区算法

1.节点取余分区。使用特定的数据,如Redis的键或用户ID,对节点数量N取余:hash(key)%N计算出哈希值,用来决定数据映射到哪一个节点上

优点是简单性。扩容时通常采用翻倍扩容,避免数据映射全部被打乱导致全量迁移的情况。

2.一致性哈希分区。为系统中每个节点分配一个token,范围一般在0~232,这些token构成一个哈希环。数据读写执行节点查找操作时,先根据key计算hash值,然后顺时针找到第一个大于等于该哈希值的token节点。

这种方式相比节点取余最大的好处在于加入和删除节点只影响哈希环中相邻的节点,对其他节点无影响。

3.虚拟槽分区,所有的键根据哈希函数映射到0~16383整数槽内,计算公式:slot=CRC16(key)&16383。每一个节点负责维护一部分槽以及槽所映射的键值数据。Redis Cluser采用虚拟槽分区算法。

优点:
1.无中心架构
2.数据存储在多个节点,节点之间数据共享,可以动态扩展
3.高可用:部分节点不可用时,集群仍可用。实现故障的自动转移,节点之间通过gossip协议交换状态信息

缺点:
1.client实现复杂,异步复制,不能保证数据的强一致性
2.key事务操作仅支持在同一节点上的事务操作
3.不能将很大的键值对象如hash,list映射到不同的节点。

redis的过期键删除

1.主动:定期清理key,每次随机取出一些key,如果过期就删除
2.被动:在访问key时,发现过期则清除
3.内存不够时清理,按照内存淘汰策略

Volatile-lru,ttl,random,lfu:从已设置过期时间的数据集中挑选:最近最少使用,将要过期的,随机挑选,最不经常使用,来进行淘汰
Allkeys-lru,random,lfu:当内存不足时,在建空间中移除:最近最少使用的key,任意选择,最不经常使用

缓存和DB之间的一致性?

redis和数据库的一致性问题:
不管是先写数据库,再删除缓存,还是先删除缓存,再写数据库都会出现数据不一致的情况。
先写数据库,再删除缓存,写数据库宕机,没有删除掉缓存,就会出现不一致的情况。
先删除缓存,没来的写完数据库,另一个线程读取,再去数据库中读取数据写入缓存,此时缓存就为脏数据

因为读和写是并发的就会出现数据不一致的问题。

方法:
1.延时双删:
先删除缓存
写数据库
再删除缓存(评估项目读取数据的业务逻辑耗时,在读数据业务逻辑的耗时基础上,加几百ms,确保读请求结束,写请求可以删除读请求的脏数据)

+缓存超时,给缓存设置过期时间

弊端:双删策略+缓存超时,最差的情况就是在超时时间内数据存在不一致,而且增加了写请求的耗时。

2.异步更新缓存(基于订阅binlog的同步机制):
Mysql binlog增量订阅消费+消息队列+增量数据更新到redis

读redis:热点数据在redis
写mysql:增删改查操作都是mysql
更新redis数据:mysql的数据操作binlog,来更新到redis
原理:
一旦数据库出现写,更新,删除等操作,把binlog相关消息推送到redis,redis读取binlog分析后,以增量的方式对redis进行更新

实现方法:
通过阿里的canal,对mysql的binlog进行订阅。他模仿了mysql的slave数据库的备份请求,使得redis数据库更新达到相同的效果。

三大问题

1.缓存穿透:
方法:
缓存空值,不会查询数据库
布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,查询不存在的数据会被这个bitmap拦截

启动的时候过滤器需要全表扫描,更新数据的时候也需要更新过滤器。
使用:bloomfilter插件

当布隆过滤器说某个值存在时,可能不存在,但是说不存在,则一定不存在

可以使用布隆过滤器解决缓存穿透的问题,把已存在数据的key存在布隆过滤器中。当有新的请求时,先到布隆过滤器中查询是否存在,如果不存在该条数据直接返回;如果存在该条数据再查询缓存查询数据库。
向布隆过滤器询问 key 是否存在时,跟 add 一样,也会把 hash 的几个位置都算出来,看看位数组中这几个位置是否都为 1,只要有一个位为 0,那么说明布隆过滤器中这个 key 不存在。如果都是 1,这并不能说明这个 key 就一定存在,只是极有可能存在,因为这些位被置为 1 可能是因为其它的 key 存在所致。如果这个位数组比较稀疏,判断正确的概率就会很大,如果这个位数组比较拥挤,判断正确的概率就会降低。

命令:bf.add/bf.exists/bf.madd/bf.mexists/bf.reverse[创建filter]

2.缓存雪崩:设置同一过期时间,导致缓存大面积失效
方法:
原有失效时间加上一个随机值

3.缓存击穿:大量请求查询同一个key
方法:
加互斥锁

pipeline的批操作

redis客户端执行一条命令分4个过程: 发送命令、命令排队、命令执行、返回结果。使用Pipeline可以批量请求批量返回结果,执行速度比逐条执行要快。

使用pipeline组装的命令个数不能太多,不然数据量过大,增加客户端的等待时间,还可能造成网络阻塞,可以将大量命令的拆分多个小的pipeline命令完成。

原生批命令(mset, mget)与Pipeline对比

原生批命令是原子性,pipeline是非原子性。pipeline命令中途异常退出,之前执行成功的命令不会回滚。

原生批命令只有一个命令, 但pipeline支持多命令。

LUA

Redis 通过 LUA 脚本创建具有原子性的命令: 当lua脚本命令正在运行的时候,不会有其他脚本或 Redis 命令被执行,实现组合命令的原子操作。

在Redis中执行Lua脚本有两种方法:eval和evalsha。

eval 命令使用内置的 Lua 解释器,对 Lua 脚本进行求值。

Redis的分布式锁

在这里插入图片描述
在分布式系统中,三个用户进来下单,正好三个请求被分到了三个不同的服务节点上面,三个节点 检查剩余库存,发现还有1个,然后都去进行扣减,这样就导致库存为负数,有两个用户没有货发,就是俗称的超卖。
即对共享资源多个请求并发的操作的情况。

通常第一个想法是加synchronize同步代码块,因为获取数量和减库存不是原子性操作,因此加同步代码块执行块中的代码。
但是这通常在单机情况下没有问题,在集群架构中不行。

这就需要分布式锁:
本地锁可以通过语言本身实现,但是要实现分布式锁就需要通过中间件来实现:
redis中的setnx命令,原子操作,只有在key不存在的情况下,才能set成功

方式:
1.redis的setnx命令
2.判断是否拿到锁
3.拿到则执行相应的业务逻辑(获取数量减库存,并将库存重置到Redis中)
4.释放锁资源(保证获取值和删除操作的原子性),LUA脚本保证删除的原子性

在try/finally中把使用完的锁删除,否则一旦抛出异常,一个线程会一直持有锁,而导致其他线程没有机会获取。
当然:为了避免在if(stock>0){代码块中出现宕机而导致一直持有锁,可以给锁上一个超时时间}
并把获取锁+设置超时时间合并为一个原子操作。
至此,在并发量不大的情况下,基本没有问题。

为了避免不同的线程(锁到期被抢走)去删除其他线程的锁,需要解决:自己加的锁生成唯一的id,作为分布式锁的值,在释放锁的时候,判断当前线程的id是否和缓存里id相等。相等则进行删除。

而解决锁超时的问题:给锁续命的操作
在当前主线程获取到锁以后,可以fork出一个线程,执行Timer定时器操作,假如默认超时时间为30秒,那么定时器每隔10秒去看下这把锁还是否存在,存在就说明这个锁里的逻辑还没有执行完,那么就可以把当前主线程的超时时间重新设置为30秒;如果不存在,就直接结束掉。

第二种:高并发场景下:redisson

RLock redissionLock = redisson.getLock(lockKey);
try{
redissonLock.lock();
//获取库存
//减库存
//重新设置到redis中
}finally{
redissonLock.unlock();
}

在lock()方法中,redissLock类中lockInterruptibly方法中的tryAcquire方法,把线程id传入,通过LUA脚本来实现。
当锁不存在,则把线程ID和过期时间(默认30s)存入hashmap。
还支持可重入锁。(通过一个监听器,里面有一个定时任务的轮询,判断ID是否相同来执行续命的操作。)
若其他线程来调用lock方法,则返回锁的剩余过期时间

当再回到lockInterruptibly中当ttl==null,说明加锁成功,否则则不断调用的tryAcquire方法来尝试获取锁。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值