01 redis数据结构

vvEcho 2024-01-20 14:08:37
Categories: Tags:

redis是一种基于键值对的来存储数据的nosql数据库

其中对应的value支持string,list,set,zset,bitmaps,hyperloglog,GEO地理信息数据

常用的有String,list,set,zset,hash

使用场景

string 用来缓存存储一些简单的数据,比如用户名,密码,token,session等;
hash键值对存储,hset user:1 name zhangsan age 18 gender male
list 存储一些需要按照顺序存储的数据,比如消息队列,日志,缓存等;
set 存储一些需要去重且无序的数据,比如用户访问过的页面,用户访问过的商品等(抽奖系统:SADD 添加参与者,SPOP 随机抽取中奖者;标签/好友关系:存储用户标签,通过 SINTER 查找共同关注;去重统计:记录用户点赞、签到等唯一行为);
sort set 元素关联分数,按分数排序;如游戏积分,排行榜

底层数据结构

hash底层的数据结构是压缩列表ziplist或hashtable,键值对个数超过512 会由ziplist转换为hashtable

set底层的数据结构是intset或hashtable,元素为整数且键值对个数小于512为intset,若超过或不符合则转换为hashtable

zset底层数据结构为压缩列表ziplist或skiplist+dict(跳表+字典),元素数量小于128且元素长度小于64;超过此阈值则转换为跳表+字典

list的底层数据结构为压缩列表ziplist或快速列表quicklist,元素数量小于512且元素长度小于64用ziplist;超过此阈值则转换为quicklist,由多个ziplist节点组成的双向链表,可以平衡内存与性能

List

Redis-List是一个双端队列(Deque),适合顺序、队列、流式消费 场景,但不适合随机访问和复杂聚合

使用场景
对可靠性要求不高的异步任务,我们会用 Redis List 做轻量队列。例如记录某用户访问过的页面,或者记录用户访问过的商品,或者记录用户点赞、签到等唯一行为。

redission存储最近浏览的100个商品示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void recordSkuView(long userId, long skuId) {
RList<Long> list = redissonClient.getList(
"user:view:sku:" + userId
);

// 如果已经存在,先移除(避免重复)
list.remove(skuId);

// 再头插
list.add(0, skuId);

// 最多保存 100 条
if (list.size() > 100) {
list.removeRange(100, list.size() - 1);
}
}

Set

redis的set是一个无序、不重复元素集合;底层是个intset(整数集合)或者是hashtable(hash表);
zset是一个有序的集合,支持排序和范围查询的Set;底层是一个dict(字典)+skiplist(跳表)

set的使用场景
例如,黑白名单,幂等校验,在线用户,活跃用户,订阅关系等;另外它可以快速取两个集合的交集

1
2
3
4
// 获取vip和kyc的交集
Set<String> target =
redisson.getSet("kyc_users")
.readIntersection(redisson.getSet("vip_users"));

zset的使用场景

  1. 排行榜 / Top N

    1
    2
    3
    4
    5
    6
    RScoredSortedSet<String> rank =
    redisson.getScoredSortedSet("rank");

    rank.add(100, "user1");
    Collection<String> top10 =
    rank.valueRangeReversed(0, 9);
  2. 延迟队列(交易所/风控高频)

    1
    2
    3
    4
    5
    6
    7
    RScoredSortedSet<String> delayQueue =
    redisson.getScoredSortedSet("delay_queue");

    delayQueue.add(executeTime, orderId);

    // 拉取到期任务
    Collection<String> tasks = delayQueue.valueRange(0, true, now, true);
  3. 实时排序 / 统计

    1
    rank.addScore(userId, delta);
  4. 滑动时间窗口,只关注窗口内的数据

    1
    rank.removeRangeByScore(0, true, now - windowMs, true);

bitmaps

Bitmap = 用bit(0 / 1)表示状态的紧凑型数据结构
Bitmap 是基于String 实现的bit操作
使用场景

  1. 用户签到 / 活跃统计
    1
    2
    3
    4
    5
    public void sign(long userId, int day) {
    String key = "user:sign:" + userId + ":2026-02";
    RBitSet bitSet = redisson.getBitSet(key);
    bitSet.set(day - 1, true);
    }
  2. DAU(日活)/WAU(周活) MAU(月活)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    //记录活跃
    public void markActive(long userId) {
    String key = "dau:2026-02-12";
    redisson.getBitSet(key).set(userId);
    }
    //判断是否活跃
    public boolean isActive(long userId) {
    return redisson.getBitSet("dau:2026-02-12").get(userId);
    }
    //统计 DAU
    public long dauCount() {
    return redisson.getBitSet("dau:2026-02-12").cardinality();
    }
    //MAU(多天 OR)
    public long mauCount(List<String> dayKeys) {
    RBitSet result = redisson.getBitSet("tmp:mau");

    for (String key : dayKeys) {
    result.or(redisson.getBitSet(key));
    }
    return result.cardinality();
    }
    //or() 会修改当前 BitSet,生产中建议用临时 key + TTL
  3. 黑名单 / 风控标记(布隆过滤器)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    //封禁 / 解封
    public void ban(long userId) {
    redisson.getBitSet("user:blacklist").set(userId, true);
    }
    public void unban(long userId) {
    redisson.getBitSet("user:blacklist").set(userId, false);
    }
    //判断是否封禁(QPS 很高的接口)
    public boolean isBanned(long userId) {
    return redisson.getBitSet("user:blacklist").get(userId);
    }
  4. 去重(不要求精确对象内容)
    1
    2
    3
    4
    5
    6
    //判断是否已领取空投
    public boolean hasClaimed(long userId, long airdropId) {
    String key = "airdrop:claimed:" + airdropId;
    return redisson.getBitSet(key).get(userId);
    }
    //高并发下需 Lua 保证 check + set 原子
  5. Web3 / 交易所场景(综合)
    1
    2
    3
    4
    5
    6
    7
    //是否参与launchpad
    public void markJoined(long userId, long launchpadId) {
    redisson.getBitSet("launchpad:join:" + launchpadId).set(userId);
    }
    public boolean hasJoined(long userId, long launchpadId) {
    return redisson.getBitSet("launchpad:join:" + launchpadId).get(userId);
    }

    总结
    bitmaps的offset设计是生死线,否则会导致内存浪费,Bitmap不适合极稀疏数据
    RBitSet 的and /or 会改自身,使用是需注意;可以用临时key,或设置TTL,或使用redis层copy
    Redisson 通过 RBitSet 封装 Redis Bitmap,适合签到、DAU、风控、空投去重等状态型场景,具备 O(1) 判断和极高的空间效率,但需要谨慎设计 offset,避免稀疏数据导致内存浪费