《Redis开发与运维》第二章 API的理解和使用(上)读书笔记

345 阅读10分钟

全局命令

  Redis有五种数据结构,它们是键值对中的值,对于键来说有一些通用的命令。

  • 查看所有的键(keys *keys *会遍历所有的键,生产环境禁止使用。
172.17.236.250:6379> set hello world
OK
172.17.236.250:6379> set java jedis
OK
172.17.236.250:6379> set python redis-py
OK
172.17.236.250:6379> keys *
1) "java"
2) "python"
3) "hello"
172.17.236.250:6379> 
  • 键总数(dbsizedbsize命令计算键总数时直接获取Redis内置的键总数变量,不会去遍历所有的键。
172.17.236.250:6379> dbsize
(integer) 3
  • 检查键是否存在(exists key) 存在返回1,不存在返回0。
172.17.236.250:6379> exists java
(integer) 1
172.17.236.250:6379> exists pp
(integer) 0
  • 删除键(del key \[key ...\]) del无论值是什么类型,del命令都可以将其删除。也可以支持删除多个键。返回结果为删除成功的键数,如果删除一个不存在的键则返回0。
172.17.236.250:6379> del java
(integer) 1
172.17.236.250:6379> del python hello
(integer) 2
172.17.236.250:6379> exists java
(integer) 0
  • 键过期(expire key seconds) Redis支持对键添加过期时间,当超过过期时间后,会自动删除键,例如为键hello设置10秒过期时间。
172.17.236.250:6379> set hello world
OK
172.17.236.250:6379> expire hello 10
(integer) 1
  • 返回键过期的剩余时间(ttl keyttl key有三种返回值:   大于等于0的整数:键剩余的过期时间;   -1:键没有设置过期时间;   -2:键不存在。
172.17.236.250:6379> set hello world
OK
172.17.236.250:6379> ttl helo
(integer) -2
172.17.236.250:6379> ttl hello
(integer) -1
172.17.236.250:6379> expire hello 10
(integer) 1
172.17.236.250:6379> ttl hello
(integer) 5
  • 键的数据结构类型(type keytype key会显示键的数据结构类型,如果键不存在则返回结果为none

数据结构和内部编码

  type key命令实际返回的就是键当前的数据结构类型,分别是string(字符串)、hash(哈希)、list(列表)、set(集合)、zset(有序集合)。但是这些都是Redis对外的数据结构。如下图所示:

在这里插入图片描述
  每一种数据结构都有自己的底层内部编码实现,并且是多种实现,Redis会根据具体的场景去选择合适的内部编码。如下图所示:
在这里插入图片描述

  • 查看内部编码(object encoding key),例如查看键hello对应值的内部编码
172.17.236.250:6379> set hello world
OK
172.17.236.250:6379> object encoding hello
"embstr"
  • 设计优点
    • 可以改进内部编码,但是对外部的数据结构无影响;
    • 多种内部编码实现可以在不同场景下发挥各自的优势;

单线程架构

  • 开启两个客户端同时执行命令:
127.0.0.1:6379> incr counter
127.0.0.1:6379> incr counter

  看到如下Redis客户端与服务端的简化模型图:

在这里插入图片描述
  因为Redis是单线程处理命令,所以当一条命令到达服务端是不会被立即执行,所有的命令都会进入到一个队列中,然后再逐步执行,所以上面两个命令的执行顺序是不确定的。如下图所示:
在这里插入图片描述
  但是可以确定的是不会有两条命令同时执行,所以两条incr命令不管是怎么执行都是2,不会产生并发问题。

为什么Redis单线程还能那么快?

  • Redis将所有的数据存在内存中,是纯内存操作;
  • 非阻塞I/O,Redis使用epoll作为I/O多路复用技术实现,再加上Redis自身的事件处理模型将epoll中的连接、读写、关闭都转行为事件,不会在网络I/O上浪费时间。如下图:
    在这里插入图片描述
  • 单线程避免了线程切换和竞态产生消耗;

单线程的好处:

  • 简化数据结构和算法实现;
  • 避免了线程切换和竞态的消耗;

单线程的问题:

  • 如果某个命令执行过长,会造成其它命令阻塞,这对Redis是致命的。

字符串

  字符串是Redis最基础的数据结构。首先键都是字符串类型,并且其他数据结构都是在字符串类型的基础上进行构建的。   字符串类型的值可以为:

  • 字符串(简单的字符串和复杂的字符串(例如:JSON、XML))
  • 数字(整数、浮点数)
  • 二进制(音视频或图片(不能超过512MB))
    在这里插入图片描述

字符串常用操作命令

设置值
  • 设置成功则返回OK
set key value \[ex seconds\] \[px milliseconds] \[nx|xx\]

set命令有以下几个选项:

  • ex seconds:为键设置秒级过期时间;
  • px milliseconds:为键设置毫秒级过期时间;
  • nx:键必须不存在才可以设置成功;
  • xx:与nx相反,键必须存在,才可以设置成功; 除了set之外,Redis还提供了setexsetnx两个命令,作用和set命令的exnx选项一样。如下:
setex key seconds value
setnx key value

用例子说明set、setnx、set xx的区别:

##键hello不存在
172.17.236.250:6379> exists hello
(integer) 0
##set xx更新键hello的值返回nil
172.17.236.250:6379> set hello redis xx
(nil)
##为键hello设置值
172.17.236.250:6379> set hello world
OK
##setnx为键hello设置值,因为键hello已存在所以设置失败返回0
172.17.236.250:6379> setnx hello redis
(integer) 0
##set xx为键hello更新值成功
172.17.236.250:6379> set hello jedis xx
OK
172.17.236.250:6379> get hello
"jedis"
  • 批量设置值
###mset key value \[key value ...\]
172.17.236.250:6379> mset a 1 b 2 c 3 d 4
OK

  在实际的应用场景中,由于Redis是单线程的,如果有多个客户端同时执行setnx key value,根据setnx的特性只有一个客户端能设置成功,setnx可以作为分布式锁的一个实现方案(Redis官方使用 setnx 实现分布式锁方案传送门

获取值
  • 获取单个值,如果要获取的键不存在则返回nil
get key
  • 批量获取值(如果获取的键不存在则返回nil
mget key \[key ... \]
172.17.236.250:6379> mget a b c d
1) "1"
2) "2"
3) "3"
4) "4"

  批量操作命令可以提高开发效率,如果没有mget这样的命令,要执行n次get命令会按照下图方式执行,具体耗时如下:

n次get时间 = n次网络时间 + n次命令执行时间

在这里插入图片描述
  使用mget命令会按照下图方式执行,具体耗时如下:

n次get时间 = 1次网络时间 + n次命令时间

在这里插入图片描述
  Redis一次命令的执行时机包括网络时间和命令执行时间,Redis服务端的执行速度已经够快。网络可能会成为性能瓶颈。在实际开发场景中,每次批量操作发送的命令数不是无节制的,数量过多可能会造成Redis阻塞或网络堵塞

计数
incr key

incr命令用于对值进行自增操作,返回情况有三种:

  • 值不是整数,返回错误;
  • 值是整数,返回自增后的结果;
  • 键不存在,按照值为0自增,返回结果为1。   示例如下:
### 键 a 不存在
172.17.236.250:6379> exists a
(integer) 0
### 对不存在的键 a 进行自增,返回结果为 1
172.17.236.250:6379> incr a
(integer) 1
### 再次对 a 进行自增操作,返回结果为 2
172.17.236.250:6379> incr a
(integer) 2
### 值不是整数,报错
172.17.236.250:6379> set hello world
OK
172.17.236.250:6379> incr hello
(error) ERR value is not an integer or out of range

  除了incr命令,Redis提供了decr自减、incrby自增指定数字、decrby自减指定数字、incrbyfloat自增浮点数。如下所示:

decr key
incrby key increment
decrby key increment
incrbyfloat key increment

172.17.236.250:6379> decr a
(integer) 1
172.17.236.250:6379> incrby a 5
(integer) 6
172.17.236.250:6379> decrby a 5
(integer) 1
172.17.236.250:6379> incrbyfloat a 4.9
"5.9"

字符串不常用操作命令

追加值
append key value

  append可以向字符串尾部追加值,如下所示:

172.17.236.250:6379> set hello world
OK
172.17.236.250:6379> get hello
"world"
172.17.236.250:6379> append hello redis
(integer) 10
172.17.236.250:6379> get hello
"worldredis"
字符串长度
strlen key

  当前值为worldredis,所以返回长度为10(中文占用3个字节),如下:

172.17.236.250:6379> strlen hello
(integer) 10
设置并返回原值
getset key value

  getsetset会设置值,不同的是,它会返回这个键原来的值,如下:

172.17.236.250:6379> getset hello world
(nil)
172.17.236.250:6379> getset hello redis
"world"
设置指定位置的字符串
setrange key offeset value

  将"adcd"变成"ddcd":

172.17.236.250:6379> set redis adcd
OK
172.17.236.250:6379> setrange redis 0 d
(integer) 4
172.17.236.250:6379> get redis
"ddcd"
获取部分字符串
getrange key start end

  start和end分别是开始和结束的偏移量,偏移量从0开始计算,如下:

172.17.236.250:6379> getrange redis 0 1
"dd"
字符串类型命令时间复杂度表

在这里插入图片描述

内部编码

字符串的内部编码有3种:

  • int:8个字节的长整型;
  • embstr:小于等于39个字节的字符串;
  • raw:大于39个字节的字符串。 Redis会根据当前值的类型和长度决定使用哪种内部编码实现。如下所示:
#### int 类型
172.17.236.250:6379> set key 81
OK
172.17.236.250:6379> object encoding key
"int"
#### embstr 类型
172.17.236.250:6379> set key hello,world
OK
172.17.236.250:6379> object encoding key
"embstr"
#### raw 类型
172.17.236.250:6379> set key qwertyuiopasdfghjklzxcvbnmqwertyuioplkjhgfdsaczvxbnm
OK
172.17.236.250:6379> object encoding key
"raw"

典型使用场景

缓存功能

  Redis作为缓存层,MySQL作为存储层,绝大部分请求的数据都是从Redis中获取。因为Redis有支撑高并发的特性,所以缓存能起到加速读写和降低后端压力的作用。如下图:

在这里插入图片描述
思路如下:

  • 定义获取用户基础信息函数
  • 首先从Redis获取用户信息
  • 若没有从Redis中获取到用户信息,需要从MySQL中进行获取,并且将结果写入Redis中,添加过期时间
  • 伪代码如下:
UserInfo getUserInfo(long id) {
    //定义键
    userRedisKey = "user:info:"+id;
    //从Redis获取值
    value = redis.get(userRedisKey);
    if(value!=null){
         //  将值进行反序列化为UserInfo并返回结果
        userInfo = deserialize(value);
        return userInfo;
    } else {
        // 从MySQL获取用户信息
        userInfo = mysql.get(id);
        // 将userInfo序列化,设置3600秒过期时间,存入Redis
        if (userInfo != null) {
            redis.setex(userRediskey,3600,serialize(userInfo));
        }
    }
}

  注意:Redis没有命令空间,也没有对键名有强制要求(除了不能使用一些特殊字符)。键名要设计合理,有利于防止键冲突项目的可维护性。推荐使用"业务名: 对象名:id:[属性]"作为键名。可以在能描述键的含义下适当减少键的长度,从而减少键过长而造成的内存浪费。

计数

  使用Redis作为计数的基础工具,他可以实现快速计数、查询缓存功能,同时数据可以异步落地到其他数据源。 伪代码如下:

long incrVideoCounter(long id) {
    key = "video:playCount:"+id;
    return redis.incr(key);
}

  在实际开发中,计数系统要考虑很多东西:防作弊、按照不同的维度计数,数据持久化到底层数据源等等。

共享session

  一个分布式Web服务将用户的session信息保存在各自的服务器上,但是出于负载均衡的考虑,分布式服务会将用户的访问均衡到不同的服务器上,这样就导致用户刷新一次访问可能就会发现需要重新登录。如下图:

在这里插入图片描述
  为了解决这个问题,开发人员可以使用Redis将用户的session进行集中的管理。在这种模式下只需要保证Redis是高可用和扩展性的,每次用户更新或者查询登录信息都可以直接从Redis种获取。如下图:
在这里插入图片描述

限速

  很多应用在每次进行登录时,会让用户输入手机验证码来确定是否是用户本人。为了让短信接口不被频繁访问,会限制用户在每分钟的获取验证码的频率,例如一分钟不能超过5次。如下图:

在这里插入图片描述
  伪代码如下:

phoneNum=""135xxxxxxxx;
key = "shortMsg:limit:"+phoneNum;
isExists = redis.set(key,1,"EX 60","NX");
if(isExists != null || redis.incr(key) <= 5) {
    //通过
} else {
    //限速
}

  例如某些网站不能在1秒之内访问超过n次也可以采用类似的思路。   除了以上几种应用场景,字符串还有很多的应用场景,这需要我们结合字符串提供的相应命令去决定怎样使用。