说起Redis ,我之前一直以为“Redis”就是开发者取的名字,没想到后面看官方解释,发现居然是Remote Dictionary Server的合成,那Redis到底有何神通呢?我们一起来学习一下。
Redis简介:
官方的概念不再重复,这里抛出几个Redis比较核心的几个point:
- Redis是一个Key-Value形式的内存存储系统;
- 只包含“键”-“值”;
- 缓存;
- 分布式锁。
前两点说的是结构,后两点说的是功能,我们根据这几个point逐一展开:
Redis的基本结构:
如同简介里的前两点说的,Redis的结构是Key-Value:
Key(唯一性):字符串;
Value(形式多样):string(字符串)、list(列表)、hash(字典)、set(集合)、zset(有序集合) ;这一丰富的类型结构也是Redis 具有突出优势的关键,我们先依次看一下这几个类型:
- string:string是value涵盖下的最简单的数据结构了,内部表示就是一串字符数组,这段字符串是动态可修改的,内部的结构类似ArrayList;应用广泛,比如Redis作缓存时,可以将序列化后的用户信息存进Redis;
- list:list相当于LinkedList,也就是链表的形式,意味着列表的插入删除的效率很高,时间复杂度为O(1),然而由于索引定位很慢,时间复杂度为O(n);列表结构常用来做异步队列使用。
- hash:咦 ?中文名叫字典?但也不用想太多,结构还是类似我们熟悉的HashMap(数组+链表的二维结构), 内部存储了很多键值对,是无序字典,但也跟HashMap有区别:Redis的hash的value只能是字符串;Redis的hash是渐进式rehash(新hash生成时会保留旧hash,循序渐进地搬迁数据,搬迁完成,新的才取代旧的);hash跟string相比,可以在缓存用户信息时单独存储用户的每个字段(string是以整个用户信息的字符串形式存储),由此,string读取的时候会一次性全部读取,可能会造成网络浪费,但hash存储的消耗又高于string,所以各有利弊。
- set:相当于HashSet,它内部的键值是无序且唯一的,它内部相当于一个value全部为null的字典;主要应用在不可重复的信息保存上。
- zset:是一个很有特点的数据结构,相当于是SortedSet和HashMap的结合体,一方面是一个set,保证了value的唯一性,另一方面value被赋予一个score,代表这个value的排序权重。内部实现依赖“跳表”这种数据结构。可以应用在需要分值或程度表示的数据中。
应用:
缓存:
Redis做缓存,是大家很容易就想到的应用,它的优点突出:1. 纯内存操作,不说花里胡哨的话,总之就是很快;2. 可以支持保存多种类型结构,功能决定特点啊,Redis形式多样的value,为它成为一个支持多种数据结构的数据库提供了保障。
但,优秀如Redis,也在做缓存时遇到小问题:
缓存穿透:
一般都按key去查询缓存,如果不存在对应的value,就去数据库之类的后端系统查询,一些恶意请求利用这点,会故意查询不存在的key,请求量很大,就会对后端系统造成压力。
如何避免:
- 对查询结果为空的情况也进行缓存,缓存时间设置的短一点,或者该key对应的数据插入之后清理缓存。
- 对一定不存在的key进行过滤,把所有可能存在key放在一个Bitmap中,查询时通过Bitmap进行过滤。
缓存雪崩:
当缓存服务器重启或大量缓存集中在某个时间段失效,失效时,会给后端带来很大的压力,导致系统崩溃。
如何避免:
- 缓存失效后,通过加锁或队列控制读数据库,其他写数据库的线程等待。
- 做二级缓存,A为原始缓存,失效时间短;B为拷贝缓存,失效时间长;A失效时可以访问B。
- 不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。
为什么那么快?
- 纯内存操作,Redis为了达到最快的读写速度,将数据都存在内存中,并通过异步方式将数据写入磁盘,所以Redis具有快速和持久化的特征,如果不将数据放在内存中,磁盘I/O速度将会严重影响Redis的性能。
注意事项:Redis为了快,将数据放在内存上,凡事都有利弊,内存储存带来高性能的同时,也使得Redis会受到物理容量的限制,如此,Redis更适合小数据的高性能操作和运算上。 - 单线程,避免上下文切换和竞争条件,不存在多线程切换而导致的CPU消耗,不存在多线程加锁释放锁的问题,避免了死锁导致的性能消耗。
3.Redis的数据结构专门设计的,所以操作简单快捷。 - 多路复用I/O模型。
数据持久化
Redis将数据库的数据加载到内存当中进行操作,会定期通过异步操作把数据flush到硬盘上进行保存,这涉及到了数据持久化存储到硬盘的过程,一般通过两种方式实现:
- 清除原来的存储结构,只将数据存储到磁盘中,消耗的时间比较多。
- 保留原来的存储结构,将数据按原有的格式存在磁盘中。
分布式锁:
其实这个功能是Redis的亮点功能,但往往被很多人忽视,我们可以一起来学习一下。
先来个常见的场景,在淘宝买买买时,如果两个下单是同时完成的,相当于并发执行,在减库存时,两个线程可能拿到的初始库存一样,最后都减1,这样导致的结果是当前比实际库存多了,为了避免这样的情况,就需要分布式锁,Redis在此可以作为分布式锁实现的方式之一。
分布式锁的奥义:
分布式锁其实就是在Redis里占一个“坑”,其他进程想来占坑时,发现坑里已经有人了,就只能放弃或等待。占坑指令一般使用“setnx(set if not exists)”,只允许被一个客户端占坑。释放时使用“del”指令。先来先得,只允许一个客户端持有。
如果在没来及下达“del”指令之前出现了异常,可能会陷入死锁,锁一直得不到释放,所以一般会使用“expire”设置过期时间,但如果setnx和expire之间服务器进程出现问题,expire得不到执行,也会造成死锁。
问题的根源在于setnx和expire是两条指令,不是一个原子操作。如果这两个指令一起执行,就不会出现问题。所以在Redis2.8版本里,setnx和expire组合成了一条原子指令,也算是分布式锁的奥义所在了。
分布式锁的超时问题:
如果在加锁和释放锁之间逻辑执行太长,以至于超出了锁的超时限制,也就是说,第一个线程持有的锁已经到期了,但临界区的任务还没有执行完,就会导致第二个线程开始持有这把锁,临界区的任务无法继续执行。所以,Redis无法解决分布式锁超时的问题。
分布式锁的可重入性:
可重入性指的是线程在持有锁的情况下,再次请求加锁,如果一个锁支持线程多次加锁,那么这个锁就是可重入锁。如何做到的?将锁的值设置成每个线程一个随机的值,如果想要获取锁的线程发现锁的值属于自己的这个线程,那么就可以进入临界区。Redis做分布式锁时,如果对set进行包装,增加代码的复杂性,所以不推荐,在此不再赘述。
网友评论