Redis数据类型及实现算法


一、缓存

1.1 Redis的使用场景

减少DB交互: 数据库可通过读写分离,分库分表减轻DB压力。 将已经访问过的数据存储起来,再次访问返回缓存数据可以大量减少DB交付。数据库的数据是存在文件里,也就是硬盘, 会与内存做交换(swap)。高并发时会因为频繁IO导致无法响应,将数据存在Redis中也就是存在了内存中。内存天然支持高并发访问,可处理大量请求。

Session分离:集群分布式环境不同的tomcat管理各自的session。通过网络和IO进行session复制,影响系统性能。将登陆成功后的Session信息存放在Redis中,多个服务器(Tomcat)可以共享信息。

分布式锁: 多个进程(JVM)在并发时也会产生问题,也要控制时序性可以采用分布式锁。使用Redis实现setNX。

乐观锁:同步锁和数据库中的行锁、表锁都是悲观锁悲观锁的性能是比较低的,响应性比较差高性能、高响应(秒杀)采用乐观锁Redis可以实现乐观锁 watch + incr。

1.2 缓存的读写模式

Cache Aside Pattern(旁路缓存): 最经典的缓存+数据库读写模式,读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。更新的时候,先更新数据库,然后再删除缓存。

为什么是删除缓存而不更新?答:1. 缓存的值是一个结构:hash、list需要遍历会耗时。 2. 懒加载无法应用。

高并发脏读三种情况:①先更新数据库再更新缓存:update与commit之间,更新缓存,commit失败则DB与缓存数据不一致②先删除缓存再更新数据库:update与commit之间有新的读,缓存空,读DB数据到缓存,数据是旧的数据。commit后DB为新数据则DB与缓存数据不一致③先更新数据库再删除缓存(推荐):update与commit之间,有新的读,缓存空,读DB数据到缓存,数据是旧的数据。commit后DB为新数据,则DB与缓存数据不一致(可采用延时双删策略)。

1.3 缓存架构思路

1.多层次

2. 简单数据类型可采用Memcached, Value是hash、set、list、zset复杂数据类型需要存储关系、聚合、计算可采用Redis。

3. 要做集群, 哨兵+主从。

二、Redis数据类型与底层数据结构

2.1 数据类型

key的类型是字符串。value的数据类型有string、list、set、sortedset(zset)有序集合、hash类型, 不常见的bitmap位图类型, geo地址位置类型, stream类型。

String常见操作命令

set  赋值;get 取值; getset 取值并赋值; setnx 当value不存在时采用赋值;set NX PX 3000 原子操作,px 设置毫秒数;

append 向尾部追加值;  strlen  获取字符串长度;  incr 递增数字; incrby  增加指定的整数;decr 递减数字;decrby  减少指定的整数。

应用场景:1. incr用于乐观锁,递增数字可用于实现乐观锁watch(事务)。2. setnx用于分布式锁:举例:setnx name gxy //name不存在赋值, setnx name brook //再次赋值失败。

list常见操作命令

lpush key v1 v2 v3 ...从左侧插入列表;   lpop key 从列表左侧取出;  rpush key v1 v2 v3 ...从右侧插入列表;  rpop key 从列表右侧取出; lpushx key value将值插入到列表头部;

rpushx key value将值插入到列表尾部;  blpop key timeout从列表左侧取出,当列表为空时阻塞,可以设置最大阻塞时间,单位为秒;

blpop key timeout 从列表右侧取出,当列表为空时阻塞,可以设置最大阻塞时间,单位为秒;  llen key 获得列表中元素个数; lindex key index 获得列表中下标为index的元素 index从0开始;

lrange key start end 返回列表中指定区间的元素,区间通过start和end指定;  lrem key count value删除列表中与value相等的元素当count>0时, lrem会从列表左边开始删除;当count<0时,lrem会从列表后边开始删除;当count=0时, lrem删除所有值为value的元素;

lset key index value 将列表index位置的元素设置成value的值;    ltrim key start end对列表进行修剪,只保留start到end区间;     rpoplpush key1 key2 从key1列表右侧弹出并插入到key2列表左侧;

brpoplpush key1 key2从key1列表右侧弹出并插入到key2列表左侧,会阻塞;     linsert key BEFORE/AFTER pivot value 将value插入到列表,且位于值pivot之前或之后

应用场景:列表有序可以作为栈和队列使用, 可用于各种列表,比如用户列表、商品列表、评论列表等。

set常见操作命令

sadd key mem1 mem2 .... 为集合添加新成员;    srem key mem1 mem2 .... 删除集合中指定成员;       smembers key 获得集合中所有元素;     spop spop key 返回集合中一个随机元素,并将该元素删除;

srandmember key 返回集合中一个随机元素,不会删除该元素;     scard key 获得集合中元素的数量;      sismember key member 判断元素是否在集合内;           sinter key1 key2 key3 求多集合的交集;

sdiff key1 key2 key3 求多集合的差集;          sunion key1 key2 key3 求多集合的并集。

应用场景:适用于不能重复的且不需要顺序的数据结构  比如:关注的用户,还可以通过spop进行随机抽奖。

sortedset常见操作命令

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常见操作命令

hset key field value 赋值,不区别新增或修改;   hmset key field1 value1 field2 value2 批量赋值;    hsetnx key field value 赋值,如果filed存在则不操作

hexists key filed 查看某个field是否存在;     hget key field 获取一个字段值;    hmget key field1 field2 ... 获取多个字段值;   hdel key field1 field2... 删除指定字段;

