正式开始!
我的思考:我清楚 redis 不通的对象,?️不同的的底层数据结构, 你问的是数据结构,而不是对象,因此我这样回答
redis数据结构有这些。
全部结构都回答一遍没有遗漏,然后不断回忆全部内容。
防不胜防别人点评:
教训:你应该问一下数据结构是对象还是编码方式。他们问题有陷阱
有人称数据结构 就是基本的形式,最基本最傻的形式对比 容器的类型(string,vector,list map hash,优先级队列等) 这属于他们面试官认知不清楚,提问不高级 还是刻意这样做,就不清楚了。 这叫做诈唬。被忽悠了。看出自己不清楚。
在认知中:红黑树是数据结构,map不是,因为map更加高级。
这一点错误理解,导致你不会主动沟通。
你能怪面试官不正规吗?
数据结构普通的类 ADT抽象,不一定是底层实现,c语言深陷太深,缘故, 遇到人工智能,其他岗位面试官你出问题了。你问清楚数据值是什么?不要自己想。
reids 有很对数据结构,每个不通数据结构不通实现。我全部说一遍,我自己记不住呀 因此必须学会分类回答 ,然后重点说其中一个。重点自动调整,之前学容器时候 考虑每个结构使用条件。这里都给安排好了。
typedef struct redisObject {
// 类型
unsigned type:4;
// 编码
unsigned encoding:4;
// 指向数据的指针
void *ptr;
// 记录对象最后一次被程序访问时间,用于计算空转时长(当前时间-lru)
unsigned lru:22; /* lru time (relative to server.lruclock) */
// 引用计数,用于内存回收
int refcount;
} robj;
回答:
编码 | 对象 |
---|---|
REDIS_ENCODING_INTSET | 使用整数集合实现的集合对象。 |
REDIS_ENCODING_HT | 使用字典实现的集合对象。 |
REDIS_ENCODING_ZIPLIST | 使用压缩列表实现的有序集合对象。 |
REDIS_ENCODING_SKIPLIST | 使用跳跃表和字典实现的有序集合对象。5个对象类型,8个对象编码方式 |
、
# 键为字符串对象,值为字符串对象
redis> SET msg "hello world"
OK
redis> TYPE msg
string
# 键为字符串对象,值为列表对象
redis> RPUSH numbers 1 3 5
(integer) 6
redis> TYPE numbers
list
# 键为字符串对象,值为哈希对象
redis> HMSET profile name Tome age 25 career Programmer
OK
redis> TYPE profile
hash
# 键为字符串对象,值为集合对象
redis> SADD fruits apple banana cherry
(integer) 3
redis> TYPE fruits
set
# 键为字符串对象,值为有序集合对象
redis> ZADD price 8.5 apple 5.0 banana 6.0 cherry
(integer) 3
redis> TYPE price
zset
/* Object types */
// 对象类型
#define REDIS_STRING 0
#define REDIS_LIST 1
#define REDIS_SET 2
#define REDIS_ZSET 3
#define REDIS_HASH 4
rewriteAppendOnlyFile(char *filename) {
if (o->type == REDIS_STRING) {
/* Emit a SET command */
char cmd[]="*3\r\n$3\r\nSET\r\n";
if (rioWrite(&aof,cmd,sizeof(cmd)-1) == 0) goto werr;
/* Key and value */
if (rioWriteBulkObject(&aof,&key) == 0) goto werr;
if (rioWriteBulkObject(&aof,o) == 0) goto werr;
} else if (o->type == REDIS_LIST) {
if (rewriteListObject(&aof,&key,o) == 0) goto werr;
} else if (o->type == REDIS_SET) {
if (rewriteSetObject(&aof,&key,o) == 0) goto werr;
} else if (o->type == REDIS_ZSET) {
if (rewriteSortedSetObject(&aof,&key,o) == 0) goto werr;
} else if (o->type == REDIS_HASH) {
if (rewriteHashObject(&aof,&key,o) == 0) goto werr;
} else {
redisPanic("Unknown object type");
}
/* Objects encoding. Some kind of objects like Strings and Hashes can be
* internally represented in multiple ways. The 'encoding' field of the object
* is set to one of this fields for this object. */
// 对象编码
#define REDIS_ENCODING_RAW 0 /* Raw representation */
#define REDIS_ENCODING_INT 1 /* Encoded as integer */
#define REDIS_ENCODING_HT 2 /* Encoded as hash table */
#define REDIS_ENCODING_ZIPMAP 3 /* Encoded as zipmap */
#define REDIS_ENCODING_LINKEDLIST 4 /* Encoded as regular linked list */
#define REDIS_ENCODING_ZIPLIST 5 /* Encoded as ziplist */
#define REDIS_ENCODING_INTSET 6 /* Encoded as intset */
#define REDIS_ENCODING_SKIPLIST 7 /* Encoded as skiplist */
#define REDIS_ENCODING_EMBSTR 8 /* Embedded sds string encoding */
/* Save a Redis object. Returns -1 on error, 0 on success.
*
* 将给定对象 o 保存到 rdb 中。
*
* 保存成功返回 rdb 保存该对象所需的字节数 ,失败返回 0 。
*
* p.s.上面原文注释所说的返回值是不正确的
*/
int rdbSaveObject(rio *rdb, robj *o) {
int n, nwritten = 0;
// 保存字符串对象
if (o->type == REDIS_STRING) {
/* Save a string value */
if ((n = rdbSaveStringObject(rdb,o)) == -1) return -1;
nwritten += n;
// 保存列表对象
} else if (o->type == REDIS_LIST) {
/* Save a list value */
if (o->encoding == REDIS_ENCODING_ZIPLIST) {
size_t l = ziplistBlobLen((unsigned char*)o->ptr);
// 以字符串对象的形式保存整个 ZIPLIST 列表
if ((n = rdbSaveRawString(rdb,o->ptr,l)) == -1) return -1;
nwritten += n;
} else if (o->encoding == REDIS_ENCODING_LINKEDLIST) {
list *list = o->ptr;
listIter li;
listNode *ln;
if ((n = rdbSaveLen(rdb,listLength(list))) == -1) return -1;
nwritten += n;
// 遍历所有列表项
listRewind(list,&li);
while((ln = listNext(&li))) {
robj *eleobj = listNodeValue(ln);
// 以字符串对象的形式保存列表项
if ((n = rdbSaveStringObject(rdb,eleobj)) == -1) return -1;
nwritten += n;
}
} else {
redisPanic("Unknown list encoding");
}
// 保存集合对象
} else if (o->type == REDIS_SET) {
/* Save a set value */
if (o->encoding == REDIS_ENCODING_HT) {
dict *set = o->ptr;
dictIterator *di = dictGetIterator(set);
dictEntry *de;
if ((n = rdbSaveLen(rdb,dictSize(set))) == -1) return -1;
nwritten += n;
// 遍历集合成员
while((de = dictNext(di)) != NULL) {
robj *eleobj = dictGetKey(de);
// 以字符串对象的方式保存成员
if ((n = rdbSaveStringObject(rdb,eleobj)) == -1) return -1;
nwritten += n;
}
dictReleaseIterator(di);
} else if (o->encoding == REDIS_ENCODING_INTSET) {
size_t l = intsetBlobLen((intset*)o->ptr);
// 以字符串对象的方式保存整个 INTSET 集合
if ((n = rdbSaveRawString(rdb,o->ptr,l)) == -1) return -1;
nwritten += n;
} else {
redisPanic("Unknown set encoding");
}
// 保存有序集对象
} else if (o->type == REDIS_ZSET) {
/* Save a sorted set value */
if (o->encoding == REDIS_ENCODING_ZIPLIST) {
size_t l = ziplistBlobLen((unsigned char*)o->ptr);
// 以字符串对象的形式保存整个 ZIPLIST 有序集
if ((n = rdbSaveRawString(rdb,o->ptr,l)) == -1) return -1;
nwritten += n;
} else if (o->encoding == REDIS_ENCODING_SKIPLIST) {
zset *zs = o->ptr;
dictIterator *di = dictGetIterator(zs->dict);
dictEntry *de;
if ((n = rdbSaveLen(rdb,dictSize(zs->dict))) == -1) return -1;
nwritten += n;
// 遍历有序集
while((de = dictNext(di)) != NULL) {
robj *eleobj = dictGetKey(de);
double *score = dictGetVal(de);
// 以字符串对象的形式保存集合成员
if ((n = rdbSaveStringObject(rdb,eleobj)) == -1) return -1;
nwritten += n;
// 成员分值(一个双精度浮点数)会被转换成字符串
// 然后保存到 rdb 中
if ((n = rdbSaveDoubleValue(rdb,*score)) == -1) return -1;
nwritten += n;
}
dictReleaseIterator(di);
} else {
redisPanic("Unknown sorted set encoding");
}
// 保存哈希表
} else if (o->type == REDIS_HASH) {
/* Save a hash value */
if (o->encoding == REDIS_ENCODING_ZIPLIST) {
size_t l = ziplistBlobLen((unsigned char*)o->ptr);
// 以字符串对象的形式保存整个 ZIPLIST 哈希表
if ((n = rdbSaveRawString(rdb,o->ptr,l)) == -1) return -1;
nwritten += n;
} else if (o->encoding == REDIS_ENCODING_HT) {
dictIterator *di = dictGetIterator(o->ptr);
dictEntry *de;
if ((n = rdbSaveLen(rdb,dictSize((dict*)o->ptr))) == -1) return -1;
nwritten += n;
// 迭代字典
while((de = dictNext(di)) != NULL) {
robj *key = dictGetKey(de);
robj *val = dictGetVal(de);
// 键和值都以字符串对象的形式来保存
if ((n = rdbSaveStringObject(rdb,key)) == -1) return -1;
nwritten += n;
if ((n = rdbSaveStringObject(rdb,val)) == -1) return -1;
nwritten += n;
}
dictReleaseIterator(di);
} else {
redisPanic("Unknown hash encoding");
}
} else {
redisPanic("Unknown object type");
}
return nwritten;
}
redis> SET msg "hello wrold"
OK
redis> OBJECT ENCODING msg
"embstr"
redis> SET story "long long long long long long ago ..."
OK
redis> OBJECT ENCODING story
"raw"
http://redisbook.com/preview/object/set.html
当集合对象可以同时满足以下两个条件时, 对象使用 intset 编码:
//存放的是整数类型,编码类是:intset
127.0.0.1:6379> SADD numbers 1 3 5
(integer) 3
127.0.0.1:6379> OBJECT ENCODING numbers
"intset"
//存放的是字符串类型,编码类是:hashtable,代表是无序 唯一。
127.0.0.1:6379> SADD numbers "seven"
(integer) 1
127.0.0.1:6379> OBJECT ENCODING numbers
"hashtable"
//超过512整数,编码类是:hashtable
127.0.0.1:6379> EVAL "for i=1, 512 do redis.call('SADD', KEYS[1], i) end" 1 integers
(nil)
127.0.0.1:6379> OBJECT ENCODING integers
"intset"
127.0.0.1:6379> SADD integers 10086
(integer) 1
127.0.0.1:6379> OBJECT ENCODING integers
"hashtable"
127.0.0.1:6379>
http://redisbook.com/preview/object/set.html
ziplist 编码的有序集合对象使用压缩列表作为底层实现
skiplist当有序集合对象可以同时满足以下两个条件时, 对象使用 ziplist 编码:
//有序集合保存的元素数量小于 128 个
127.0.0.1:6379> ZADD price 8.5 apple 5.0 banana 6.0 cherry
(integer) 3
127.0.0.1:6379> object encoding price
"ziplist"
127.0.0.1:6379> type price
zset
# 对象包含了 128 个元素
redis> EVAL "for i=1, 128 do redis.call('ZADD', KEYS[1], i, i) end" 1 numbers
(nil)
redis> ZCARD numbers
(integer) 128
redis> OBJECT ENCODING numbers
"ziplist"
# 再添加一个新元素
redis> ZADD numbers 3.14 pi
(integer) 1
# 对象包含的元素数量变为 129 个
redis> ZCARD numbers
(integer) 129
# 编码已改变
redis> OBJECT ENCODING numbers
"skiplist"
# 向有序集合添加一个成员只有三字节长的元素
redis> ZADD blah 1.0 www
(integer) 1
redis> OBJECT ENCODING blah
"ziplist"
# 向有序集合添加一个成员为 66 字节长的元素
redis> ZADD blah 2.0 oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo
(integer) 1
# 编码已改变
redis> OBJECT ENCODING blah
"skiplist"
Redis 可以根据不同的使用场景来为一个对象设置不同的编码, 从而优化对象在某一场景下的效率。
举个例子, 在列表对象包含的元素比较少时, Redis 使用压缩列表作为列表对象的底层实现:
typedef struct redisObject {
// 类型
unsigned type:4;
// 编码
unsigned encoding:4;
// 指向数据的指针
void *ptr;
// 记录对象最后一次被程序访问时间,用于计算空转时长(当前时间-lru)
unsigned lru:22; /* lru time (relative to server.lruclock) */
// 引用计数,用于内存回收
int refcount;
} robj;
如果redis配置了maxmemory和maxmemory-policy策略,则当redis内存数据达到maxmemory时,会根据maxmemory-policy配置来淘汰内存数据,以避免OOM。redis提供了以下6种淘汰策略:
Redis的每个slot槽里存储的key就是使用了Radix树。
现在我们就演绎下Trie树是如何浪费内存和空间的。比如下面的一组数据:
{ "deck": someValue, "did": someValue, "doe": someValue, "dog": someValue, "doge": someValue, "dogs": someValue }
我用Trie树的画法把上面的key value画出来如下:
Redis是如何实现Radix树的呢?
以下内容摘自http://mysql.taobao.org/monthly/2019/04/03/
raxNode是radix tree的核心数据结构,其结构体如下所示:
typedef struct raxNode {
uint32_t iskey:1; //是否包含key,
uint32_t isnull:1; //是否有存储value值,比如存储元数据就只有key,没有value值。value值也是存储在data中
uint32_t iscompr:1; //是否做了前缀压缩
uint32_t size:29; //该节点存储的字符个数
unsigned char data[]; //存储子节点的信息
} raxNode;
Radix和Trie树对于字符串的检索,特别是有公共前缀的场景。如当输入一个网址,可以自动搜索出可能的选择。当没有完全匹配的搜索结果,可以返回前缀最相似的可能。总之对于字符串的检索,Trie类树都比较适合,比如本文中的Redis的key这样的场景就非常适合。
到这里才刚刚开始
面试题:用数据库来算附近的人(这是第二次被问了,还是通一个公司 忘记惨痛教训,后悔无用,必须在看看在看在看)
前言:针对“附近的人”这一位置服务领域的应用场景,常见的可使用PG、MySQL和MongoDB等多种DB的空间索引进行实现。
而Redis另辟蹊径,结合其有序队列zset以及geohash编码,实现了空间搜索功能,且拥有极高的运行效率。
https://zhuanlan.zhihu.com/p/90464063
https://blog.csdn.net/weixin_43246215/article/details/108041739
https://halfrost.com/go_spatial_search/
如何存储一个位置
app 界面上会显示出自己附近一个范围内可用的出租车或者共享单车。假设地图上会显示以自己为圆心,5公里为半径,这个范围内的车。如何实现呢?最直观的想法就是去数据库里面查表,计算并查询车距离用户小于等于5公里的,筛选出来,把数据返回给客户端。
这种做法比较笨,一般也不会这么做。为什么呢?因为这种做法需要对整个表里面的每一项都计算一次相对距离。太耗时了。既然数据量太大,我们就需要分而治之。那么就会想到把地图分块。这样即使每一块里面的每条数据都计算一次相对距离,也比之前全表都计算一次要快很多。
我们也都知道,现在用的比较多的数据库 MySQL、PostgreSQL 都原生支持 B+ 树。这种数据结构能高效的查询。地图分块的过程其实就是一种添加索引的过程,如果能想到一个办法,把地图上的点添加一个合适的索引,并且能够排序,那么就可以利用类似二分查找的方法进行快速查询。
问题就来了,地图上的点是二维的,有经度和纬度,这如何索引呢?如果只针对其中的一个维度,经度或者纬度进行搜索,那搜出来一遍以后还要进行二次搜索。那要是更高维度呢?三维。可能有人会说可以设置维度的优先级,比如拼接一个联合键,那在三维空间中,x,y,z 谁的优先级高呢?设置优先级好像并不是很合理。
首先在每个geohash网格中的geohash值都是连续的,有固定范围。
所以只要找出有序集合中,处在该范围的位置对象即可。
以下是有序集合的跳表数据结构
其拥有类似二叉查找树的查询效率,操作平均时间复杂性为O(log(N))。且最底层的所有元素都以链表的形式按序排列。
所以在查询时,只要找到集合中处在目标geohash网格中的第一个值,后续依次对比即可,不用多次查找。
基础知识不牢固呀:
查找--排序---索引(tree ,hash,quicklist)
同类问题:
我不知道mysql 查询那个记录,我怎么做加锁查询。索引是全部记录做排序
我不知道用户查询那个位置,我怎么对全部位置进行排序!地理位置本身有序的,我怎么存储起来呀。每次都都计算,性能不一定高效
查找 -有序数组 --平衡二叉树-- 跳跃表(考虑元素采用什么结构存储很重要)
//01 geoadd key 经度 纬度 地理位置
127.0.0.1:6379> geoadd china:city 116.40 39.90 beijing 121.47 32.23 shanghai
(integer) 2
//02 geopos命令:从key里返回所有给定位置元素的位置(经度和纬度)
127.0.0.1:6379> geopos china:city beijing
1) 1) "116.39999896287918091"
2) "39.90000009167092543"
//03 georadius命令:以给定的经纬度为中心,找出某一半径内的元素
127.0.0.1:6379> georadius china:city 116 39 1000 km
1) "shanghai"
2) "beijing"
//04 geohash命令:返回一个或多个位置元素的Geohash字符串
127.0.0.1:6379> geohash china:city beijing
1) "wx4fbxxfke0"
Geohash字符串查询
http://geohash.org/wx4fbxxfke0
127.0.0.1:6379> debug object china:city
Value at:0x7f2ad2c285c0 refcount:1
encoding:ziplist
serializedlength:51
lru:4668404
lru_seconds_idle:5768
Redis另辟蹊径,结合其有序队列zset以及geohash编码, 实现了空间搜索功能,且拥有极高的运行效率。
/* GEOADD key long lat name [long2 lat2 name2 ... longN latN nameN] */
void geoaddCommand(client *c) {
int elements = (c->argc - 2) / 3;
int argc = 2+elements*2; /* ZADD key score ele ... */
robj **argv = zcalloc(argc*sizeof(robj*));
argv[0] = createRawStringObject("zadd",4);
argv[1] = c->argv[1]; /* key */
incrRefCount(argv[1]);
/* Create the argument vector to call ZADD in order to add all
* the score,value pairs to the requested zset, where score is actually
* an encoded version of lat,long. */
int i;
for (i = 0; i < elements; i++) {
double xy[2];
if (extractLongLatOrReply(c, (c->argv+2)+(i*3),xy) == C_ERR) {
for (i = 0; i < argc; i++)
if (argv[i]) decrRefCount(argv[i]);
zfree(argv);
return;
}
/* Turn the coordinates into the score of the element. */
GeoHashBits hash;
geohashEncodeWGS84(xy[0], xy[1], GEO_STEP_MAX, &hash);
GeoHashFix52Bits bits = geohashAlign52Bits(hash);
//score设置
robj *score = createObject(OBJ_STRING, sdsfromlonglong(bits));
robj *val = c->argv[2 + i * 3 + 2];
argv[2+i*2] = score;//坐标
argv[3+i*2] = val;//
incrRefCount(val);
}
/* Finally call ZADD that will do the work for us. */
replaceClientCommandVector(c,argc,argv);
zaddCommand)//zset存储
}
部署说明
在一个地图应用中,车的数据、餐馆的数据、人的数据可能会有百万千万条,如果使用 Redis 的 Geo 数据结构,它们将 全部放在一个 zset 集合中。
在 Redis 的集群环境中,集合可能会从一个节点迁移到另一个节点,如果单个 key 的数据过大,会对集群的迁移工作造成较大的影响【一个key 只能在一个slot,这里说对大key 数据进行拆分 才会从一个节点到另外一个节点。平时一个key就在一个节点,不会移动的。】
在集群环境中单个 key 对应的数据量不宜超过 1M,否则会导致集群迁移出现卡顿现象,影响线上服务的正常运行。
所以,这里建议 Geo 的数据使用 单独的 Redis 实例部署,不使用集群环境。
如果数据量过亿甚至更大,就需要对 Geo 数据进行拆分,按国家拆分、按省拆分,按市拆分,在人口特大城市甚至可以按区拆分。这样就可以显著降低单个 zset 集合的大小。
https://zhuanlan.zhihu.com/p/40854446
耗时: