Redis随手记

1.什么是缓存?什么是Redis?
Redis是一个性能非常夸张的内存非关系数据库。基于Key-Value来做数据存储,Value的数据类型常规情况下我们认为有下图五种。

实际上,Java代码中,我们可以选择把想要存到redis的对象序列化为Json字符串,然后再保存到redis中。取出的时候,得到Json串,再反序列化为对象就可以了。

对于没有分布式缓存概念的同学,我们这里可以简单的把redis想象成一个部署在所有应用之外、一个巨大的独立的Map。这个东西有什么用呢?

假设我有一个页面首页的公告,它有动态编辑的需求,所以存储的时候是放在数据库里的。但是本身改动又不是很频繁,而且所有用户进入系统都会默认跳转到首页,并且读取这段公告。如果后台的数据每次都是从数据库直接取,那么无疑会给DB带来额外的压力。

如果我把这个公告,从数据库里查询出来,然后保存到Java代码里的一个Map里,那么下次接口再查询数据时,我直接把Map里存的数据给它,就避免了查询数据库。那么,为了保证你Map里存的数据一直是对的,当公告内容发生修改时,你需要去Map中删掉公告内容,那么下一次查询的时候,Map里没有,我们就会再次从DB里拿,从而实现Map里的数据刷新。这次一种非常简单的缓存的使用。

但是,如果你在Java代码里写一个Map也会有问题,假设现在你的项目部署了三台服务器,A、B、C,三台服务器里,都缓存了公告内容。如果这时候一个修改请求,从C服务器执行成功,C会删掉/刷新自己的Map里存的公告,但是,A和B并不会。因为他们的Java代码里是没有走修改操作的,你除非额外再想办法通知AB来做更新。这是一种思路,但是这样就非常麻烦了。

假如,我们3台服务器,共用一个Map,大家都往一个Map里存,一个Map里取,是不是就可以避免这种不同步的问题了呢?

所以,我们有了Redis。

当然,这个场景只是Redis众多功能中的一个小部分,它还提供了很丰富的功能。

2.一些常用的操作

Redis是一个单线程的项目,所有的读写任务全部封闭在一个线程中操作,所以Redis是线程安全的。但是它使用了IO复用技术,来提升单线程对于IO的处理能力。再加上Redis的数据全部是在内存中(持久化除外),本身计算和查找就非常的快,是以Redis提供了非常夸张的性能。号称可以达到11W次/秒的读取性能和8W次/秒的写入性能。

Redis是线程安全的,它提供了INCR命令,原子性自增操作。命令行中写法为:

INCR test_key

Spring的Java代码可以写为:

redisTemplate.opsForValue().increment("test_key",1);

如果test_key在redis中不存在,那么test_key会被初始化,并且返回0,如果key已经存在并且有值,则会返回原子+1后的值。这一特性使得Redis可以用来做计数器,或者说自增流水号等内容。

另一个非常有用的数据结构是ZSet,什么是ZSet?

它是一个可以实现排序的数据结构,内部使用SkipList(跳表)来实现。那,什么是跳表?

我们假设一个场景,现在我们有一个空的联表,如果每次插入的时候我们做顺序插入,那么最后得到的链表就是一个有序链表。现在链表中有1~10个元素,如下图:

为了维护有序链表,我们每次插入数据时,都需要对联表做一次顺序查找,来确定新元素的插入位置。如果元素数量非常多的时候,这个顺序查找的效率无疑非常低。

那,我们该怎么办?

其实之前在分享MYSQL索引的时候,我们简单学习过B+tree,B+tree引入了一种姑且称为“分层索引”的理念,来加快查找速度。我们可以用这个想法对链表做一个改动。

我们在这个链表上面一层,做一个新的链表,借鉴b+tree的思想,对它进行分段,然后得到下图:

原始的链表中,我们要查找到9,需要从1逐个向后遍历,一直遍历到9,需要8次操作才能拿到数据,新增了一层结构后,我们用图形标识一下它的查找过程。

那么它只进行了1~4~7~8~9,4次移动操作就拿到了我们想要的9。

如果链表的长度进一步扩大,我们可以选择增加层数的方式来提升查找效率,示意图如下:

SkipList提供了良好的查找性能,这是ZSet排序的基础。假设我们把用户的积分作为跳表的排序依据,我们只需要把用户

数据逐个添加到Redis的ZSet中,就能得到一个已经排序好了的用户排行榜、

3.持久化

Redis是内存型数据库,顾名思义,运行时所有数据都是在内存中,如果不做额外处理,一旦宕机,数据就GG了。数据持久化这块,Redis是选择落地到磁盘文件系统中。它提供了两种方式:

快照持久化,很好理解,类似我们在虚拟机中那种保存快照的机制,经由一段时间后,Redis会把当前所有的数据做成快照,保存在磁盘中。缺点非常明显:当前数据过大,快照保存耗时会比较长,如果发生宕机,两次快照落地之间的数据会丢失。

AOF持久化,Append of file,换句话说,就是把你对于Redis操作的命令(Java封装的客户端,其实最后也是用命令来操作Redis),写入到日志文件中。和kafka的思路一样,对于命令的磁盘IO写入操作,Redis也是把命令先buffer起来,然后在某个时间点一次性写入到磁盘文件中,不过可以通过file.flush()命令强制写入。

AOF持久化对于文件写入提供了三种选择:

always,意思就是总是写入,换句话说就是每个命令都写入。磁盘IO性能先天就落后于内存,这种保存方式无疑会严重拖累Redis的性能。

everysec,每秒同步一次,很容易理解,每秒进行一次file.flush的操作,和always比,性能损失不大,但是如果宕机,会丢失两次file.flush之间的数据。

no,让Redis决定何时保存,但从实际效果来看,性能提升不明显,而且宕机丢失的数据量会更大。

AOF技术中,Redis还提供了压缩技术,例如,在两次file.flush之间,你分别调用了 set a 100 ,set a 200,set a 300,Redis会只保存最后一条指令,因为对于最终结果来说,前两次的操作没有记录的意义。

参考&引用:
CyC2018@Github

ApacheCrazyFan@ Java基础 - 跳表(SkipList)