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

Redis五种基本数据类型的用法和常见命令的使用

Redis数据类型和应用场景

Redis是一个Key-Value的存储系统,使用ANSI C语言编写。

key的类型是字符串。

value的数据类型有:

常用的:string字符串类型、list列表类型、set集合类型、sortedset(zset)有序集合类型、hash类型。

不常见的:bitmap位图类型、geo地理位置类型。

Redis5.0新增一种:stream类型

注意:Redis中命令是忽略大小写,(set SET),key是不忽略大小写的 (NAME name)

Redis的Key的设计

  1. 用:分割
  2. 把表名转换为key前缀, 比如: user:
  3. 第二段放置主键值
  4. 第三段放置列名

比如:用户表user, 转换为redis的key-value存储

username 的 key: user:9:username

{userid:9,username:zhangf}

email的key user:9:email

表示明确:看key知道意思

不易被覆盖

string字符串类型

Redis的String能表达3种值的类型:字符串、整数、浮点数 100.01 是个六位的串

应用场景:

1、key和命令是字符串

2、普通的赋值

3、incr用于乐观锁

incr:递增数字,可用于实现乐观锁 watch(事务)

4、setnx用于分布式锁

当value不存在时采用赋值,可用于实现分布式锁

list列表类型

应用场景:

1、作为栈或队列使用

列表有序可以作为栈和队列使用

2、可用于各种列表,比如用户列表、商品列表、评论列表等。

set集合类型

Set:无序、唯一元素

集合中最大的成员数为 2^32 - 1

应用场景:

适用于不能重复的且不需要顺序的数据结构

比如:关注的用户,还可以通过spop进行随机抽奖

sortedset有序集合类型

SortedSet(ZSet) 有序集合: 元素本身是无序不重复的

每个元素关联一个分数(score) 可按分数排序,分数可重复

应用场景:

由于可以按照分值排序,所以适用于各种排行榜。比如:点击排行榜、销量排行榜、关注排行榜等。

hash类型(散列表)

Redis hash 是一个 string 类型的 field 和 value 的映射表,它提供了字段和字段值的映射。
每个 hash 可以存储 2^32 - 1 键值对(40多亿)。

应用场景:

对象的存储 ,表数据的映射

bitmap位图类型

bitmap是进行位操作的

通过一个bit位来表示某个元素对应的值或者状态,其中的key就是对应元素本身。

bitmap本身会极大的节省储存空间。

应用场景:

1、用户每月签到,用户id为key , 日期作为偏移量 1表示签到

2、统计活跃用户, 日期为key,用户id为偏移量 1表示活跃

3、查询用户在线状态, 日期为key,用户id为偏移量 1表示在线

geo地理位置类型

应用场景:

1、记录地理位置

2、计算距离

3、查找”附近的人”

stream数据流类型

stream是Redis5.0后新增的数据结构,用于可持久化的消息队列。几乎满足了消息队列具备的全部内容,包括:

消息ID的序列化生成

消息遍历

消息的阻塞和非阻塞读取

消息的分组消费

未完成消息的处理

消息队列监控

每个Stream都有唯一的名称,它就是Redis的key,首次使用 xadd 指令追加消息时自动创建。

底层数据结构

Redis没有表的概念,Redis实例所对应的db以编号区分,db本身就是key的命名空间。
比如:user:1000作为key值,表示在user这个命名空间下id为1000的元素,类似于user表的id=1000的行。

RedisDB结构

Redis中存在“数据库”的概念,该结构由redis.h中的redisDb定义。

当redis 服务器初始化时,会预先分配 16 个数据库

所有数据库保存到结构 redisServer 的一个成员 redisServer.db 数组中

redisClient中存在一个名叫db的指针指向当前使用的数据库

1
2
3
4
5
6
7
8
9
10
typedef struct redisDb {
int id; //id是数据库序号,为0-15(默认Redis有16个数据库)
long avg_ttl; //存储的数据库对象的平均ttl(time to live),用于统计
dict *dict; //存储数据库所有的key-value
dict *expires; //存储key的过期时间
dict *blocking_keys;//blpop 存储阻塞key和客户端对象
dict *ready_keys;//阻塞后push 响应阻塞客户端 存储阻塞后push的key和客户端对象
dict *watched_keys;//存储watch监控的的key和客户端对象
} redisDb;

RedisObject结构

Value是一个对象

包含字符串对象,列表对象,哈希对象,集合对象和有序集合对象

结构信息概览

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

  • 4位type
  • 4位encodin
  • 24位LRU

lru 记录的是对象最后一次被命令程序访问的时间,( 4.0 版本占 24 位,2.6 版本占 22 位)。

高16位存储一个分钟数级别的时间戳,低8位存储访问计数(lfu : 最近访问次数)

lru—-> 高16位: 最后被访问的时间

lfu—–>低8位:最近访问次数

  • refcount

refcount 记录的是该对象被引用的次数,类型为整型。

refcount 的作用,主要在于对象的引用计数和内存回收。

当对象的refcount>1时,称为共享对象

Redis 为了节省内存,当有一些对象重复出现时,新的程序不会创建新的对象,而是仍然使用原来的对象。

  • ptr

