1、减少客户端访问服务端次数
2、减少服务端请求次数
3、减少数据库压力
4、减少磁盘IO次数
1、应用中哪类数据使用缓存
2、应用数据时,如何写入缓存
3、应用缓存数据如何保证命中率
4、缓存如何保证实时性
5、如何保证缓存数据不丢失
Java程序使用ThreadLocal,List,Map等保存应用数据,生命周期等同JVM。
1、基于Java开发,足够简单,轻量化
2、纯Java的进程内缓存框架,非分布式的,应用服务器多节点部署时存在问题
3、被广泛运用与其他的开源项目,比如Ehcache和Hibernate进行整合时,能将查询出的对象集合放入内存中,下次如果再查询,将直接从内存中返回这个数据集合,不需要再进行数据库的查询。
3、自己项目中直接使用Ehcache作为缓存方案的情况较少
1、C语言编写,高性能,分布式对象缓存系统
2、最初设计于缓解动态网站数据库加载数据的延迟性
3、常用于网站KV形式的缓存,可以把它想象成一个巨大的内存HashTable
4、Memcache支持多cpu同时工作
5、基于纯内存,不做持久化,掉电数据丢失,数据体量大时的宕机重启不可容忍(会压跨数据库)
Redis是在Memcache之后编写的,所以经常把这两者拿出来做比较
官网网址:https://redis.io/commands/
中文网址:http://www.redis.cn/commands.html#string
Remote DIctionary Server(Redis) 是一个由 Salvatore Sanfilippo 写的 key-value 存储系统,官方 定义自己为:”跨平台的非关系型数据库“。实际上也就是拿来做缓存用。
开源的,使用 ANSI C 语言编写、遵守 BSD 协议、支持网络、可基于内存、分布式、可选持久性,并提 供多种语言的 API。
缓存处理上应该说Memcached和Redis都能很好的满足解决我们的问题,它们性能都很高,总的来 说,可以把Redis理解为是对Memcached功能上的拓展,是更加重量级的实现,提供了更多更强大的功 能。
Redis是k-v型数据库,每个数据的key都是字符串类型。value可以为以下几种对象类型:
Redis没有直接使用C语言传统的字符串,而是自己构建了一种名为简单动态字符串(Simle Dynamic String 简称:SDS)的抽象类型,并将SDS用作Redis的默认字符串表示。
SDS_TYPE_5,SDS_TYPE_8,SDS_TYPE_16,SDS_TYPE_32,SDS_TYPE_64
常用命令
set
:设置值,覆写旧值,无视类型,原有的 TTL 将被清除append
:将 value 追加到 key 原来的值的末尾,如果 key 不存在,就简单地将给定 key 设为 valuemset
: 同时设置一个或多个 key-value 对getrange
: 返回 key 中字符串值的子字符串,字符串的截取范围由 start 和 end 两个偏移量决定(包 括 start 和 end 在内)。 负数偏移量表示从字符串最后开始计数, -1 表示最后一个字符, -2 表示倒数第二个,以 此类推set EX second
设置键的过期时间为 second 秒。 SET key value EX second 效果等同于 SET EX key second value 。set PX millisecond
设置键的过期时间为 millisecond 毫秒。 SET key value PX millisecond 效果等同于 PSET EX key millisecond valueset NX
只在键不存在时,才对键进行设置操作。 SET key value NX 效果等同于 SETNX key value 。set XX
只在键已经存在时,才对键进行设置操作运算:INCR 、 INCRBY,DECR,DECRBY
Hash类似于java里的HashMap,多用于存储多属性对象,除了基本的set、get方法之外,提供了单属性 的值运算,判断属性是否存在等高级使用方法。
与String不同的是,我们的一个key可以保存多个属性对应的值。可以用来保存我们的热点数据,相比于 使用String类型缓存json字符串,更方便后续单个属性的操作。
常用命令
hget
返回哈希表 key 中给定域 field 的值。当给定域不存在或是给定 key 不存在时,返回空hexists
: 查看哈希表 key 中,给定域 field 是否存在。如果哈希表含有给定域,返回 1 ,否则返回 0hsetnx
: 将哈希表 key 中的域 field 的值设置为 value ,当且仅当域 field 不存在。若域 field 已经存在,该操作无效。hgetall
: 返回哈希表 key 中,所有的域和值hincrby
: 为哈希表 key 中的域 field 的值加上增量 increment ,增量也可以为负数,相当于对给定域 进行减法操作。思考
Redis存储java对象,一般是String 或 Hash 两种,那到底什么时候用String ? 什么时候用hash ?
String的存储通常用在频繁读操作,它的存储格式是json,即把java对象转换为json,然后存入Redis。 Hash的存储场景应用在频繁写操作,即,当对象的某个属性频繁修改时,不适用string+json的数据结 构,因为不灵活,每次修改都需要把整个对象转换为json存储。如果采用hash,就可以针对某个属性单 独修改,不用序列号去修改整个对象。例如短链接,商品的库存、价格、关注数、评价数经常变动时, 就使用存储hash结果。
注意
Redis强大的过期功能不用应用在field上,只能应用在key上。如果key过期,则所有的属性值全部消失
List即链表,链表提供了高效的节点重排能力,以及顺序性的节点访问方式,并且可以通过增加、删除 节点来灵活的调整链表的长度。 双端队列
因为C语言没有内置这种数据结构,所以Redis构建了自己的链表实现。
// 链表节点对象结构
typedef struct listNode{
// 当前节点的前置节点
struct listNode prev;
// 当前节点的后置节点
struct listNode next;
// 当前节点的值
void value;
}
多个listNode可以通过prev和next指针组成双端链表,如下图:
常用命令:
lindex
: 返回下标为 index 的元素, 0 表示第一个元素, 1 表示第二个元素, -1 表示最后一个元素, -2 表示倒数第二个元素 lpush
: 将一个或多个值 value 插入到列表 key 的表头,如果有多个 value 值,那么各个 value 值按 从左到右的顺序依次插入到表头。lpop
: 移除并返回列表 key 的头元素。blpop
: 它是 lpop命令的阻塞版本,当给定列表内没有任何元素可供弹出的时候,直到等待超时或发现可 弹出元素为止。rpop
: 移除并返回列表 key 的尾元素。rpush
: 将一个或多个值 value 插入到列表 key 的表尾(最右边)。brpop
: 它是 命令的阻塞版本,当给定列表内没有任何元素可供弹出的时候,直到等待超时或发现可弹 出元素为止。因为链表的特性,对于节点的读取与插入时的顺序保持一致性,可以做微型消息传递、阻塞队列、栈等 业务功能。
Set等同于java里的HashSet,功能类似,当时相比于HashSet多了一些高级功能。Redis的Set里的数据 是无序的,即不保证插入的顺序,也不保证插入值得排序。相比与List,提供了判断一个成员是否存在的 API,而相比于Hash缺少了value,只保留了field属性。
基于Set我们可以轻易实现2个集合的差集、交集、并集等操作,满足日常中对2个集合的操作,如:重复 配置、共同权限、共同关注等。
常用命令
sadd
: 将一个或多个 member 元素加入到集合 key 当中,已经存在于集合的 member 元素将被忽略srem
: 移除集合 key 中的一个或多个 member 元素,不存在的 member 元素会被忽略。smove
: 将 member 元素从 source 集合移动到 destination 集合sdiff
: 返回一个集合的全部成员,该集合是所有给定集合之间的差集sunion
: 返回一个集合的全部成员,该集合是所有给定集合的并集。sinter
: 返回一个集合的全部成员,该集合是所有给定集合的交集。srandmember
: 如果命令执行时,只提供了 key 参数,那么返回集合中的一个随机元素, 如果提供 count参数,则弹出count个元素,如果大于集合的数量,则返回整个集合。spop:
移除并返回集合中的一个随机元素(重点:移除)。zset同set相似度很高,不同的是zset引入了一个score分值概念,对于集合里的每个元素必须自定分 值,并且根据指定的分值进行排序。 通过score的值,我们可以完成日常工作中点击量排序、点赞排序、延时任务、榜单等。
常用命令
redis维护一个二进制数组,最大长度2的32次方(bit映射被限制在512M之内),可以保存40亿的结果,但 是空间使用很低。如果下图:
常用命令:
在初始化服务器时,程序会根据服务器状态的dbnum属性来决定应该创建多少个数据库,默认为16 个。每个Redis客户端都有自己的目标数据库,每当客户端执行数据库写命令或者数据库读命令时,目标 数据库就会成为这些命令的操作对象。切换数据库的命令为select,后面的数字为数据库编号,从0开 始。 Redis中每个数据库的数据是独立的,不能跨库操作数据。
缓存具有短暂性的特点,并且内存空间资源紧张,缓存在使用后需要及时清除。Redis通过命令expire对 key添加过期时间如下:
expire key 30 // 设置过期时间30秒
redis使用一个字典保存所有键的过期时间,称为过期字典。
好处: 当对一个键添加或删除过期时间时,只需要操作过期字典,不需要对原键进行操作。当Redis判断或者 删除过期键时,只需要便利本过期字典即可,不需要遍历所有键
Redis与Memcache最大的不同点就是支持数据的持久化。
缓存数据为什么要进行持久化?如果我们的缓存数据是从数据库或其他介质一次加载,并且永不变 更只支持查询,我们的数据可以容忍非持久化。但是如果缓存里是实时业务数据,涉及到新增、修改、 删除呢?
Redis提供了两种不同的持久化方式:RDB(Redis DataBase)和AOF(Append Only File)
RDB是基于数据进行持久化,Redis数据中过期数据不进行持久化 下图
save <seconds> <changes>
Redis 默认配置文件中提供了三个条件:
save 900 1
save 300 10
save 60 10000
分别表示 900 秒(15 分钟)内有 1 个更改,300 秒(5 分钟)内有 10 个更改以及 60 秒内有 10000
个更改
AOF是基于Redis命令进行持久化,保存的是Redis执行过的所有命令 。如下图
appendonly no 是否开启AOF持久化,默认为no,需要改为yes
appendfilename "appendonly.aof" AOF持久化文件
在Redis中,用户可以通过执行slaveof命令或者设置slaveof选项,让一个服务器去复制另一个服务器, 我们称被复制的为主服务器,而对主服务器进行复制的则称为从服务器。
在这里,我们使用了复制来形容2个服务器之间的关系,所以2个服务器的数据完全一致。一个主服务器 可以拥有多个从服务器,并且在主服务器和从服务器上实施读写分离。
Sentinel(哨兵)是Redis的高可用性解决方案,由一个或多个Sentinel实例组成的Sentinel集群可以监视任 意多个主服务器,以及其所有的从服务器,并在被监视的主服务器进入下线状态时,自动将下线主服务 器的某个从服务器升级为新服务器。然后由新的主服务器继续处理命令请求。
主观下线状态:
当Sentinel在down-after-milliseconds毫秒内,连续收到监控服务器的无效回复,那么就标志实例为主观下线状态
客观下线状态:
当Sentinel将一个主服务器判断为主观下线后,为了确认这个主服务器是否真的下线了,它会向同样 监视这一主服务器的其他Sentinel进行询问,看他们是否也认为主服务器已经下线。如果是,则判定为 客观下线,并对主服务器执行故障转移。
即使使用哨兵,Redis每个实例也是全量存储完整的数据,而Redis基于内存的特性决定了它每个实例存 储的数据是有限的,受限于机器的内存。所以应对海量数据单集群不行,分片是最直接的手段,类比 mysql的分库分表。
Redis集群是Redis提供的分布式数据库方案,集群通过分片来进行数据共享,并提供复制和故障转移功 能。
一个Redis集群通常由多个节点组成,Redis集群通过分片的方式来保存数据库中的键值对,整个集群被 分为16384个槽(slot),每个节点可以处理0个或者最多16384个槽。只有所有槽都有节点处理时,集群才 处于上线状态。
注意:开启集群,redis启动时的cluster-enabled配置必须为yes,默认是注释的
思考:根据上面的图示,cluster最重要的知识点是槽。那么关于槽,我们该如何的分配,读写,存储该 如何做呢?
struct clusterNode{
// 创建节点的时间
mstime_t ctime;
//节点的名字
char name;
// 节点标识,目前所处的状态,或者主节点还是从节点
int flags;
// 节点的ip地址
char ip;
// 节点的端口号
int port;
// 在当前节点的视觉下,集群的状态,集群包含多少个节点等等
clusterStated state;
// ......
// 一个二进制位数组,长度位16384/8 = 2048个字节。用来记录节点负责处理那些槽。
char slots[16384/8]
// 节点负责处理的槽的数量,即二进制位为1的数量
int numslots;
}
struct clusterState {
// 指向当前节点的指针
clusterNode myself;
// 集群节点名单(包括节点本身)
// 结构类似HashMap,key为节点的名字,value为对应节点的clusterNode对象
dict nodes;
// 每个数组项都是指向clusterNode结构的指针
clusterNode slots[16384]
//......
}
根据上面的知识,我们知道了一个节点负责处理哪些槽,那如果我们想知道一个槽由哪个节点负责,该 如何呢?
此时clusterState的slots数组起到关键作用。
思考:为什么每个节点记录了自己负责的槽,并且每个节点都会接收其他节点槽信息 (clusterNode.slots[]),还要再clusterState里维护一个slots[16384]的数组呢?
总结:clusterState.slots数组记录了集群中所有槽的指派信息,clusterNode.slots数组只记录本节点负 责哪些槽。
为什么Redis集群设计为16384个槽?
理论上CRC16算法可以得到2^16个数值,其数值范围在0-65535之间,也就是最多可以有65535个虚拟 槽,取模运算key的时候,应该是CRC(key)%65535;但是却设计为CRC(key)%16384,原因是作者在设 计的时候做了空间上的权衡,觉得节点最多不可能超过1000个,节点数量越多,节点间通信的成本越大 (节点间通信的消息体内容越大,具体是消息头中携带的其他节点信息越大),为了保证节点之间通信 效率,权衡之下所以采用了2^14个哈希槽;
Redis的慢查询日志功能用于记录执行时间超过给定时长的命令请求,用户可以通过这个功能产生的日志 来监视和优化查询速度。 服务器配置有2个相关的选项:
slowlog-log-slower-than 10000 //时间超过多久会被记录,时间单位微妙,1秒 = 1000 000微妙
slowlog-max-len 128 // 服务器最多保存多少条慢查询日志,如果满了,优先删除最旧的那条日志,再
添加新日志。底层用list实现
某个key缓存没有,数据库也没有。一般这种情况发生了用户恶意请求或者攻击。造成一直不停查库
解决方案
最顶层拦截,不合理的id直接打回去或者布隆过滤器 db如果差不多,设置个null进Redis,这样下次就不会打到db,但是要注意合理的过期时间。
大批量不同的key同一时间到期,造成缓存失效,请求压到数据库。
解决方案
没有很好的办法,设置key的时候注意错开过期时间,有些热点数据甚至可以设置不过期
某个key缓存过期,但是数据库有。正好这个点请求并发超高,还没来的及写缓存都压到了数据库上。
解决方案
加锁!第一个请求来时,如果发现为空,加锁住让后面的请求等待一下,参考多线程单例模式