Redis
1. Redis是什么?都有哪些应用场景?
NoSQL(NoSQL = Not Only SQL ),意即“不仅仅是SQL”, 泛指非关系型的数据库。随着互联网web2.0网站的兴起,传统的关系数据库在应付web2.0网站,特别是超大规模和高并发的SNS类型的web2.0纯动态网站已经显得力不从心,暴露了很多难以克服的问题,而非关系型的数据库则由于其本身的特点得到了非常迅速的发展。NoSQL数据库的产生就是为了解决大规模数据集合多重数据种类带来的挑战,尤其是大数据应用难题,包括超大规模数据的存储。 (例如谷歌或Facebook每天为他们的用户收集万亿比特的数据)。这些类型的数据存储不需要固定的模式,无需多余操作就可以横向扩展。
Redis: Remote Dictionary Server(远程字典服务)是完全开源免费的,用C语言编写,遵守BSD协议,是一个高性能(Key/Value)分布式内存数据库,基于内存运行并支持持久化的NoSQL数据库,是当前最热门的NoSQL数据之一,也被人们称为数据结构服务器。
使用场景
- 热点数据的缓存
- 现实业务的运用:Redis可以使用expire命令设置一个键的生存时间,到时间后redis会删除它,利用这一特性可以运用在限时的优惠活动信息、手机验证的业务场景
- 计数器相关问 题: Redis使用incrby命令可以实现原 子性递增,可以运用高并发的秒杀活动、分布式序号的生成、具体业务还体现在不如限制一个手机发多少跳短信、一个接口一分钟限制多少请求、一个接口一天限制调用多少次等等
- 排行榜相关问题
- 分布式锁
- 延时操作
- 分页、模糊搜索
- 点赞、好友等相互关系的存储
- 队列
2. Redis有哪些功能?
- 基于本机内存的缓存:减少短期内对数据库的反复检索
- 服务端的Redis: 在API服务器的内存都被缓存塞满的时候,可以把换成直接丢到一个专门的服务器上
- 持久化(Persistence)
- 哨兵(Sentinel)和复制(Replication):Redis通过这两个功能保证高可用
- 集群(Cluster):单台服务器的资源总是有上限的,CPU资源和IO资源我们可以通过主从复制,进行读写分离,把一部分的CPU和IO的压力转移到从服务器上。但是内存资源怎么办,主从模式做到的只是相同数据的备份,并不能横向扩充内存;单台机器的内存也只能进行加大处理,但是总有上限的。所以我们就需要一种解决方案,可以让我们横向扩展。最终的目的既是把每台服务器只负责其中的一部分,让这些所有的服务器构成一个整体,对外界的消费者而言,这一组分布式的服务器就像是一个集中式的服务器一样
3. Redis和Memcache有什么区别?
- Redis和Memcache都是将数据存放在内存中,都是内存数据库。不过Memcache还可用于缓存其他东西,例如图片、视频等等。
- Redis不仅仅支持简单的K/V类型的数据,同时还提供list,set,hash等数据结构的存储。
- 虚拟内存 :Redis当物理内存用完时,可以将一些很久没用到的value 交换到磁盘
- 过期策略 : Memcache在set时就指定,例如set key1 0 0 8,即永不过期。Redis可以通过例如expire 设定,例如expire name 10
- 分布式: 设定Memcache集群,利用Magent做一主多从;Redis可以做一主多从。都可以一主一从
- 存储数据安全: Memcache挂掉后,数据没了;Redis可以定期保存到磁盘(持久化)
- 灾难恢复 :Memcache挂掉后,数据不可恢复; Redis数据丢失后可以通过AOF恢复
- Redis支持数据的备份,即master-slave模式的数据备份。
4. Redis为什么是单线程的?
官方解释
因为Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了。
性能指标
关于Redis性能,官方给出来,普通笔记本轻松处理几十万的请求你。
具体原因
-
不需要各种锁的性能消化
Redis的数据结构并不全是简单的Key-Value,还有list,hash等复杂的结构,这些结构有可能会进行很细粒度的操作,比如在很长的列表后面添加一个元素,在hash当中添加或者删除一个对象。这些操作可能就需要加非常多的锁,导致的结果是同步开销大大增加。
总之,在单线程的情况下,就不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗。
-
单线程多进程集群方案
单线程的威力实际上非常强大,每核心效率也非常高,多线程自然是可以比单线程有更高的性能上限,但是在今天的计算环境中,即使是单机多线程的上限也往往不能满足需要了,需要进一步摸索的是多服务器集群化的方案,这些方案中多线程的技术照样是用不上的。
-
CPU消耗
采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU。
但是如果CPU成为Redis瓶颈,或者不想让服务器其他CUP核闲置,那怎么办?可以考虑多起几个Redis进程,Redis是Key-Value数据库,不是关系数据库,数据之间没有约束。只要客户端分清哪些Key放在哪个Redis进程上就可以了。
Redis单线程的优劣势
- 优势
- 代码更清晰,处理逻辑更简单
- 不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗
- 不存在多进程或者多线程导致的切换而消耗CPU
- 劣势
- 无法发挥多核CPU性能,不过可以通过在单机开多个Redis实例来完善;
IO多路复用技术
Redis 采用网络IO多路复用技术来保证在多连接的时候, 系统的高吞吐量。
多路-指的是多个socket连接,复用-指的是复用一个线程。多路复用主要有三种技术:select,poll,epoll。epoll是最新的也是目前最好的多路复用技术。
这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗),且Redis在内存中操作数据的速度非常快(内存内的操作不会成为这里的性能瓶颈),主要以上两点造就了Redis具有很高的吞吐量。
Redis高并发快总结
- Redis是纯内存数据库,一般都是简单的存取操作,线程占用的时间很多,时间的花费主要集中在IO上,所以读取速度快。
- 再说一下IO,Redis使用的是非阻塞IO,IO多路复用,使用了单线程来轮询描述符,将数据库的开、关、读、写都转换成了事件,减少了线程切换时上下文的切换和竞争。
- Redis采用了单线程的模型,保证了每个操作的原子性,也减少了线程的上下文切换和竞争。
- 另外,数据结构也帮了不少忙,Redis全程使用hash结构,读取速度快,还有一些特殊的数据结构,对数据存储进行了优化,如压缩表,对短数据进行压缩存储,再如,跳表,使用有序的数据结构加快读取的速度。
- 还有一点,Redis采用自己实现的事件分离器,效率比较高,内部采用非阻塞的执行方式,吞吐能力比较大。
5. 什么是缓存穿透?怎么解决?
定义
一般的缓存系统,都是按照key去缓存查询,如果不存在对应的value,就应该去数据库查询,一些恶意的请求会故意大量的查询不存在的key,就会对数据库造成很大的压力,这就叫做缓存穿透。
避免
-
采用布隆过滤器,将所有可能存在的数据存到一个bitMap中,不存在的数据就会进行拦截
-
对查询结果为空的情况也进行缓存,缓存时间设置短一点,不操作五分钟
6. Redis支持哪些数据类型?
String字符串:
格式: set key value
string类型是二进制安全的。意思是redis的string可以包含任何数据。比如jpg图片或者序列化的对象 。
string类型是Redis最基本的数据类型,一个键最大能存储512MB。
Hash(哈希)
格式: hmset name key1 value1 key2 value2
Redis hash 是一个键值(key=>value)对集合。
Redis hash是一个string类型的field和value的映射表,hash特别适合用于存储对象。
List(列表)
Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)
格式: lpush name value
在 key 对应 list 的头部添加字符串元素
格式: rpush name value
在 key 对应 list 的尾部添加字符串元素
格式: lrem name index
key 对应 list 中删除 count 个和 value 相同的元素
格式: llen name
返回 key 对应 list 的长度
Set(集合)
格式: sadd name value
Redis的Set是string类型的无序集合。
集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。
zset(sorted set:有序集合)
格式: zadd name score value
Redis zset 和 set 一样也是string类型元素的集合,且不允许重复的成员。
不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。
zset的成员是唯一的,但分数(score)却可以重复。
7. Redis支持的Java客户端都有哪些?
Redis的Java客户端很多,官方推荐的有三种:Jedis、Redisson和lettuce。
在这里对Jedis和Redisson进行对比介绍
Jedis:
- 轻量,简洁,便于集成和改造
- 支持连接池
- 支持pipelining、事务、LUA Scripting、Redis Sentinel、Redis Cluster
- 不支持读写分离,需要自己实现
- 文档差(真的很差,几乎没有……)
Redisson:
- 基于Netty实现,采用非阻塞IO,性能高
- 支持异步请求
- 支持连接池
- 支持pipelining、LUA Scripting、Redis Sentinel、Redis Cluster
- 不支持事务,官方建议以LUA Scripting代替事务
- 支持在Redis Cluster架构下使用pipelining
- 支持读写分离,支持读负载均衡,在主从复制和Redis Cluster架构下都可以使用
- 内建Tomcat Session Manager,为Tomcat 6/7/8提供了会话共享功能
- 可以与Spring Session集成,实现基于Redis的会话共享
- 文档较丰富,有中文文档
对于Jedis和Redisson的选择,同样应遵循前述的原理,尽管Jedis比起Redisson有各种各样的不足,但也应该在需要使用Redisson的高级特性时再选用Redisson,避免造成不必要的程序复杂度提升。
8. 怎么保证缓存和数据库数据的一致性?
- 淘汰还是更新缓存?
选择淘汰缓存,原因是数据可能为简单数据,也可能为复杂数据,复杂数据进行缓存的更新,成本会比较高,因此一般推荐淘汰缓存
- 先淘汰缓存还是先更新数据库?
选择先淘汰缓存,再更新数据库,原因是,假如先更新数据库,再淘汰缓存,假如缓存缓存失败,那么后面的请求都会得到脏数据,直至缓存过期;假如先淘汰缓存再更新数据库,假如数据库更新失败,只会产生一次缓存miss,相比较而言,后者对于业务的影响更小一点。
-
延时双删策略
如下场景:同时有一个请求A进行更新操作,另外一个请求B进行查询操作
1)请求A进行写操作,删除缓存
2)请求B进行查询,发现缓存不存在
3)请求B去数据库查询得到旧的值
4)请求B将旧的值写入缓存
5) 请求A将新值写入数据库
这种情况下,会出现数据不一致问题,采用双删策略可以解决
public void write(String key,Object data){
redisUtils.del(key);
db.update(data);
Thread.Sleep(100);
redisUtils.del(key);
}
这么做,可以将1秒杀内所造成的缓存脏数据,再次删除。
-
数据库读写分裂的场景 两个请求,一个请求A进行更新操作,另一个请求B进行查询操作。
1)请求A进行写操作,删除缓存
2)请求A将数据写入数据库了,
3)请求B查询缓存发现,缓存没有值
4)请求B去从库查询,这时,还没有完成主从同步,因此查询到的是旧值
5)请求B将旧值写入缓存
6)数据库完成主从同步,从库变为新值
依旧采用延时双删策略解决此问题
9. Redis 持久化有几种方式?
Redis持久化的两种方式
-
RDB:RDB持久化机制,是对 Redis 中的数据执行周期性的持久化。
-
AOF:AOF机制对每条写入命令作为日志,以append-only的模式写入一个日志文件中,在Redis重启的时候,可以通过回放AOF日志中的写入指令来重新构建整个数据集。
通过RDB或AOF,都可以将Redis内存中的数据给持久化到磁盘上面来,然后可以将这些数据备份到别的地方去,比如说阿里云等云服务。
如果Redis挂了,服务器上的内存和磁盘上的数据都丢了,可以从云服务上拷贝回来之前的数据,放到指定的目录中,然后重新启动Redis,Redis就会自动根据持久化数据文件中的数据,去恢复内存中的数据,继续对外提供服务。如果同时使用RDB和AOF两种持久化机制,那么在Redis重启的时候,会使用AOF来重新构建数据,因为AOF中的数据更加完整。
RDB优缺点
- RDB会生成多个数据文件,每个数据文件都代表了某一个时刻中Redis的数据,这种多个数据文件的方式,非常适合做冷备,可以将这种完整的数据文件发送到一些远程的安全存储上去,比如说Amazon的S3云服务上去,在国内可以是阿里云的ODPS分布式存储上,以预定好的备份策略来定期备份Redis中的数据。
- RDB对Redis对外提供的读写服务,影响非常小,可以让Redis保持高性能,因为Redis主进程只需要fork一个子进程,让子进程执行磁盘IO操作来进行RDB持久化即可。
- 相对于AOF持久化机制来说,直接基于RDB数据文件来重启和恢复Redis进程,更加快速。
- 如果想要在Redis故障时,尽可能少的丢失数据,那么RDB没有AOF好。一般来说,RDB数据快照文件,都是每隔5分钟,或者更长时间生成一次,这个时候就得接受一旦Redis进程宕机,那么会丢失最近5分钟的数据。
- RDB每次在fork子进程来执行RDB快照数据文件生成的时候,如果数据文件特别大,可能会导致对客户端提供的服务暂停数毫秒,或者甚至数秒。 AOF优缺点
- AOF可以更好的保护数据不丢失,一般AOF会每隔1秒,通过一个后台线程执行一次fsync操作,最多丢失1秒钟的数据。
- AOF日志文件以append-only模式写入,所以没有任何磁盘寻址的开销,写入性能非常高,而且文件不容易破损,即使文件尾部破损,也很容易修复。
- AOF日志文件即使过大的时候,出现后台重写操作,也不会影响客户端的读写。因为在rewrite log的时候,会对其中的指令进行压缩,创建出一份需要恢复数据的最小日志出来。在创建新日志文件的时候,老的日志文件还是照常写入。当新的merge后的日志文件ready的时候,再交换新老日志文件即可。
- AOF日志文件的命令通过非常可读的方式进行记录,这个特性非常适合做灾难性的误删除的紧急恢复。比如某人不小心用flushall命令清空了所有数据,只要这个时候后台rewrite还没有发生,那么就可以立即拷贝 AOF 文件,将最后一条flushall命令给删了,然后再将该AOF文件放回去,就可以通过恢复机制,自动恢复所有数据。
- 对于同一份数据来说,AOF日志文件通常比RDB数据快照文件更大。
- AOF开启后,支持的写QPS会比RDB支持的写QPS低,因为AOF一般会配置成每秒fsync一次日志文件,当然,每秒一次fsync,性能也还是很高的。(如果实时写入,那么QPS会大降,Redis性能会大大降低)
- 以前AOF发生过bug,就是通过AOF记录的日志,进行数据恢复的时候,没有恢复一模一样的数据出来。所以说,类似AOF这种较为复杂的基于命令日志/merge/回放的方式,比基于RDB每次持久化一份完整的数据快照文件的方式,更加脆弱一些,容易有bug。不过AOF就是为了避免rewrite过程导致的bug,因此每次rewrite并不是基于旧的指令日志进行merge的,而是基于当时内存中的数据进行指令的重新构建,这样健壮性会好很多。
RDB和AOF到底该如何选择
- 不要仅仅使用RDB,因为那样会导致你丢失很多数据;
- 也不要仅仅使用AOF,因为那样有两个问题:第一,你通过AOF做冷备,没有RDB做冷备来的恢复速度更快;第二,RDB每次简单粗暴生成数据快照,更加健壮,可以避免AOF这种复杂的备份和恢复机制的bug;
- Redis支持同时开启开启两种持久化方式,我们可以综合使用AOF和RDB 两种持久化机制,用AOF来保证数据不丢失,作为数据恢复的第一选择; 用 RDB来做不同程度的冷备,在AOF文件都丢失或损坏不可用的时候,还可以使用RDB来进行快速的数据恢复。
10. Redis怎么实现分布式锁?
分布式锁需要解决的问题
- 互斥性:任意时刻只能有一个客户端拥有锁,不能同时被多个客户端获取
- 安全性:锁只能被持有该锁的用户删除,而不能被其他用户删除
- 死锁:获取锁的客户端因为某些原因而宕机而未能释放锁,其他客户端无法获取此锁,需要有机制来避免该类问题
- 容错:当部分节点宕机,客户端仍然能获取锁或者释放锁
非完善方法实现
setnx key value:如果key不存在,则创建并赋值
- 实际复杂度:O(1)
- 返回值:设置成功
此时我们获取的key是长期有效的,所以我们应该如何解决长期持有的问题
expire key seconds
- 设置key的生存时间,当key过期时,会被自动删除
- 缺点:原子性得不到满足
伪代码:
//该程序存在危险,如果执行到第二行就崩溃了,则此时key会被一直占用而无法被释放
RedisService redisService = SpringUtils.getBean(Redi sService.class);
long status = redisService.setnx(key, "1");
if(status == 1) {
redisService.expire(key, expire);
//执行独占资源逻辑
doOcuppiedWork();
}
正确方式实现
set key value [ex seconds] [px milliseconds [nx|xx]
ex seconds: 设置键的过期时间为seconds秒px milliseconds: 设置键的过期时间为milliseconds毫秒nx: 只在键不存在时,才对键进行设置操作xx: 只在键已经存在时,才对键进行设置操作- set操作成功完成时,返回OK,否则返回nil
伪代码
RedisService redisService = SpringUtils.getBean(RedisService.class); .
String result = redisService.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
if ("OK".equals(result)) {
//执行独占资源逻辑
doOcuppiedWork();
}
大量key同时过期的注意事项
- 集中过期,由于清楚大量的key很耗时,会出现短暂的顿卡现象
- 解决方案,在设置key的过期时间的时候,给每个key加上一个随机值
11. Redis分布式锁有什么缺陷?
//todo
12. Redis如何做内存优化?
1) 缩减键值对象
缩减键和值的长度,key长度,如在设计键时,在完整描述业务情况下,键值越短越好;value长度,值对象缩短比较复杂,常见需求是
把业务对象序列化城二进制数组放入Redis,首先应该在业务上精简业务对象,去掉不必要的属性毕淼存储无效数据,其次在序列化工具
选择上,应该选择更高效的序列化工具来降低字节数组的大小,以java为例,内置的序列化方式无论从速度还是压缩比都不尽如人意,
这时可以选择更高效的序列化工具,如protostuff、kryo等等。
2)共享对象池
共享对象池指Redis内部维护[0-9999]的整数对象池,创建大量的整数类型RedisObject存在内存开销,每个RedisObject内部结构至
少占16字节,甚至超过了整数自身空间消耗,所以Redis内存维护一个[0-9999]的整数对象池,用于节省内存,除了整数值对象,其他
类型如list、hash、set、zset内部元素也可以使用整数对象池,因此开发中满足需求的前提下,尽量使用整数对象以节约内存。
3)字符串优化
4)编码优化
5)控制key的数量
13. Redis淘汰策略有哪些?
14. Redis常见的性能问题有哪些?该如何解决?
- noeviction: 不删除策略, 达到最大内存限制时, 如果需要更多内存, 直接返回错误信息。 大多数写命令都会导致占用更多的内存(有极少数会例外, 如 DEL )。
- allkeys-lru: 所有key通用; 优先删除最近最少使用(less recently used ,LRU) 的 key。
- volatile-lru: 只限于设置了 expire 的部分; 优先删除最近最少使用(less recently used ,LRU) 的 key。
- allkeys-random: 所有key通用; 随机删除一部分 key。
- volatile-random: 只限于设置了 expire 的部分; 随机删除一部分 key。
- volatile-ttl: 只限于设置了 expire 的部分; 优先删除剩余时间(time to live,TTL) 短的key
maxmemory 用于指定 Redis 能使用的最大内存。既可以在 redis.conf 文件中设置, 也可以在运行过程中通过 CONFIG SET 命令动态修改。
当内存使用达到最大限制时, 如果需要存储新数据, 根据配置的策略(policies)的不同, Redis可能直接返回错误信息, 或者删除部分老的数据。
策略选择:
-
如果分为热数据与冷数据, 推荐使用 allkeys-lru 策略。 也就是, 其中一部分key经常被读写. 如果不确定具体的业务特征, 那么 allkeys-lru 是一个很好的选择。
-
如果需要循环读写所有的key, 或者各个key的访问频率差不多, 可以使用 allkeys-random 策略, 即读写所有元素的概率差不多。
-
假如要让 Redis 根据 TTL 来筛选需要删除的key, 请使用 volatile-ttl 策略。
volatile-lru 和 volatile-random 策略主要应用场景是: 既有缓存,又有持久key的实例中。 一般来说, 像这类场景, 应该使用两个单独的 Redis 实例。
值得一提的是, 设置 expire 会消耗额外的内存, 所以使用 allkeys-lru 策略, 可以更高效地利用内存, 因为这样就可以不再设置过期时间了。