文章对Redis数据结构指令、高并发处理、持久化、集群、复制、Redis 应用等问点做了解析。
用XMind画了一张导图记录Redis的学习笔记和一些面试解析(源文件对部分节点有详细备注和参考资料, 欢迎关注我的公众号:以Java架构赢天下 后台发送【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)却可以重复。
Redis 中的事务是一组命令的集合,是 Redis 的最小执行单位,一个事务要么都执行,要么都不执行。 带有以下三个重要的保证:
Redis 事务的原理是 先将属于一个事务的命令发送给 Redis,然后依次执行这些命令。
单个 Redis 命令的执行是原子性的,但 Redis 没有在事务上增加任何维持原子性的机制,所以 Redis 事务的执行并不是原子性的。
事务可以理解为一个打包的批量执行脚本,但批量指令并非原子化的操作,中间某条指令的失败不会导致前面已做指令的回滚,也不会造成后续的指令不做。
DISCARD: 取消事务,放弃执行事务块内的所有命令。
EXEC: 执行所有事务块内的命令。
MULTI: 标记一个事务块的开始。
WATCH: Redis Watch 命令用于监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断
UNWATCH : 取消 WATCH 命令对所有 key 的监视。
用一句话可以将持久化概括为:将数据(如内存中的对象)保存到可永久保存的存储设备中。
持久化的主要应用是将内存中的对象存储在数据库中,或者存储在磁盘文件中、 XML 数据文件中等等。
也可以从如下两个层面来理解持久化:
应用层: 如果关闭( Close )你的应用,然后重新启动则先前的数据依然存在。
系统层: 如果关闭( Shut Down )你的系统(电脑),然后重新启动则先前的数据依然存在。
Redis 提供两种方式进行持久化。
RDB 持久化: 原理是将 Reids 在内存中的数据库记录定时 dump 到磁盘上的 RDB 持久化。
AOF(append only file)持久化: 原理是将 Redis 的操作日志以追加的方式写入文件。
aof,rdb是两种 redis持久化的机制。用于crash后,redis的恢复。
rdb的特性如下:
aof有如下特性:
两种区别就是, 一个是持续的用日志记录写操作,crash后利用日志恢复;一个是平时写操作的时候不触发写,只有手动提交save命令,或者是关闭命令时,才触发备份操作。
选择的标准,就是看系统是愿意牺牲一些性能,换取更高的缓存一致性(aof),还是愿意写操作频繁的时候,不启用备份来换取更高的性能,待手动运行save的时候,再做备份(rdb)。rdb这个就更有些 eventually consistent的意思了。
RDB优点
RDB缺点
AOF优点
AOF缺点:
Redis的内存淘汰策略是指在Redis的用于缓存的内存不足时,怎么处理需要新写入且需要申请额外空间的数据。
Redis 的缓存淘汰策略有:
noeviction: 当内存不足以容纳新写入数据时,新写入操作会报错。
allkeys-lru: 当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。
allkeys-random: 当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。
volatile-lru: 当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。
volatile-random: 当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。
定时过期策略
每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。
惰性过期策略
只有当访问一个key时,才会判断该key是否已过期,过期则清除。该策略可以最大化地节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。
定期过期策略
每隔一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。
Redis中同时使用了惰性过期和定期过期两种过期策略。
所谓定期删除,指的是 redis 默认是每隔 100ms 就随机抽取一些设置了过期时间的 key,检查其是否过期,如果过期就删除。
假设 redis 里放了 10w 个 key,都设置了过期时间, 你每隔几百毫秒,就检查 10w 个 key,那 redis 基本上就死了,cpu 负载会很高的,消耗在你的检查过期 key 上了。 注意, 这里可不是每隔 100ms 就遍历所有的设置过期时间的 key,那样就是一场性能上的灾难。实际上 redis 是每隔 100ms 随机抽取一些 key 来检查和删除的。
但是问题是,定期删除可能会导致很多过期 key 到了时间并没有被删除掉,那咋整呢? 所以就是惰性删除了。这就是说,在你获取某个 key 的时候,redis 会检查一下 ,这个 key 如果设置了过期时间那么是否过期了?如果过期了此时就会删除,不会给你返回任何东西。
获取 key 的时候,如果此时 key 已经过期,就删除,不会返回任何东西。 但是实际上这还是有问题的,如果定期删除漏掉了很多过期 key,然后你也没及时去查,也就没走惰性删除,此时会怎么样?如果大量过期 key 堆积在内存里,导致 redis 内存块耗尽了,咋整? 答案是:走内存淘汰机制。
1.1、什么是缓存雪崩?
如果缓存集中在一段时间内失效,发生大量的缓存穿透,所有的查询都落在数据库上,造成了缓存雪崩由于原有缓存失效,新缓存未到期间所有原本应该访问缓存的请求都去查询数据库了,而对数据库CPU和内存造成巨大压力,严重的会造成数据库宕机。
举例来说, 我们在准备一项抢购的促销运营活动,活动期间将带来大量的商品信息、库存等相关信息的查询。 为了避免商品数据库的压力,将商品数据放入缓存中存储。 不巧的是,抢购活动期间,大量的热门商品缓存同时失效过期了,导致很大的查询流量落到了数据库之上。对于数据库来说造成很大的压力。
1.2、有什么解决方案来防止缓存雪崩?
1、加锁排队
mutex互斥锁解决,Redis的SETNX去set一个mutex key,当操作返回成功时,再进行加载数据库的操作并回设缓存,否则,就重试整个get缓存的方法
2、数据预热
缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题。用户直接查询事先被预热的缓存数据。可以通过缓存reload机制,预先去更新缓存,再即将发生大并发访问前手动触发加载缓存不同的key
3、双层缓存策略
C1为原始缓存,C2为拷贝缓存,C1失效时,可以访问C2,C1缓存失效时间设置为短期,C2设置为长期
4、定时更新缓存策略
实效性要求不高的缓存,容器启动初始化加载,采用定时任务更新或移除缓存
5、设置不同的过期时间,让缓存失效的时间点尽量均匀
2.1.什么是缓存预热
缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题。 用户直接查询事先被预热的缓存数据。如图所示:
如果不进行预热, 那么 Redis 初识状态数据为空,系统上线初期,对于高并发的流量,都会访问到数据库中, 对数据库造成流量的压力。
2.2.有什么解决方案?
3.1、什么是缓存穿透?
缓存穿透是指用户查询数据,在数据库没有,自然在缓存中也不会有。这样就导致用户查询的时候,在缓存中找不到对应key的value,每次都要去数据库再查询一遍,然后返回空(相当于进行了两次无用的查询)。这样请求就绕过缓存直接查数据库
3.2、有什么解决方案来防止缓存穿透?
1、缓存空对象
简单粗暴的方法,如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。
2、布隆过滤器
优势: 占用内存空间很小,位存储;性能特别高,使用key的hash判断key存不存在
将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力
降级的情况,就是缓存失效或者缓存服务挂掉的情况下,我们也不去访问数据库。我们直接访问内存部分数据缓存或者直接返回默认数据。
举例来说:
对于应用的首页,一般是访问量非常大的地方,首页里面往往包含了部分推荐商品的展示信息。这些推荐商品都会放到缓存中进行存储,同时我们为了避免缓存的异常情况,对热点商品数据也存储到了内存中。同时内存中还保留了一些默认的商品信息。如下图所示:
降级一般是有损的操作,所以尽量减少降级对于业务的影响程度。
5.1、什么是缓存击穿?
缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力
5.2、会带来什么问题
会造成某一时刻数据库请求量过大,压力剧增
5.3、如何解决
5.3.1.使用互斥锁(mutex key)
这种解决方案思路比较简单,就是只让一个线程构建缓存,其他线程等待构建缓存的线程执行完,重新从缓存获取数据就可以了。 如果是单机,可以用synchronized或者lock来处理,如果是分布式环境可以用分布式锁就可以了(分布式锁,可以用memcache的add, redis的setnx, zookeeper的添加节点操作)。
5.3.2."永远不过期"
从功能上看,如果不过期,那不就成静态的了吗?所以我们把过期时间存在key对应的value里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建,也就是“逻辑”过期
5.3.3.缓存屏障
该方法类似于方法一:使用countDownLatch和atomicInteger.compareAndSet()方法实现轻量级锁
class MyCache{ private ConcurrentHashMap<String, String> map; private CountDownLatch countDownLatch; private AtomicInteger atomicInteger; public MyCache(ConcurrentHashMap<String, String> map, CountDownLatch countDownLatch, AtomicInteger atomicInteger) { this.map = map; this.countDownLatch = countDownLatch; this.atomicInteger = atomicInteger; } public String get(String key){ String value = map.get(key); if (value != null){ System.out.println(Thread.currentThread().getName()+"/t 线程获取value值 value="+value); return value; } // 如果没获取到值 // 首先尝试获取token,然后去查询db,初始化化缓存; // 如果没有获取到token,超时等待 if (atomicInteger.compareAndSet(0,1)){ System.out.println(Thread.currentThread().getName()+"/t 线程获取token"); return null; } // 其他线程超时等待 try { System.out.println(Thread.currentThread().getName()+"/t 线程没有获取token,等待中。。。"); countDownLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } // 初始化缓存成功,等待线程被唤醒 // 等待线程等待超时,自动唤醒 System.out.println(Thread.currentThread().getName()+"/t 线程被唤醒,获取value ="+map.get("key")); return map.get(key); } public void put(String key, String value){ try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } map.put(key, value); // 更新状态 atomicInteger.compareAndSet(1, 2); // 通知其他线程 countDownLatch.countDown(); System.out.println(); System.out.println(Thread.currentThread().getName()+"/t 线程初始化缓存成功!value ="+map.get("key")); } } class MyThread implements Runnable{ private MyCache myCache; public MyThread(MyCache myCache) { this.myCache = myCache; } @Override public void run() { String value = myCache.get("key"); if (value == null){ myCache.put("key","value"); } } } public class CountDownLatchDemo { public static void main(String[] args) { MyCache myCache = new MyCache(new ConcurrentHashMap<>(), new CountDownLatch(1), new AtomicInteger(0)); MyThread myThread = new MyThread(myCache); ExecutorService executorService = Executors.newFixedThreadPool(5); for (int i = 0; i < 5; i++) { executorService.execute(myThread); } } } 复制代码
1.1slaveof命令
1.2.redis.conf配置文件配置
主从结构一是可以进行冗余备份,二是可以实现读写分离
主从复制
冗余备份(还可以称为:主从复制,数据冗余,数据备份,可以实现容灾快速恢复)
持久化保证了即使redis服务重启也会丢失数据,因为redis服务重启后会将硬盘上持久化的数据恢复到内存中,但是当redis服务器的硬盘损坏了可能会导致数据丢失,如果通过redis的主从复制机制就可以避免这种单点故障. 例如:我们搭建一个主叫做redis0,两个从,分别叫做redis1和redis2,即使一台redis服务器宕机其它两台redis服务也可以继续提供服务。主redis中的数据和从redis上的数据保持实时同步,当主redis写入数据时通过主从复制机制会复制到两个从redis服务上。
1.一个Master可以有多个Slave,不仅主服务器可以有从服务器,从服务器也可以有自己的从服务器
2.复制在Master端是非阻塞模式的,这意味着即便是多个Slave执行首次同步时,Master依然可以提供查询服务;
3.复制在Slave端也是非阻塞模式的:如果你在redis.conf做了设置,Slave在执行首次同步的时候仍可以使用旧数据集提供查询;你也可以配置为当Master与Slave失去联系时,让Slave返回客户端一个错误提示;
4.当Slave要删掉旧的数据集,并重新加载新版数据时,Slave会阻塞连接请求
读写分离
主从架构中,可以考虑关闭主服务器的数据持久化功能,只让从服务器进行持久化,这样可以提高主服务器的处理性能。从服务器通常被设置为只读模式,这样可以避免从服务器的数据被误修改。
慢查询就是指,系统执行命令之后,计算统计每条指令执行的时间,如果执行时间超过设置的阈值,就说明该命令为慢指令。
Redis 慢查询配置参数为:
slowlog-log-slower-than:设置慢查询定义阈值,超过这个阈值就是慢查询。单位为微妙,默认为 10000。
slowlog-max-len:慢查询的日志列表的最大长度,当慢查询日志列表处于最大长度的时候,最早插入的一个命令将从列表中移除。
主从模式,一般是一个主节点,一或多个从节点,为了保证我们的主节点宕机后,数据不丢失,我们将主节点的数据备份到从节点,从节点并不进行实际操作,只做实时同步操作,并不能起到高并发的目的。
哨兵模式,一个哨兵集群和一组主从架构组成。比主从更好的是当我们的主节点宕机以后,哨兵会主动选举出一个主节点继续向外提供服务。
集群架构,由很多个小主从聚集在一起,一起向外提供服务的,将16384个卡槽切分存储,并不是一个强一致性的集群架构,每一个小主从节点内会存在选举机制,保证对外的高可用架构。
有时候讲出来的更通俗
Redis分布式锁正确实现视频讲解
2020年面试必备的Java后端进阶面试题总结了一份将近500页的pdf文档,欢迎关注我的公众号:以Java架构赢天下,回复【2020】领取这些整理的资料!
喜欢文章记得关注我点个赞哟,感谢支持!