五种数据结构

    • keys * :遍历所有 key,生产环境不建议使用,时间复杂度 O(n)
    • dbsize key:计算 key 的总数,Redis 内置了这个计数器,会实时更新 key 的总数,时间复杂度 O(1)
    • exists key:检查 key 是否存在,时间复杂度为O(1)
    • expire key seconds:key 在指定 seconds(单位: 秒)后过期,时间复杂度 O(1)
    • ttl key:key 剩余的过期时间(单位:秒),时间复杂度O(1)
    • persist key:去掉 key 的过期时间,时间复杂度 O(1)
    • type key:查看 key 的类型,时间复杂度 O(1)

    数据结构之字符串(string)

    最大限制 512MB,适用于缓存、计算器、分布式锁等,字符串类型的值可以为简单的字符串、JSON、XML、数组甚至是二进制(音视频)。

    重点命令介绍

    set

    • seconds:单位(秒)
    • milliseconds:单位(毫秒)
    • nx:key 存在不做任何操作,等价于 setnx
    • xx:key 存在做操作与 nx 相反,等价于 setex

    mget、mset

    mget、mset 可以批量的获取或设置值,如果使用 get 多次读取数据等价于 n 次网络时间 + n 次命令时间,这种方式则可以利用 mget 优化,等价于 1 次网络时间 + n 次命令时间,但是也要注意这是一个 O(n) 的操作,避免命令过多客户端阻塞。

    1. 127.0.0.1:6379> mset key value [key value ...]
    2. # mset 演示
    3. 127.0.0.1:6379> mset key1 val1 key2 val2 key3 val3
    4. OK
    5. # mget 语法
    6. 127.0.0.1:6379> mget key [key ...]
    7. # mget 演示
    8. 127.0.0.1:6379> mget key1 key2
    9. 1) "val1"
    10. 2) "val2"

    incr、decr、incrby、decrby

    • incr:自增
    • decr:自减
    • incrby:指定数字自增
    • decrby:指定数字自减
    • incrbyfloat:指定浮点数自增
    1. incr key
    2. decr key
    3. incrby key increment
    4. decrby key decrement
    5. incrbyfloat key increment

    应用场景

    1. 缓存

    例如,对城市列表数据进行缓存,伪代码如下:

    1. function cityList() {
    2. const redisKey = 'city';
    3. let cities = redis.get(redisKey);
    4. if (!cities) {
    5. cities = mongo.getCityList();
    6. redis.set(redisKey, JSON.stringify(cities)); // 存储的时候进行序列化
    7. }
    8. return cities;
    9. }

    2. 分布式锁

    Redis 官方 2.8 版本之后支持 set 命令传入 setnx、expire 扩展参数,这样就可以一条命令一口气执行,实现了原子性操作,如果在 Sentinel、Redis Cluster 模式,可以参考 Redlock 算法

    1. set key value [EX seconds] [PX milliseconds] [NX|XX]

    3. 计数器

    这个场景也还是比较多的,例如网站 PV/UV 统计、文章点赞/阅读量、视频网站的播放量。Redis 提供的 incr 命令可实现计数器功能,且性能非常好复杂度为 O(1)。

    这在用户登陆注册管理系统中是很常见的,相较于 Memcache 存储 Session 信息,Redis 不会因为服务重启而导致 Session 数据丢失,因为其有数据持久化功能,不会因为 Session 而导致的用户体验问题。之前用过公司内部一个产品发现每当他们发版之后(大概已经知道用户信息是怎么做了)都要去重新登录,真的会影响用户体验。

    5. 限流

    例如,短信发送为了避免接口被频繁调用,通常要在指定时间内避免重复发送

    1. const SMSLimit = async phone => {
    2. const key = `sms:limit:${phone}`;
    3. const result = await redis.set(key, 1, 'EX', 60, 'NX');
    4. if (result === null) {
    5. console.log('60 秒之内无法再次发送验证码');
    6. return false;
    7. }
    8. console.log('可以发送');
    9. return true;
    10. }
    11. SMSLimit(18800000000);

    哈希结构有一个特点,所有的命令都是以 H 开头,hash 类型其值本身是由一个或多个 filed-value 构成,如下所示:

    1. hashKey = {
    2. field1: value1,
    3. }
    • 优点:节省空间,可以部分更新。
    • 缺点:不支持 TTL 设置,Redis 中过期时间只针对顶级 Key,无法对 Hash Key 中的 field 设置过期时间,只能对整个 Key 通过 expire 设置。
    • 注意:在使用 hgetall 的时候要注意,如果集合很大,将会浪费性能。

    常用命令

    常用命令实践

    1. # 设置 student 的 name 为 Jack
    2. $ hset student name Jack
    3. # 获取 student 的 name 值
    4. $ hget student name
    5. "Jack"
    6. # 对 key student 批量设置 age、sex 属性
    7. $ hmset student age 18 sex man
    8. # 批量查询 student 的 age、sex 属性
    9. $ hmget student age sex
    10. 1) "18"
    11. 2) "man"
    12. # 获取 student 的所有 fields、value
    13. $ hgetall student
    14. 1) "name"
    15. 2) "Jack"
    16. 3) "age"
    17. 4) "18"
    18. 6) "man"

    hash 适合将一些相关的数据存储在一起,例如,缓存用户信息,与字符串不同的是,hash 可以对用户信息结构中的每个字段单独存储,当我们需要获取信息时可以仅获取我们需要的部分字段,如果使用字符串存储,两种方式,一种是将用户信息拆分为多个键(每个属性一个键)来存储,这样就显得有点冗余,占用过的 Key 同时也占用空间,另一种是序列化字符串存储,这种方式如果取数据只能全部取出并且还要进行反序列化,序列化/反序列化也有一定的内存开销。

    以下为缓存用户信息代码示例:

    1. // 模拟查询 Mongo 数据
    2. const mongo = {
    3. getUserInfoByUserId: userId => {
    4. return {
    5. name: 'Jack',
    6. age: 19,
    7. }
    8. }
    9. }
    10. // 获取用户信息
    11. async function getUserInfo(userId) {
    12. const key = `user:${userId}`;
    13. try {
    14. // 从缓存中获取数据
    15. const userInfoCache = await redis.hgetall(key);
    16. // 如果 userInfoCache 为空,返回值为 {}
    17. if (Object.keys(userInfoCache).length === 0) {
    18. const userInfo = mongo.getUserInfoByUserId(userId);
    19. await redis.hmset(key, userInfo);
    20. await redis.expire(key, 120);
    21. return userInfo;
    22. }
    23. return userInfoCache;
    24. } catch(err) {
    25. console.error(err);
    26. throw err;
    27. }
    28. }
    29. getUserInfo(1)

    数据结构之列表(list)

    Redis 的列表是用来存储字符串元素的集合,基于 Linked Lists 实现,这意味着插入、删除操作非常快,时间复杂度为 O(1),索引很慢,时间复杂度为 O(n)。

    Redis 列表的命令都是以 L 开头,在实际应用中它可以用作队列或栈

    • Stack(栈):后进先出,实现命令lpush + lpop
    • Queue(队列):先进先出,实现命令lpush + rpop
    • Capped Collection(有限集合):lpush + ltrim
    • Message Queue(消息队列):lpush + brpop

    常用命令

    常用命令实践

    应用场景

    1. 消息队列

    Redis List 结构的 lpush + brpop 命令可实现消息队列,lpush 命令是从左端插入数据,brpop 命令是从右端阻塞式的读取数据,阻塞读过程中如果队列中没有数据,会立即进入休眠直到数据到来或超过设置的 timeout 时间,则会立即醒过来。

    1. async function test() {
    2. const key = 'languages';
    3. // 阻塞读,timeout 为 5 秒钟
    4. const result = await redis.brpop(key, 5);
    5. console.log(result);
    6. }
    7. test();
    8. test();

    Redis 的集合类型可用来存储多个字符串元素,和列表不同,集合元素不允许重复,集合中的元素是无须的,也不能通过索引下标获取元素。

    常用命令

    1. # 集合中添加元素
    2. $ sadd languages Nodejs JavaScript
    3. # 计算集合中元素个数
    4. $ scard languages
    5. (integer) 2
    6. # 判断集合中是否存在指定元素
    7. $ sismember languages Nodejs
    8. # 随机从集合中返回指定元素
    9. $ srandmember languages 2
    10. 1) "Nodejs"
    11. 2) "JavaScript"

    集合间操作

    1. # 设置用户 1 使用的语言
    2. $ sadd user:1 Nodejs JavaScript
    3. (integer) 2
    4. # 设置用户 2 使用的语言
    5. (integer) 2
    6. # 求 user:1 与 user:2 交集
    7. $ sinter user:1 user:2
    8. 1) "Nodejs"
    9. # 求 user:1 与 user:2 并集
    10. $ sunion user:1 user:2
    11. 1) "Python"
    12. 2) "Nodejs"
    13. 3) "JavaScript"
    14. # 求 user:1 与 user:2 差集
    15. $ sdiff user:1 user:2
    16. 1) "JavaScript"

    应用场景

    1. 抽奖

    Redis 的集合由于有去重功能,在一些抽奖类项目中可以存储中奖的用户 ID,能够保证同一个用户 ID 不会中奖两次。

    1. async function test(userId) {
    2. const key = `luck:users`;
    3. const result = await redis.sadd(key, userId);
    4. // 如果元素存在,返回 0 表示未添加成功
    5. if (result === 0) {
    6. console.log('您已中奖 1 次,无法再次参与');
    7. return false;
    8. }
    9. console.log('恭喜您中奖');
    10. return true;
    11. }
    12. test(1);

    2. 计算用户共同感兴趣的商品

    sadd + sinter 可用来统计用户共同感兴趣的商品,sadd 保存每个用户喜欢的商品标签,使用 sinter 对每个用户感兴趣的商品标签求交集。

    数据结构之有序集合(zset)

    Redis 的有序集合(zset)保留了集合(set)元素不能重复的特性之外,在有序集合的元素中是可以排序的,与列表使用索引下标不同的是有序集合是有序集合给每个元素设置一个分值(score)做为排序的依据。

    Redis 有序集合的命令都是以 Z 开头

    常用命令

    常用命令实践

    sadd

    Redis 3.2 对 zadd 增加了三个选项 [NX|XX]、[CH]、[INCR]:

    • [NX|XX]:NX,member 必须不存在才添加成功,用于 Create;XX,member 必须存在才可更新成功,用于 UPDATE。
    • [CH]:返回此次操作后有序集合元素和分数发生的变化
    • [INCR]:对 score 做增加,相当于 zincrby
    • score:代表分数(排序)
    • member:成员

    集合的增删改查

    1. # 有序集合 grades 中添加 3 个元素
    2. $ zadd grades NX 80 xiaoming 75 xiaozhang 85 xiaoli
    3. (integer) 3
    4. # 查看成员 xiaozhang 分数
    5. $ zscore grades xiaozhang
    6. "75"
    7. # 更新成员 xiaozhang 分数
    8. $ zadd grades XX 90 xiaozhang
    9. # 再次查看成员 xiaozhang 分数
    10. $ zscore grades xiaozhang
    11. "90"
    12. # 查看成员排名
    13. $ zrank grades xiaozhang # 分数从低到高返回排名
    14. (integer) 2
    15. $ zrevrank grades xiaozhang # 分数从高到底返回排名
    16. (integer) 0
    17. # 增加成员分数
    18. $ zincrby grades 5 xiaozhang
    19. "95"
    20. # 返回指定范围成员排名,WITHSCORES 可选参数,去掉则不反回分数
    21. $ zrange grades 0 2 WITHSCORES
    22. 1) "xiaoming"
    23. 2) "80"
    24. 3) "xiaoli"
    25. 4) "85"
    26. 5) "xiaozhang"
    27. 6) "95"
    28. # 返回指定分数范围内的成员列表
    29. $ zrangebyscore grades 85 100
    30. 1) "xiaoli"
    31. 2) "xiaozhang"
    32. # 删除指定成员

    应用场景

    Redis 的有序集合一个比较典型的应用场景就是排行榜,例如,游戏排行榜、用户抽奖活动排行榜、学生成绩排行榜等。