1.redis是什么?
C编写的开源,支持网络,基于内存,可持久化的改性能键值对数据库
2.优缺点
优点
读写性能优异, Redis能读的速度是110000次/s,写的速度是81000次/s。
支持数据持久化,支持AOF和RDB两种持久化方式。
支持事务,Redis的所有操作都是原子性的,同时Redis还支持对几个操作合并后的原子性执行。
数据结构丰富,除了支持string类型的value外还支持hash、set、zset、list等数据结构。
支持主从复制,主机会自动将数据同步到从机,可以进行读写分离。缺点:
数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此Redis适合的场景主要局限在较小数据量的高性能操作和运算上。
Redis 不具备自动容错和恢复功能,主机从机的宕机都会导致前端部分读写请求失败,需要等待机器重启或者手动切换前端的IP才能恢复。
主机宕机,宕机前有部分数据未能及时同步到从机,切换IP后还会引入数据不一致的问题,降低了系统的可用性。
Redis 较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。为避免这一问题,运维人员在系统上线时必须确保有足够的空间,这对资源造成了很大的浪费。
3.为什么要用 Redis /为什么要用缓存
主要从“高性能”和“高并发”这两点来看待这个问题。
- 高性能:
假如用户第一次访问数据库中的某些数据。这个过程会比较慢,因为是从硬盘上读取的。将该用户访问的数据存在数缓存中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据即可! - 高并发:
直接操作缓存能够承受的请求是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。
4.为什么要用 Redis 而不用 map/guava 做缓存?
缓存分为本地缓存和分布式缓存。
以 Java 为例,使用自带的 map 或者 guava 实现的是本地缓存,最主要的特点是轻量以及快速,生命周期随着 jvm 的销毁而结束,并且在多实例的情况下,每个实例都需要各自保存一份缓存,缓存不具有一致性。
5.Redis为什么这么快
- 完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于 HashMap,HashMap 的优势就是查找和操作的时间复杂度都是O(1);
- 数据结构简单,对数据操作也简单,Redis 中的数据结构是专门进行设计的;
- 采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
- 使用多路 I/O 复用模型,非阻塞 IO;
- 使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis 直接自己构建了 VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;
6.数据类型:
- 字符串String
set key value:key存在,则覆盖,返回OK;get key,存在返string,不存在返null;getset key value:先获取该key的值,然后在设置该key的值。incr key:指定key的value递增1.如key不存在,创建该key初始value值为0,在incr后其为1;append key value:如该key存在,则在原有的value后拼接该值;如该key不存在,则重创一个key/value。 - 列表List
lpush/rpush mylist l1:从列表左/右侧头部添加数据;lpop/rpop:从给左/右侧头部取出一个元素;llen list 0 -1:获取列表的长度;lindex list 2:获取第几个坐标下的值 - 哈希Hash
hset/hget key k1 v1 设置/获取;hmset key/hmget key k2 v2 k3 v3 同时设置/获取多;hkeys/hvals key获取所有keys/values;hdel key k1 k2 k3 删除key 下指定键值;hexists key field 判断key 下指定键值是否存在 - 集合Set
sadd myset vi,v2…添加元素;smembers myset 获取集合中所有元素;sismember myset v1 判断元素是否在集合中 - 有序集合Zset/Sort set
zadd key v k 往key中添加一个元素; zadd artHits 99 12表示id为12的文章点击量为99次;zrange key start end 根据v的值由小到大进行排序来获得start到end之间的元素;zcard 返回key集合中元素个数; zscore key k 取出集合key中键为k对应的值v。
数据类型 | 可以存储的值 | 应用场景1 | 应用场景2 |
---|---|---|---|
String | 字符串,整数或者浮点数 | 很简单的键值缓存;短信验证码,敏感词信息等,就用这种类型来存储 | 缓存、计数器、分布式锁等 |
list | 列表 | 存储一些列表形的数据结构,省市区表、字典表等。 | 链表、队列、微博关注人时间轴列表等 |
set | 无序集合 | 交集,并集,差集的操作 | 去重、赞、踩、共同好友等 |
hash | 包含键值对的无序散列表 | 结构化的数据,比如一个对象。一般key为ID或者唯一标示,value对应的就是详情了。如机构信息详情,域控详情等。 | 用户信息、Hash 表等 |
zset | 有序集合 | 去重但可以排序。增加了一个score参数,自动会根据score的值进行排序。根据代理商的代理人数排序 | 访问量排行榜、点击量排行榜等 |
Bitmaps:并不是一种真实的数据结构,本质上是Strings数据结构,只不过操作的力度变成位bit。String最大长度512MB,它是2^32个bit
HyperLogLogs
GEO:存储地理坐标,并且坐标有限制。有效的经度从-180度到180度;有效的维度从-85.05到85.05
Streams:Redis实现的内存版kafka,strems底层的数据结构是redistree
7.持久化:
将内存中的数据写入非易失介质中,比如机械磁盘和SSD
1)RDB
将数据库快照以二进制的方式保存到磁盘中;适合数据的容灾备份 和恢复,耗时短速度快
SAVE:阻塞式持久化
BESAVE:非阻塞式持久化
每隔一段时间持久化一次,故障就会丢失宕机时刻与上一次持久化之间的数据,无法保持数据完整性
fork
2)AOF
以协议文本方式,将所有对数据库进行写入的命令和参数记录到AOF文件,从而记录数据库状态。
redis将每一个收到的写明了通过write函数追加到文件中
就是AOF是通过保存对redis服务端的写命令来记录数据库状态的
存储的是指令序列,恢复重放时要花很长时间并且文件更大
8.分布式锁:
互斥性在任意时刻,只有一个客户端能持有锁;不能死锁客户端在持有锁的期间崩溃而没有主动解锁;容错性大部分的Redis节点正常运行
分布式锁的三个主要元素:加锁,解锁,锁超时。
Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问
多客户端对Redis的连接并不存在竞争关系Redis中可以使用SETNX命令实现分布式锁。
①使用setnx+expire
两个命令实现
当且仅当 key 不存在,将 key 的值设为 value。若给定的 key 已经存在,则 SETNX 不做任何动作。设置成功,返回 1 。设置失败,返回 0 。 setnx key value1 get key
使用SETNX完成同步锁的流程及事项如下:
使用SETNX命令获取锁,若返回0(key已存在,锁已存在)则获取失败,反之获取成功
缺点:不是同步操作,当setnx成功后,准备执行expire前,程序突然出现错误,则添加的数据就无法清除了,因为没有超时时间,不会自动清除。
② set key random_value ex px NX ex分钟,px毫秒,xn
键不存在则对值操作
问题:过期时间如何设置?如果客户端在操作共享资源的过程中,因为长期阻塞的原因,导致锁过期,那么接下来访问共享资源就不安全。
有一个内容修改页面,为了避免出现多个客户端修改同一个页面的请求,采用分布式锁。只有获得锁的客户端,才能修改页面:
缺点:当存储所对应的key那个节点挂了,就会存在锁丢失的风险,导致多个客户端持有同一把锁。
③红锁—可重入考虑失败情况,可以设置锁的最大等待时间
1)redission
提供了实现红锁的办法RedissionLock,解决了单点失败,需要搭建额外的环境。
2)自己实现-Jedis开源组件,有set之类的方法。
9.遇到的问题
1.缓存穿透
指查询一个一定不存在的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。
解决方案:
①业务id不在区间内的直接返回
②如果一个查询返回的数据为空(不管是数 据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。
③采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,【判断该key是否存在,对该key分别用三个hash方法,得到三个bit值且值为1,证明key存在;有一个为0,则不存在】从而避免了对底层存储系统的查询压力。
2.缓存雪崩
设置缓存时采用了相同的时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。过期
解决方案:讲缓存失效时间分散开,比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
3.缓存击穿
一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据,在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
解决方案:目标是尽量少的线程构建缓存(甚至是一个) + 数据一致性 + 较少的潜在危险
使用互斥锁(mutex key): 这种解决方案思路比较简单,就是只让一个线程构建缓存,其他线程等待构建缓存的线程执行完,重新从缓存获取数据就可以了。如果是单机,可以用synchronized或者lock来处理,如果是分布式环境可以用分布式锁就可以了(分布式锁,可以用memcache的add, redis的setnx, zookeeper的添加节点操作)。
①memcache的add
②redis的setnx
针对业务系统,永远都是具体情况具体分析,没有最好,只有最合适。
可靠性
首先,为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:
互斥性。在任意时刻,只有一个客户端能持有锁。
不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
10.内存淘汰策略
Redis是基于内存的key-value数据库,因为系统的内存大小有限,所以我们在使用Redis的时候可以配置Redis能使用的最大的内存大小。
1. 通过配置文件配置
通过在Redis安装目录下面的redis.conf配置文件中添加以下配置设置内存大小
2. 通过命令修改
Redis支持运行时通过命令动态修改内存大小
Q1:配置的内存就有用完的时候,还继续往Redis里面添加数据,不能直接返回异常,如何设置?
Redis定义了几种策略用来处理这种情况:
noeviction(默认策略):对于写请求不再提供服务,直接返回错误(DEL请求和部分特殊请求除外)
allkeys-lru:从所有key中使用LRU算法进行淘汰
volatile-lru:从设置了过期时间的key中使用LRU算法进行淘汰
allkeys-random:从所有key中随机淘汰数据
volatile-random:从设置了过期时间的key中随机淘汰
volatile-ttl:在设置了过期时间的key中,根据key的过期时间进行淘汰,越早过期的越优先被淘汰
LRU:近最少使用,是一种缓存置换算法。在使用内存作为缓存的时候,缓存的大小一般是固定的。当缓存被占满,这个时候继续往缓存里面添加数据,就需要淘汰一部分老的数据,释放内存空间用来存储新的数据.。核心思想是:如果一个数据在最近一段时间没有被用到,那么将来被使用到的可能性也很小,所以就可以被淘汰掉。
LRU在Redis中的实现
Redis使用的是近似LRU算法,它跟常规的LRU算法还不太一样。近似LRU算法通过随机采样法淘汰数据,每次随机出5(默认)个key,从里面淘汰掉最近最少使用的key。
为了实现近似LRU算法,给每个key增加了一个额外增加了一个24bit的字段,用来存储该key最后一次被访问的时间。
11.事务的属性
传播行为,隔离级别,制度和事务超时,主要通过5个命令来操作的
multi,exec,watch,unwatch,discard
标记事务块开始->执行事务块命令->监控1/n个key,被监视前key被改动则是事务被打断(类似乐观锁)->取消watch对key的监控->取消实物,放弃事务块内所有命令
12.为什么要做Redis分区
分区可以让Redis管理更大的内存,Redis将可以使用所有机器的内存。如果没有分区,你最多只能使用一台机器的内存。分区使Redis的计算能力通过简单地增加计算机得到成倍提升,Redis的网络带宽也会随着计算机和网卡的增加而成倍增长。