ptr 指针指向具体的数据,比如:set hello world,ptr 指向包含字符串 world 的 SDS。

type

  • 字符串对象
  • 跳跃表

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

跳跃表的基本思想:

将有序链表中的部分节点分层,每一层都是一个有序链表。

  • 数组
  • Hash表
  • dict字典
  • 压缩列表
  • 快速列表
  • 双向列表

缓存过期和淘汰策略

Redis性能高:

官方数据

读:110000次/s

写:81000次/s

长期使用,key会不断增加,Redis作为缓存使用,物理内存也会满

内存与硬盘交换(swap) 虚拟内存 ,频繁IO 性能急剧下降

maxmemory

不设置的场景

Redis的key是固定的,不会增加

Redis作为DB使用,保证数据的完整性,不能淘汰 , 可以做集群,横向扩展

缓存淘汰策略:禁止驱逐 (默认)

设置的场景

Redis是作为缓存使用,不断增加Key

maxmemory : 默认为0 不限制

问题:达到物理内存后性能急剧下架,甚至崩溃

内存与硬盘交换(swap) 虚拟内存 ,频繁IO 性能急剧下降

设置多少与业务有关

1个Redis实例,保证系统运行 1 G ,剩下的就都可以设置Redis

一般情况设置为物理内存的3/4

slaver : 留出一定的内存

在redis.conf中 : maxmemory 1024mb

命令: 获得maxmemory数

1
CONFIG GET maxmemory

设置maxmemory后,当趋近maxmemory时,通过缓存淘汰策略,从内存中删除对象

不设置maxmemory 无最大内存限制 maxmemory-policy noeviction (禁止驱逐) 不淘汰

设置maxmemory maxmemory-policy 要配置

expire

在Redis中可以使用expire命令设置一个键的存活时间(ttl: time to live),过了这段时间,该键就会自动被删除。

删除策略

Redis的数据删除有定时删除、惰性删除和主动删除三种方式。

Redis目前采用惰性删除+主动删除的方式。

  • 定时删除

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

需要创建定时器,而且消耗CPU,一般不推荐使用。

  • 惰性删除

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

调用expireIfNeeded函数,该函数的意义是:读取数据之前先检查一下它有没有失效,如果失效了就删除它。

  • 主动删除

在redis.conf文件中可以配置主动删除策略,默认是no-enviction(不删除)

  • LRU

LRU (Least recently used) 最近最少使用,算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。

最常见的实现是使用一个链表保存缓存数据,详细算法实现如下:

  1. 新数据插入到链表头部;
  2. 每当缓存命中(即缓存数据被访问),则将数据移到链表头部;
  3. 当链表满的时候,将链表尾部的数据丢弃。
  4. 在Java中可以使用LinkHashMap(哈希链表)去实现LRU

Redis的LRU 数据淘汰机制

在服务器配置中保存了 lru 计数器 server.lrulock,会定时(redis 定时程序 serverCorn())更新,server.lrulock 的值是根据 server.unixtime 计算出来的。

另外,从 struct redisObject 中可以发现,每一个 redis 对象都会设置相应的 lru。可以想象的是,每一次访问数据的时候,会更新 redisObject.lru。

LRU 数据淘汰机制:在数据集中随机挑选几个键值对,取出其中 lru 最大的键值对淘汰。
不可能遍历key 用当前时间-最近访问 越大 说明 访问间隔时间越长

volatile-lru

从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰

allkeys-lru

从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰

  • LFU

LFU (Least frequently used) 最不经常使用,如果一个数据在最近一段时间内使用次数很少,那么在将来一段时间内被使用的可能性也很小。

  • 随机

volatile-random

从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰

allkeys-random

从数据集(server.db[i].dict)中任意选择数据淘汰

  • ttl

volatile-ttl

从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰

redis 数据集数据结构中保存了键值对过期时间的表,即 redisDb.expires。

TTL 数据淘汰机制:从过期时间的表中随机挑选几个键值对,取出其中 ttl 最小的键值对淘汰。

  • noenviction

禁止驱逐数据,不删除 默认

缓存淘汰策略的选择

allkeys-lru : 在不确定时一般采用策略。 冷热数据交换

volatile-lru : 比allkeys-lru性能差 存 : 过期时间

allkeys-random : 希望请求符合平均分布(每个元素以相同的概率被访问)

案例分享:字典库失效

key-Value 业务表存 code 显示 文字

将字典库保存到redis中,设置了maxmemory,并设置缓存淘汰策略为allkeys-lru
结果造成字典库某些字段失效,缓存击穿 , DB压力剧增,差点宕机。

分析:

字典库 : Redis做DB使用,要保证数据的完整性

maxmemory设置较小,采用allkeys-lru,会对没有经常访问的字典库随机淘汰

当再次访问时会缓存击穿,请求会打到DB上。

解决方案:

1、不设置maxmemory

2、使用noenviction策略

Redis是作为DB使用的,要保证数据的完整性,所以不能删除数据。

可以将原始数据源(XML)在系统启动时一次性加载到Redis中。

Redis做主从+哨兵 保证高可用。