纵观现在的Java面试,Redis成了必问的点,为什么Redis能这样得到大家的青睐?Redis到底帮我们解决了什么问题提?Redis的优点是有哪些?…
概览
Redis官网这样描述自己:开源的内存型数据结构化存储,用作数据库、缓存和消息处理。Redis提供了string,hashes,lists,sets,sorted sets(提供范围查询),bitmaps,hyperloglogs,geospatial以及streams数据结构…
Redis(Remote Dictionary Service)最初是为了自己的网站进行数据统计而产生的,由意大利人Antirez发明,经过多年的开源发展,现在已经相当成熟,现被广泛应用于缓存中间件,而且能解决大部分的查询压力,因此在多数企业的应用中均被广泛使用。
Redis作为一个突出的非关系型数据库,具备哪些关系型数据库不具备的优势呢?
关系型数据库:
- 数据采用行模式进行存储,可以理解成Excel的行和列
- 存储结构化的数据,如数据表之间有一定的关联关系(schema)
- 支持使用SQL(结构化查询语言)进行操作,能够支持复杂的语句操作
- 通过支持事务来提供严格或者实时的数据一致性
在关系型数据库中,当我们的数据逐渐变大,这时候势必要进行扩容,但是扩容只能向上进行垂直扩容,而且不能动态扩容;在开发过程中,如果想要修改某个字段,则该字段下的所有数据均会受到影响;最重要的一点,在高并发的情况下,关系型数据库所能承受的压力不是很乐观。对于磁盘的I/O出现瓶颈。
而对于非关系型数据库,能够存储非结构化的的数据,比如文本、图片、视频、音频等,表和表之间;没有关联,非常易于扩展;保证数据的最终一致性,遵循BASE理论;支持分布式,能够对数据进行分片存储,容量扩展简单;最重要的一点,能够支持海量数据的存储和高并发的高效读写。笔者测试使用Pipeline
对100万
个数字进行set和get,电脑应用水平处于常规工作环境,耗时在1500ms左右,可见效率还是相当高的。
public class PipelineSet {
public static void main(String[] args) {
Jedis jedis = new Jedis("192.168.0.109", 6379);
Pipeline pipelined = jedis.pipelined();
long start = System.currentTimeMillis();
for (int i=0; i < 1000000; i++) {
pipelined.set("pipeline_btch_"+i,""+i);
}
pipelined.syncAndReturnAll();
long end= System.currentTimeMillis();
System.out.println("pipeline耗时:"+(t2-t1)+"ms");
}
}
public class PipelineGet {
public static void main(String[] args) {
new Thread(){
public void run(){
Jedis jedis = new Jedis("192.168.0.109", 6379);
Set<String> keys = jedis.keys("pipeline_btch_*");
List<Object> result = new ArrayList();
Pipeline pipelined = jedis.pipelined();
long start= System.currentTimeMillis();
for (String key : keys) {
pipelined.get(key);
}
result = pipelined.syncAndReturnAll();
for (Object r: result) {
//System.out.println(r);
}
System.out.println("Pipeline get耗时:"+(System.currentTimeMillis() - start));
}
}.start();
}
}
常见的NoSQL
目前常见的NoSQL主要包含:
- KV存储类型:Redis,Memcached
- 文档类型:MongoDB
- 列存储:HBase
- 图存储:Neo4j
- …
Redis脱颖而出是因为其读写速度、跨语言、支持持久化、支持淘汰等,还提供了丰富的功能,比如事务、发布订阅、pipeline、lua脚本、集群和分布式的支持等
Redis常用数据类型
string类型
string的API
命令 | 语法 | 用途 | 示例 |
---|---|---|---|
set | set 键 值 | 保存一个字符串类型的键值对,键存在则覆盖值 | set name jack |
get | get 键 | 通过键取出对应的值,如果键不存在则返回null | get name 结果:jack |
getset | getset 键 值 | 先取出键对应的值,然后修改值 | getset name rose 结果:jack 被修改为rose |
incr | incr 键 | 将该键对应的值自增1 | incr age 结果:age自增1 |
decr | decr 键 | 将该键对应的值自减1 | decr age 结果:age自减1 |
incrby | incrby 键 自增值 | 将该键对应的值自增指定数值 | incrby age 10 结果:age自增10 |
decrby | decrby 键 自增值 | 将该键对应的值自减指定数值 | decrby age 10 结果:age自减10 |
append | append 键 值 | 如果该键存在,则在该值得基础上追加一段字符串,如果不存在该键则新增一个键值对 | name原来的值rose,append name you jump 结果:rose you jump |
setnx | setnx 键 值 | 当该键存在时,不会进行写操作,返回值为0,如果该键不存在时,会进行写操作,返回值为1 |
ypedef struct dictEntry {
void *key;
// union表示共用体,使用同一段内存,修改了都会修改
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next;
} dictEntry;
string的数据结构
key以字符串的形式进行存储到SDS中,由*key直接指向;而value则会采用多种SDS进行存储,使用redisObject
进行同一管理,这点类似于Java中使用接口统一规范多个实现的思想。
string通过dictEntry实现,在dictEntry中,封装有*value指针
、*key指针
、以及指向下一个节点的指针*next
。dictEntry从命名可以看出这是一个类似于索引的结构,并没有真正存储键和值的内容,而是将键和值得内容放到了另外的存储结构中,指针直接或间接指向这个存储结构。这个存储结构就是Redis自己实现的SDS
。
在redisObject中,包含的属性含义如下:
type
:对象的类型,包括OBJ_STRING
,OBJ_LIST
,OBJ_HASH
,OBJ_SET
,OBJ_ZSET
encoding
:具体的数据结构,如 int
,embstr
,raw
lru:LRU_BITS
:共24bit,对象最后一次被访问的时间,与key淘汰
有关
refcount
:引用技术。为0的时候,表示已经不被任何对象使用,可以进行回收
*ptr
:指向对象实际存储结构的指针
而在数据结构中:
int:存储8个字节的长整型
embstr:存储小于44个字节的字符串
raw:存储大于44个字节的字符串
Redis内部对于存储结构进行了自动优化:
embstr和raw的区别是:embstr只会分配一次内存空间,即redisObject和SDS是存在一段内存连续的空间里。而raw需要分配两次内存空间,redisObject和SDS分别存储在两段不连续的内存空间内。
设置的三个key都是字符串,但是三个value的数据结构类型却不一样。如果我们对num进行追加内容,再次查看encoding会发现变化了(读者可以自行尝试,此处不再贴出验证结果)。但是可以知道的是:
当int数据不在是整数,就会转为raw 当int数据的大小超过long的范围(2^63-1),就会转为embstr,embstr长度超过了44字节转为raw,编码转换在Redis写入数据时完成,转换过程不可逆,只能从小内存编码转向大内存编码(set属于重新设置,不在该条规则的讨论范围)
。
Redis为什么要使用SDS实现字符串的存储呢?
SDS(Simple Dynamic String):简单的动态字符串。使用char数组实现,l
/* Note: sdshdr5 is never used, we just access the flags byte directly.
* However is here to document the layout of type 5 SDS strings. */
struct __attribute__ ((__packed__)) sdshdr5 {
unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; /* used */
uint8_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
uint16_t len; /* used */
uint16_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
uint32_t len; /* used */
uint32_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
uint64_t len; /* used */
uint64_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
其中len表示数组的长度,alloc表示分配的内存大小。而5 8 16 32 64指的是结构能存储的容量。如sdshdr32表示能存储2^32个字节,等于4GB。
SDS的诞生只为解决传统C语言的以下问题:
- 内存空间预先会分配好
- 获取字符串的长度的时间复杂度是O(n)
- 长度变更引起的内存重新分配
- 用’\0’来判断字符串的结束(存储链接的时候很可能会碰到\0,但是不应该结束)。
string的应用场景
1.用作缓存:标准的key-value用法,可以假想为一个全局(单机)或者多台电脑公用(分布式)的HashMap
。
2. 分布式session:将sessionId保存在Redis中,解决Session不能共享的问题。
3. 分布式锁:setnx命令的使用。
4. 分布式全局ID:使用incr命令实现。
5. 计数器:使用incr命令实现。
6. 限流:使用incr实现。
7. …
hashes类型
hashes是 key field value结构的存储方式,元素是无序且不能重复的,基于hash表实现,最大可以存储2^32(40亿左右)的数据。
student age 5
student name tom
了解hash就知道,无论key是怎样的数据,经过hash函数后,产出的就是一个数字,因此形成映射关系会比直接存储节省更多空间。
hashes相对于传统的hash来说,增加了field作为第一层key,形成两层key来确定一个value,无疑增加了key的数量,但是能大大减少key的冲突(不同field下的key可以重复),这是一种空间换时间的权衡。
hashes的缺点是不能单独对field设置过期时间,需要考虑数据量分布的问题。不能对field进行拆分存储,且field挂掉,则其下的所有key也直接丢失。
hashes的API
命令 | 语法 | 用途 | 示例 |
---|---|---|---|
hset | hset 键 | 为指定的键所对应的hash数据中的某个属性赋值 | hset student name jack |
hmset | hset 键 | 向指定的键所对应的hash数据中一次保存一个或者多个键值对数据 | |
hget | hget 键 | 查询指定键对应的hash数据中某个属性的值 | hget student name,结果:jack |
hmget | hmget 键 | 查询指定键对应的hash数据中一个或多个键值对的值 | |
hdel | hdel 键 | 删除指定键对应的hash数据中的一个或者多个属性 | |
lrange | lrange names 0 10 | 结果是names对应的集合中0-10之间的数据,如果数据长度不足,则返回已有的数据,不会出现异常 | |
hgetall | hgetall 键 | 返回指定键对应的hash数据的全部属性和值 | hgetall student |
hexists | hexists 键 属性 | 判断指定键中是否存在某个属性,返回1表示存在,0表示不存在 | hexists student name,结果:1 |
hlen | hlen 键 | 返回指定键对应的hash数据的属性个数 | hlen student |
hkeys | hkeys 键 | 返回指定键对应的hash数据中的所有属性名称 | hkeys student |
hvals | hvals 键 | 返回指定键对应的hash数据中的所有属性值 | |
hincrby | hincrby 键 | 将指定键对应的属性值自增增量的值 | hincrby student age 10 age的值在原本的基础上+10 |
hashes的数据结构
- ziplist–OBJ_ENCODING_ZIPLIST(压缩列表):是一个经过特殊编码的连续内存块组成的双向链表。内部不存储指向上一个链表节点和指向下一个链表节点的指针,而是存储上一个节点长度和当前节点长度。
基于ziplist的结构,一般在一个hash对象保存的field数量<512个、一个对象中所有的field和value的字符串长度都<64byte时使用ziplist。
typedef struct dictht {
dictEntry **table;//
unsigned long size;
unsigned long sizemask;
unsigned long used;
} dictht;
- hashtable–OBJ_ENCODING_HT(哈希表):
将dictEctry封装待dictht中,多出来的dictht是用来在扩容的时候暂存的
比如把a,b的值进行互换,我们会先把a的值放到c里面,再把b的值放到a里面,接着把c的值(实际存的a)放到b里面。此时的c就是起到暂存的作用。
dictEntry计算hash值的时候出现hash碰撞,则采用HashMap里面的处理方法,用链表来解决----形成一个链。同样如果链的长度过长,会导致效率变低。但是鉴于Redis是基于内存的,因此多数情况下依然能达到较高的速度。
hashes的使用场景
hashes可以看做是string的增强,因此string能做的,hashes都能做,而且hashes能存储对象类型的数据,能更加细化key的查找路径。
对于用户级别的产品有奇效,如购物车:
key:用户id(每个用户对应一个购物车)
field:商品id(购物车好中的多个商品)
value:商品数量(每种商品数量不同)
商品数量+1:hinc
商品数量-1:hincrby key field -1
删除商品:hdel
全选商品:hgetall
购物车商品种类数:hlen
lists类型
用来存储有序的字符串(从左到右),元素可以重复,最大能够存储2^32-1(40亿左右)的数据
list常用api
命令 | 语法 | 用途 | 示例 |
---|---|---|---|
lpush | lpush 键 值1 值2… | 在指定的键所对应的list的头部插入所有的values,键不存在则新增 | lpush names jack rose,集合中最前的两个数据是rose jack |
rpush | rpush 键 值1 值2… | 在指定的键所对应的list的尾部插入所有的values,键不存在则新增 | rpush names jack rose,集合中最后的两个数据是jack rose |
lrange | lrange 键 下标1 下标2 | 查询从下标1到下标2之间的数据,第一个数据下标为0,下标可以为负数,-1表示倒数第一个数据,-2表示倒数第二个数据,以此类推 | lrange names 0 10,结果是names对应的集合中0-10之间的数据,如果数据长度不足,则返回已有的数据,不会出现异常 |
lpushx | lpushx键 值1 | 在指定的键存在的情况下,将所有的数据插入到集合的头部 | lpushx names jack rose,集合中最前的两个数据是rose jack |
rpushx | rpushx键 值1 | 在指定的键存在的情况下,将所有的数据插入到集合的尾部 | rpush names jack rose,集合中最后的两个数据是jack rose |
lpop | lpop 键 | 将该键对应的集合中的第一个数据取出,取出之后第一个数据就从集合中移除 | lpop names,结果:取出并移除第一个数据 |
rpop | rpop键 | 将该键对应的集合中的最后一个个数据取出,取出之后数据就从集合中移除 | rpop names,结果:取出并移除最后一个数据 |
数据结构
采用quicklist进行存储。quicklist中存储了一个*head
指针和一个*tail
指针,分别指向有quicklistNode组成的双向链表的头节点和尾部节点。quicklistNode中除了存储指向上一个(图中的pre)和下一个节点(图中的tail)的指针外,还存储了一个ziplist存储结构。
typedef struct quicklist {
quicklistNode *head; // 指向双向链表的头节点
quicklistNode *tail; // 指向双向链表的尾结点,这样就等于获取到了一个完整的链表
unsigned long count; // 所有的ziplist中一个存了多少个元素 /* total count of all entries in all ziplists */
unsigned long len; // node的总数量 /* number of quicklistNodes */
int fill : QL_FILL_BITS; // ziplist最大大小 /* fill factor for individual nodes */
unsigned int compress : QL_COMP_BITS; // 压缩深度 /* depth of end nodes not to compress;0=off */
unsigned int bookmark_count: QL_BM_BITS; //
quicklistBookmark bookmarks[];
} quicklist;
lists的使用场景
列表:
消息列表、文章列表、评论列表、公告列表、活动列表等,同时还可以用作朋友圈动态(按照创建时间进行倒叙排序)
sets类型
存储无序且不重复的数据,最大可以存储2^32-1个数据
sets常用API
命令 | 语法 | 用途 | 示例 |
---|---|---|---|
sadd | add 键 值 | 在指定的键对应的set集合中保存一个或多个数据 | sadd names jack tom |
smembers | smembers键 | 查询指定键的Set集合中保存的所有数据 | smembers names |
scard | scard 键 | 查询指定键的Set集合中保存的数据个数 | scard names |
sismember | sismemb键 值 | 判断值是否是指定键对应的Set集合中的成员,返回1表示是,0表示否 | sismember names jack |
srem | srem 键 值 | 删除指定键对应的Set集合中的一个或多个数据 | srem names jack |
spop | spop 键 count | 随机删除并返回指定键对应的Set集合中的1个或多个数据,数量由count决定,但是在某些版本中count不支持,不提供count时默认count为1 | spop numbers |
srandmember | srandmember 键 count | 随机返回指定键对应的Set集合中的1个或多个数据,数量由count决定,不提供count时默认count为1 | srandmember names 2,随机返回names集合中的2个数据 |
smove | smove 键1 键2 值 | 将键1对应集合中的指定数据移动到键2的集合中 | smove names1 names2 jack |
sdiff | sdiff 键1 键2 | 返回键1的集合中在键2的集合中不存在的数据,也就是求键1集合在键2集合中的差集 | sdiff names1 names2 |
sdiffstore | sdiffstore 键1 键2 键3 | 将键2集合在键3中的差集保存到键1的集合中 | sdiffstore names names1 names2 |
sinter | sinter 键1 键2 | 返回键1集合中在键2集合中也存在的数据,也就是求键1集合和键2集合的交集 | sinter names1 names2 |
sinterstore | sinterstore 键1 键2 键3 | 将键2集合在键3中的交集保存到键1的集合中 | sinterstore names names1 names2 |
sunion | sunion 键1 键2 | 返回键1集合和键2集合的并集 | sunion names1 names2 |
sunionstore | sunion 键1 键2 键3 | 将键2集合和键3集合的并集保存到键1集合中 | sunionstore names names1 names2 |
存储结构
- intset
- hashtable
在Redis的配置文件中的set-max-intset-entries
配置项可以配置set内部存储结构切换的阈值。
sets使用场景
- 抽奖:spop pool
- 点赞、签到、打卡等
- 商品标签、用户画像(人口属性,信用属性,社交,兴趣爱好),用sets集合的
交并补
的特性
- 用户关注、推荐模型(可能认识的人)
点赞案例:
sorted sets类型
sorted sets也叫zset,是一种可以存储不重复数据的set,且元素是有序的。但是有序不是值根据key来排序,而是除了key之外,还额外增加一个score字段,用来给可以进行排名。
常用API
命令 | 语法 | 用途 | 示例 |
---|---|---|---|
zadd | add 键score value score value… 向指定键对应的集合中添加数据,score为排序值,可以是整数或小数。value为值 | add names 1 zhangsan 2 lisi | |
zcard | card键 返回指定键对应的集合中的数据总数 | card names | |
zcount | count 键min max 查询指定键的集合中排序值在min和max之间的数据个数 | count names 1 5 | |
zrange | range键 start stop 返回指定键对应的集合中排序序号(非排序值)在start和stop之间的数据,序号从0开始,数据会升序排列 | range names jack | |
zrank | rank 键 值 返回指定键对应的集合中某个值得排序序号 | rank names jack | |
zrem | rem键1 值1 值2 移除指定键对应的Set集合中的1个或多个值 | rem names1 jack | |
zremrangebyrank | remrangebyrank 键start stop 按照排序序号移除多个成员 | remrangebyrank names 0 2 | |
zremrangebyscore | remrangebyscore键 min max 按照排序值移除多个成员 | remrangebyscore names 1 2 |
zadd myzset 10 java 20 php 30 python 40 cpp
zrange myzset 0 -1 withscores
zrevrange myzset 0 -1 whtiscores
zrangebyscore myzset 20 30
zrem myzset php cpp
zcard myzset
zincrby myzset 5 java
zcount myzset 20 40
zrank myzset java
zscore myzset php
sortedsets存储结构
- ziplist(元素数量<128,所有元素长度小于64bytes)
- skiplist+dict:对于这样的存储机构
当元素存储的个数越来越大,如果刚好要查一个处于尾节点的元素,必定是要遍历n次。因此这个链表必定要经过该井优化后才能使用。
借鉴二分查找的思想,在某些节点上增加一个指针位,让他跨越一定数量的节点指向下一个,这样通过跳过随机个数的节点,实现横向的查找变成纵向的查找,减少遍历的次数。
sortedsets使用场景
排行榜:
id为7001的新闻被点击一次:
zincr不要hotnew_20210814 1 7001
获取今天热度排名前十五的新闻:
zrevrange hotnew_20210814 0 15 withscores