hincrby key field increment 指定字段自增increment;   hlen key 获得字段数量

应用场景:对象的存储 ,表数据的映射。

2.2 底层数据结构

Redis作为Key-Value存储系统,数据结构如下:

Value是一个对象,包含字符串对象,列表对象,哈希对象,集合对象和有序集合对象。结构组成如下:

typedef struct redisObject {
  unsigned type:4;//类型 五种对象类型
  unsigned encoding:4;//编码
  void *ptr;//指向底层实现数据结构的指针
  //...
  int refcount;//引用计数
  //...
  unsigned lru:LRU_BITS; //LRU_BITS为24bit 记录最后一次被命令程序访问的时间
  //...
}robj;

Redis使用了SDS(Simple Dynamic String),用于存储字符串和整型数据。

struct sdshdr{
  //记录buf数组中已使用字节的数量
  int len;
  //记录 buf 数组中未使用字节的数量
  int free;
  //字符数组,用于保存字符串
  char buf[];
}

SDS的优势

1、SDS在C字符串的基础上加入了free和len字段,获取字符串长度:SDS 是 O(1),C 字符串是O(n)。buf数组的长度=free+len+1。

2、SDS 由于记录了长度,在可能造成缓冲区溢出时会自动重新分配内存,杜绝了缓冲区溢出。

3、C字符串结尾用'\0'表示,为二进制数据。SDS中\0不是二进制,所以可存取二进制数据,以字符串长度len来作为结束标识。

跳跃表是有序集合(sorted-set)的底层实现,效率高,实现简单。(资料来自)

优点:1.可以快速查找到需要的节点 2.可以在O(1)的时间复杂度下,快速获得跳跃表的头节点、尾结点、长度和高度。

跳表具有如下性质:

(1) 由很多层结构组成    (2) 每一层都是一个有序的链表   (3) 最底层(Level 1)的链表包含所有元素     (4) 如果一个元素出现在Level i的链表中,则它在Level i 之下的链表也都会出现。  (5) 每个节点包含两个指针,一个指向同一链表中的下一个元素,一个指向下面一层的元素。

跳跃表的搜索

例子:查找元素 117

(1) 比较21, 比21 大,往后面找   (2) 比较37,   比37大,比链表最大值大,从37的下面一层开始找   (3) 比较71,  比71 大,比链表最大值大,从71的下面一层开始找

(4) 比较85, 比85大,从后面找     (5) 比较117, 等于117, 找到了节点。

跳跃表的插入

先确定要插入的层, 相当与做一次丢硬币的实验,如果遇到正面,继续丢,遇到反面,则停止,用实验中丢硬币的次数 K 作为元素占有的层数。显然随机变量 K 满足参数为 p = 1/2 的几何分布,K 的期望值 E[K] = 1/p = 2. 就是说,各个元素的层数,期望值是2层。如果K大于链表的层数,则要添加新的层。例子:插入 119, K = 4:

跳跃表的删除

在各个层中找到包含x的节点,使用标准的 delete from list 方法删除该节点。

字典dict又称散列表(hash),  是用来存储键值对的一种数据结构。Redis整个数据库都是用字典来存储的,对RedisCURD操作就是对字典中的数据进行CURD操作。

数组是用来存储数据的容器。Hash函数可以把Redis里的key: 统一转换成整数, 数组下标=hash(key)%数组容量, 采用单链表在相同的下标位置处存储原始key和value。

字典扩容: ①初次申请容量为4个dictEntry, 非初次申请为当前hash表容量的一倍。②rehashidx=0表示要进行rehash操作。③新增加的数据在新的hash表h[1]。④修改、删除、查询在老hash表h[0]、新hash表h[1]中(rehash中)。⑤将老的hash表h[0]的数据重新计算索引值后全部迁移到新的hash表h[1]中,这个过程称为rehash。

渐进式rehash:  当数据量巨大时rehash过程缓慢。服务器忙时只对一个节点进行rehash, 服务器空闲时批量rehash(100节点)。

快速列表是Redis底层重要的数据结构,是list的底层实现。快速列表是一个双向链表,链表中的每个节点是一个ziplist结构。ziplist是一个字节数组,可以包含多个节点,每个节点可以保存一个字节数组或一个整数。

quicklist每个节点的实际数据存储结构为ziplist,这种结构的优势在于节省存储空间。为了进一步降低ziplist的存储空间,还可以对ziplist进行压缩。Redis采用的压缩算法是LZF。其基本思想是:数据与前面重复的记录重复位置及长度,不重复的记录原始数据。

三、缓存过期和淘汰策略

不设置maxmemory场景: Redis的key是固定的,不会增加,作为DB使用保证数据的完整性,不能淘汰可以做集群横向扩展。

设置的场景:作为缓存使用,key会不断增加。1个Redis实例,保证系统运行1G,剩下的3/4可设置为Redis最大内存。

定时删除: 在设置键的过期时间的同时,创建一个定时器, 让定时器在键的过期时间来临时,立即执行对键的删除操作。

惰性删除:在key被访问时如果发现它已经失效,那么就删除它。

LRU最近最少使用:实现算法1. 新数据插入到链表头部;2.每当缓存命中,将数据移到链表头部;3. 当链表满的时候,将链表尾部的数据丢弃;4.在Java中可以使用LinkHashMap去实现LRU。

volatile-lru:  从已设置过期时间的数据集中挑选最近最少使用的数据淘汰。(比allkeys-lru性能差,因为要存过期时间)

allkeys-lru:从数据集中挑选最近最少使用的数据淘汰。(不确定时采用此策略)

volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰。