缓存原理和设计
缓存的基本思想
什么是缓存
缓存原来是指CPU上的一种告诉存储器,它先于内存与CPU 交换数据,速度很快,现在指存在计算机上的原始数据的复制集,便于快速访问。 这是以空间换时间的方法
缓存的使用场景
DB缓存,减轻DB服务器压力
一般情况下数据存在数据库中,应用程序直接操作数据库,当访问量上万,数据库压力增大,可以采取的方法是: 读写分离,分库分表,当访问量达到10万,百万,需要引入缓存,将已经访问过的内容或数据存储起来,当再次访问时,先访问缓存,缓存命中返回数据,不命中再找数据库,并回填到缓存。
提高系统响应
数据库的数据是存在文件中,在大量瞬时访问呢时,MySQL单机会因为频繁IO而造成无法响应,MySQL 的InnoDB是有行锁的 将数据缓存在Redis,也就是存在内存中。
做Session 分离
传统的session 是由 tomcat 自己进行维护和管理,集群或分布式环境,不同的tomcat管理各自的session ,只能在各个tomcat之间,通过网络和IO进行session的赋值,极大影响了系统性能。 1. 各个tomcat间赋值session,性能损耗,2. 不能保证各个tomcat的isesson 数据同步。
Redis基础
Redis(remote dictionary server)远程字典服务器是用C开发的一个开源的高性能键值对(K-V) 内存数据库,提供了5种数据类型来存储,字符串,散列,列表,集合,有序集合类型。是一种NoSQL 数据存储。 不常见是bitmap 位图类型 geo 类型,5.0 新增stream 类型
Redis的key设计
- 用:分割
- 把表名转换为key前缀,比如: user:
- 第二段放置主键值
- 第三段放置列名 比如: user:9:username 表示明确,看key知道意思,不易被覆盖
String 字符串类型
Redis 的string 能表达3种值的类型:字符串,证书,浮点数100.01 是个六位的字符串 常见的操作命令: set :set key value 赋值 get: get key 取值 getset : getset key value 取值并赋值 setnx :setnx key value 当value 不存在时采用赋值,set key value NX PX 300 原子操作,px 设置毫秒数 append : append key value 向尾部追加值 strlen: strlen key 获取字符串长度 incr : incr key 递增数字 incrby:incry key increment 增加指定的整数 decr: descr key 递减数字 decrby: decrby key decrement 减少指定的整数
应用场景
- key和命令是字符串
- 普通的赋值
- incr用于乐观锁, incr递增数字,可以用于实现乐观锁watch(事务)
- setnx用于分布式锁,当value不存在时采用赋值,可用于实现分布式锁
list 列表类型
list列表类型可以存储有序,可重复的元素,获取头部或者尾部附近的记录是鸡块的额,list的元素个数最多为2^32-1个40亿 常见操作命令:
lpush: lpush key v1 v2 v3 从左侧插入列表
lpop: lpop key 从列表左侧取出
rpush: rpush key v1 v2 从右侧插入列表
rpop: rpop key 从列表右侧取出
lpushx : lpushx key value 将值插入列表头部
rpushx : rpushx key value 将值插入列表尾部
blpop: blpop key timeout 将列表左侧取出,当列表为空时阻塞,可以设置最大阻塞时间,单位为秒
brpop: brpop key timeout 从列表右侧取出,当列表为空时阻塞,可以设置最大阻塞时间,单位为秒
llen : llen key 获取列表中元素个数
lindex: lindex key index ,获取列表下标为 index的元素
lrange: lrange key start end,返回列表中指定区间的元素,区间通过start 和end 指定
lrem: lrem key count value 删除列表中与value 相等的元素,当count >0 时,lrem 会从列表左边开始删除,当count<0时,lrem会从列表后边开始删除,当count=0时,lrem 删除所有值为value的元素
lset : lset key index value 将列表 index 位置的元素设置value 的值
ltrim: ltrim key start end 对列表进行修剪,只保留start 到end 区间
rpoplpush : rpoplpush key1 key2 从key1 列表右侧弹出并插入到key2列表左侧
brpoplpush: brpoplpush key1 key2 从key1 列表右侧弹出并插入到key2列表左侧 会阻塞
linsert linsert key before/after pivot value 将value 插入到列表,且位于pivot 之前或之后
应用场景:
作为栈或者队列使用,列表有序可以作为栈和队列使用
可以用于各种列表,比如用户列表,商品列表,评论列表等
Set 集合类型
Set:无序,唯一元素,集合中最大成员数为2^32 -1
sadd key mem1 mem2 为集合添加新成员
srem key mem1 mem2 删除集合中指定成员
smembers key 获取集合中所有元素
spop key 返回集合中一个随机元素,并将该元素删除
srandmember key 返回集合中一个随机元素,不会删除该元素
scard key 获取集合中元素的数量
sismember key member 判断元素是否在集合内
sinter key1 key2 key3 求多集合的交集
sdiff key1 key2 key3 求多集合的差集
sunion key1 key2 key3 求多集合的并集
应用场景
适用于不能重复的且不需要排序的数据结构,类似的有关注的用户,还可以通过spop 进行随机抽奖
sortedset 有序集合类型
SortedSet(ZSet)有序集合:元素本身是无序不重复的,每个元素关联一个分数,可以按分数排序,分数可以重复
zadd key score1 member1 score2 member2 为有序集合添加新成员
zrem key mem1 mem2 删除有序集合中指定成员
zcard key 获得有序集合中的元素数量
zcount key min max 返回集合中score 指在[min,max] 区间的元素数量
zincrby key increment member 在集合的member 分值上加 increment
zscore key member 获得集合中member的分值
zrank key member 获取集合中member的排名(按分值从小到大)
zrevrank key member 获取集合中member的排名(按分值从大到小)
zrange key start end 获取集合中指定区间成员,按分数递增排序
zrevrange key start end 获取集合中指定区间成员,按分数递减排序
应用场景
可以按照分值排序,所以适用于各种排行榜,类似,点击排行榜,销量排行,关注排行等。
Hash 类型(散列表)
Redis hash 是一个String 类型的field和value的映射表,它提供了字段和字段值的映射,每个hash 可以存储2^32-1键值对
hset key field value 赋值,不区别新增或修改
hmset field value1 field value2 批量复制
hsetnx key field value ,赋值 ,如果field 存在则不操作
hexists key field 查看某个field 是否存在
hget key field 获取一个字段值
hmget key field1 field2 获取多个字段值
hgetall key 获取所有的字段值
hdel key field1 field2 删除指定字段
ihncrby key field increment 指定字段自增increment
hlen key 获得字段数量
应用场景:
对象的存储,表数据的映射
bitmap 位类型
bitmap 是进行位操作的,通过一个bit位来表示某个元素对应的值或者状态,其中key就是对应元素本身,bitmap本身会极大的节省存储空间。
setbit key offset value 设置key在offset处的bit值(0或者1)
getbit key offset 获取key在offset处的bit值
bitcount key 获取key的bit位为1的个数
bitpos key value 返回第一个被设置为bit值的索引值
bitop and[or/xor/not] destkey key [key ...] 对多个key 进行逻辑运算后存入destkey 中
应用场景
- 每户每月签到,用户id 为key,日期作为偏移量 1表示签到
- 统计活跃用户,日期为key,用户id为偏移量 1表示活跃
- 查询用户在线状态,日期为key 用户id 为偏移量 1表示在在线
geo 地理位置类型
geo 是 redis用来处理位置信息的,在3.2 版本中使用,主要利用了Z阶曲线,Base32编码和geohash算法
z阶曲线
在x轴 和y轴上将10进制数转化为二进制数,采用x轴和y轴对应的二进制数一次交叉后得到一个六位数编码,把数字从小到大 依次连起来的曲线成为z阶曲线
Base32 编码
Base32这种数据编码机制,主要用来把二进制数据编码成可见的字符串,编码规则是:任意给定一个二进制数据,以5个位(bit)为 一组进行切分(base64以6个位(bit) 为一组),对切分而成的每个组进行编码得到一个可见字符,Base32编码表字符集中的字符总数为32个 (0-9,b-z 去掉 a,i,l,o),这也是Base32名字的由来。
geohash 算法
Geohash 是一个地理位置信息编码方法,经过geohash 映射后,将任意位置的经纬度坐标可以表示成一个较短的字符串,可以方便存储在数据库中。 Redis 中经纬度使用52位的整数进行编码,放进zset中,zset的元素是key,score是GeoHash 的52位整数值,在使用Redis 进行Geo查询时,内部对应的操作其实是 zset(skiplist)操作,通过zset的score进行排序就可以得到坐标附近的其他元素,通过将score还原成坐标值就可以得到元素的原始坐标
geoadd key 经度 维度 成员名称 经度1 维度1 成员名称1 经度2 维度2 成员名称2 添加地理坐标
geohash key 成员名称1 成员名称2 返回标准geohash 串
geopos key 成员名称1 成员名称2 返回成员经纬度
geolist key 成员1 成员2 单位 计算成员间距离
georadiusbymember key 成员 值单位 count 数 asc[desc] 根据成员查找附近的成员
应用场景
- 记录地理位置
- 计算距离
- 查找附近的人
stream数据流类型
Stream 是redis 5.0 后新增的数据结构,用于可持久化的消息队列。几乎满足了消息队列具备的全部内容
- 消息ID的序列化生成
- 消息遍历
- 消息的阻塞和非阻塞读取
- 消息的分组消费
- 未完成消息的处理
- 消息队列监控
每个stream 都有唯一的名称,Redis的key ,首次使用xadd 指令追加消息时自动创建。
xadd key id <*> field1 value1 将制定消息数据追加到指定队列(key) 中,* 表示最新生成的id(当前时间+序列号)
xread [COUNT count][BLOCK milliseconds] STREAMS key [key ...] ID [ID ...] 从消息队列中读取,COUNT:读取条数,BLOCK:阻塞读(默认不阻塞)key:队列名称,id:消息id
xrange key start end [COUNT] 读取队列中给定ID范围的消息 count :返回消息条数(消息id 从小到大)
xrevrange key start end [COUNT] 读取队列中给定ID范围的消息 count: 返回消息条数(消息id从大到小)
xdel key id 删除队列的消息
xgroup create key groupname id 创建一个新的消费组
xgroup destory key groupname 删除指定消费组
xgroup delconsumer key groupname cname 删除指定消费组的某个消费者
xgroup setid key id 删除指定消息的最大id
xreadgroup group groupname consumer COUNT streams key 从队列中的消费组中创还能消费者并消费数据(consumer 不存在则创建)
应用场景:消息队列的使用
缓存过期和淘汰策略
Redis 性能高,官方数据,读110000次/s 写 81000次/s ,长期使用,key会不断增加,redis 做为缓存使用物理内存也会满,内存与硬盘交换虚拟内存,频繁IO性能急剧下降。
maxmemory
不设置的场景
redis 的key 是固定的,不会增加
redis 作为DB使用,保证数据的完整性,不能淘汰 ,可以做集群,横向拓展
缓存淘汰策略,禁止驱逐(默认)
设置的场景
Redis 是作为缓存使用,不断增加key
maxmemory:默认为0,不限制
但是超过物理内存后性能急剧下架,可能会崩溃
不设置maxmemory 无最大设置,maxmemory-policy noeviction 禁止驱逐不淘汰,设置maxmemory 要配置淘汰缓存策略
expire数据结构
使用expire 设置一个键的存活时间ttl 过来这段时间,该键被自动删除
expire 原理
typedef struct redisDb{
dict *dict; // key 和value
dict *expires; //key 和 ttl
dict *blocking_keys;
dict *ready_keys;
dict *watched_keys;
int id;
} redisDb;
除了id以外都是指向字典的指针,dict 用来维护一个redis数据库中包含所有的key-value 键值对,expires 用于维护一个redis数据库中 设置了失效时间的键,当我们使用expire 命令设置一个key的失效时间时,首先到dict这个字典表中查找要设置的key是否存在,如果存在 就讲这个key和失效时间添加到expires这个字典表中,我们使用setex命令向系统插入数据中,redis 首先讲key和value天假到dict这个字典表中。 然后将key和失效时间添加到expires 这个字典表中,总的来说,失效时间key和具体失效时间全部都维护在expires这个字典表中。
删除策略
redis 删除策略分为3种: 定时删除,惰性删除和主动删除三种方式。目前采用惰性珊瑚和主动删除的方式。
定时删除
在设置键的过期时间的同时,创建一个定时器,让定时器在键的过期时间来临时,立即执行对键的删除操作,但是需要创建定时器,消耗cpu
惰性删除
在key 被访问时候如果发现它已经失效,就删除它。 调用expirefNeeded函数,就是读取数据之前先检查一下它有没有失效,如果失效就删除
int expireIfNeeded(redisDb *db, robj *key)
{
//获取主键失效时间
long long when = getExpire(db,key)
//如果失效时间为负数,表明该主键未设置失效时间(-1) 直接返回0
if(when <0) return 0;
//如果redis 服务器正在从RDB文件中加载数据,暂时不进行失效逐渐的删除,直接返回0
if(server.loading ) return 0;
//如果上述条件都不满足,就讲主键的失效时间和当前时间进行比较,如果发现指定主键还未失效直接返回0
if(mstime() <= when) return 0;
//如果发现主键确实已经失效,那么首先更新关于失效主键的统计个数,然后将该主键失效的信息进行广播,最后将
//主键从数据库中删除
server.stat_expiredkeys++;
propagateExpire(db,key);
return dbDelete(db,key);
}
主动删除
在redis.conf 文件中可以配置主动删除策略,默认是no-enviction(不删除) maxmemory-polict allkeys-lru
LRU
最近最少使用,详细算法如下:
- 新数据插入链表尾部;
- 每次缓存命中,就将数据移动链表头部
- 当链表满的时候,将链表的尾部数据丢弃
- 在java 中可以使用LinkHashMap (哈希链表) 实现LRU
redis LRU 数据淘汰机制
- 在服务器配置中保存了lru计数器server.lrulock,会定时更新,server.lrulock的值是根据server.unixtime计算出来的
- 从 struct redisObject 中可以发现,每一个Redis对象都会设置相应的lru,每一次访问数据的时候,会更新redisObject.lru.
- 在数据集中随机挑选几个键值对,取出其中lru最大的键值对淘汰
- 不可能遍历key,用当前时间-最近访问 越大 说明 访问间隔时间越长
- volatile-lru 从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
- allkeys-lru 从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰。
LFU
LFU(least frequently used) 最不经常使用,如果一个数据在最近一段时间内使用次数很少,那么在将来的一段时间内被使用的可能性也很小
random
随机删除 volatile-random 从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰, allkeys-random 从数据集(server.db[i].dict)中任意选择数据淘汰
ttl
volatile-ttl 从已设置过期时间的数据集(sever.db[i].expires)中选择将要过期的数据淘汰,Redis数据集数据结构中保存了键值对对期时间的表,即 redisDb.expires. TTL数据淘汰机制,从过期时间表中随机挑选几个键值对,取出其中ttl最小的键值对淘汰。
noenviction
禁止驱逐数据,不删除 默认
缓存淘汰策略的选择
- allkeys-lru :一般选择 冷热数据交换
- volatile-lru : 比 allkeys-lru 性能差,需要存:过期时间
- allkeys-random:希望请求符合平均分布(每个元素以相同的概率被访问)
- 自己控制: volatile-ttl 防止缓存穿透