redis (单线程+多路IO复用)

231 阅读10分钟

安装

参考tsuiraku.com/2021/08/19/…

---安装
yum install centos-release-scl scl-utils-build
yum install -y devtoolset-8-toolchain
scl enable devtoolset-8 bash```
wget https://download.redis.io/releases/redis-6.2.6.tar.gz
cd redis-6.2.6
make && make install
------后台启动

cp redis.conf /etc/redis.conf
// 将redis.conf复制到/etc/下

vim redis.conf# daemonize no 修改为 daemonize yes

redis-server /etc/redis.conf


key操作

keys *:查看当前库所有 key

exists key:判断某个 key 是否存在

type key:查看你的 key 是什么类型

del key :删除指定的 key 数据

unlink key:根据 value 选择非阻塞删除,仅将 keys 从 keyspace 元数据中删除,真正的删除会在后续异步操作

expire key 10 :为给定的 key 设置过期时间

ttl key:查看还有多少秒过期,-1表示永不过期,-2表示已过期

select:命令切换数据库

dbsize:查看当前数据库的 key 的数量

flushdb:清空当前库

flushall:通杀全部库

常用五大数据类型

String

set <key><value>:添加键值对

get <key>:查询对应键值

append <key><value>:将给定的  * * 追加到原值的末尾

strlen <key>:获得值的长度

setnx <key><value>:只有在 key 不存在时,设置 key 的值

incr <key>:将 key 中储存的数字值增 1,只能对数字值操作,如果为空,新增值为 1(具有原子性

decr <key>:将 key 中储存的数字值减 1,只能对数字值操作,如果为空,新增值为 -1

incrby/decrby <key><步长>:将 key 中储存的数字值增减。自定义步长

mset <key1><value1><key2><value2> :同时设置一个或多个 key-value 对

mget <key1><key2><key3>...:同时获取一个或多个 value

msetnx <key1><value1><key2><value2>... :同时设置一个或多个 key-value 对,当且仅当所有给定 key 都不存在

getrange <key><起始位置><结束位置>:获得值的范围

setrange <key><起始位置><value>:用  * * 覆写  * * 所储存的字符串值

setex <key><过期时间><value>:设置键值的同时,设置过期时间,单位秒。

getset <key><value>:以新换旧,设置了新值同时获得旧值。

list

lpush/rpush <key><value1><value2><value3> ....: 从左边/右边插入一个或多个值。

lpush k1 v1 v2 v3lrange k1 0 -1输出:v3 v2 v1rpush k1 v1 v2 v3rrange k1 0 -1输出:v1 v2 v3

lpop/rpop <key>:从左边/右边吐出一个值。值在键在,值光键亡。

rpoplpush <key1><key2>:从  * * 列表右边吐出一个值,插到  * * 列表左边。

lrange <key><start><stop>:按照索引下标获得元素(从左到右)

lrange mylist 0 -1 0:左边第一个,-1右边第一个,(0 -1表示获取所有)

lindex <key><index>:按照索引下标获得元素(从左到右)

llen <key>:获得列表长度

linsert <key> before/after <value><newvalue>:在  * * 的前面/后面插入  * * 插入值

lrem <key><n><value>:从左边删除 n 个 value(从左到右)

lset<key><index><value>:将列表 key 下标为 index 的值替换成 value

set

sadd <key><value1><value2> ..... :将一个或多个 member 元素加入到集合 key 中,已经存在的 member 元素将被忽略

smembers <key>:取出该集合的所有值。

sismember <key><value>:判断集合  * * 是否为含有该  * * 值,有返回 1,没有返回 0

scard<key>:返回该集合的元素个数。

srem <key><value1><value2> ....:删除集合中的某个元素

spop <key>:随机从该集合中吐出一个值

srandmember <key><n>:随机从该集合中取出 n 个值,不会从集合中删除

smove <source><destination>value:把集合中一个值从一个集合移动到另一个集合

sinter <key1><key2>:返回两个集合的交集元素

sunion <key1><key2>:返回两个集合的并集元素

sdiff <key1><key2>:返回两个集合的差集元素(key1 中的,不包含 key2 中的)

hash(常用于存对象)

hset <key><field><value>:给  * * 集合中的  * * 键赋值 

hget <key1><field>:从  * * 集合  * * 取出 value

hmset <key1><field1><value1><field2><value2>...: 批量设置 hash 的值

hexists <key1><field>:查看哈希表 key 中,给定域 field 是否存在

hkeys <key>:列出该 hash 集合的所有 field

hvals <key>:列出该 hash 集合的所有 value

hincrby <key><field><increment>:为哈希表 key 中的域 field 的值加上增量 1 -1

hsetnx <key><field><value>:将哈希表 key 中的域 field 的值设置为 value ,当且仅当域 field 不存在

zset(常用于排名)

zadd <key><score1><value1><score2><value2>…:将一个或多个 member 元素及其 score 值加入到有序集 key 当中

zrange <key><start><stop> [WITHSCORES] :返回有序集 key 中,下标在  * * 之间的元素

当带 WITHSCORES,可以让分数一起和值返回到结果集

zrangebyscore key min max [withscores] [limit offset count]:返回有序集 key 中,所有 score 值介于 min 和 max 之间(包括等于 min 或 max )的成员。有序集成员按 score 值递增(从小到大)次序排列。

zrevrangebyscore key max min [withscores] [limit offset count] :同上,改为从大到小排列

zincrby <key><increment><value>:为元素的 score 加上增量

zrem <key><value>:删除该集合下,指定值的元素

zcount <key><min><max>:统计该集合,分数区间内的元素个数

zrank <key><value>:返回该值在集合中的排名,从 0 开始

事务

MultiExecDiscard

从输入 Multi 命令开始,输入的命令都会依次进入命令队列中,但不会执行,直到输入 Exec 后,Redis 会将之前的命令队列中的命令依次执行。

组队的过程中可以通过 Discard 来放弃组队。

multi过程中出现错误,全部失败。

multi过程中没出现错误,执行过程出现错误,错误那行指令失败,其他成功

Watch、unwatch(乐观锁)

在执行 multi 之前,先执行 **watch key1 [key2] ,可以监视一个(或多个 ) key 。如果在事务执行之前这个 key 被其他命令所改动,那么事务将被打断。

取消 WATCH 命令对所有 key 的监视。如果在执行 WATCH 命令之后,EXEC 命令或 DISCARD 命令先被执行,那么就不需要再执行 UNWATCH

事务三特性

  • 单独的隔离操作

    事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。

  • 没有隔离级别的概念

    队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行。

  • 不保证原子性

    事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚 。

秒杀案例

** 逻辑中要解决的几个问题:库存为0秒杀失败,同一个用户秒杀成功之后不能再秒杀

redis中要解决的问题:超卖(watch监听+multi锁 ),库存遗留(1.lua脚本,利用redis的单线程。2.synchronized(效率低不推荐))**

public String test() {
    //模拟用户ID
    int uid = new Random().nextInt(10000);
    //3 拼接key
    // 3.1 库存key
    String kcKey = "sk:1001:qt";
    // 3.2 秒杀成功用户key
    String userKey = "sk:" + 1001 + ":user";
    Integer kc = (Integer) redisTemplate.opsForValue().get(kcKey);
    if (kc == null) {
        log.info("秒杀还没有开始,请等待");
        return "秒杀还没有开始,请等待";
    }
    if (redisTemplate.opsForSet().isMember(userKey, uid)) {
        log.info("已经秒杀成功了,不能重复秒杀");
        return "已经秒杀成功了,不能重复秒杀";
    }
    if (kc <= 0) {
        log.info("已经抢完了");
        return "已经抢完了";
    }
    //开抢(走到这里应该是有库存)
    redisTemplate.watch(kcKey);
    redisTemplate.multi();
    redisTemplate.opsForValue().decrement(kcKey);


    redisTemplate.opsForSet().add(userKey, uid);


    List exec = redisTemplate.exec();


    if ((exec == null || exec.isEmpty())) {
        //  kc = (Integer) redisTemplate.opsForValue().get(kcKey);
        // if (kc <= 0) {
        log.info("秒杀失败了....");
        return "秒杀失败了....";
        //   }
        //redisTemplate.watch(kcKey);
        //redisTemplate.multi();
        //redisTemplate.opsForValue().decrement(kcKey);
        //exec = redisTemplate.exec();
    }
    //成功
    log.info("秒杀成功了....");
    return "秒杀成功了....";
}

-----------------使用while解决库存遗留,自测成功

public String test() {
    //模拟用户ID
    int uid = new Random().nextInt(10000);
    //3 拼接key
    // 3.1 库存key
    String kcKey = "sk:1001:qt";
    // 3.2 秒杀成功用户key
    String userKey = "sk:" + 1001 + ":user";
    redisTemplate.watch(kcKey);
    Integer kc = (Integer) redisTemplate.opsForValue().get(kcKey);
    if (kc == null) {
        log.info("秒杀还没有开始,请等待");
        return "秒杀还没有开始,请等待";
    }
    if (redisTemplate.opsForSet().isMember(userKey, uid)) {
        log.info("已经秒杀成功了,不能重复秒杀");
        return "已经秒杀成功了,不能重复秒杀";
    }
    if (kc <= 0) {
        log.info("已经抢完了");
        return "已经抢完了";
    }
    //开抢(走到这里应该是有库存)

    redisTemplate.multi();
    redisTemplate.opsForValue().decrement(kcKey);
    redisTemplate.opsForSet().add(userKey, uid);
    List exec = redisTemplate.exec();
    //while解决库存遗留
    while ((exec == null || exec.isEmpty())) {
        redisTemplate.watch(kcKey);
        kc = (Integer) redisTemplate.opsForValue().get(kcKey);
        if (kc <= 0) {
        log.info("秒杀失败了....");
        return "秒杀失败了....";
           }
        redisTemplate.multi();
        redisTemplate.opsForValue().decrement(kcKey);
        exec = redisTemplate.exec();
    }
    //成功
    log.info("秒杀成功了....");
    return "秒杀成功了....";
}

持久化

RDB

在指定的时间间隔内将内存中的数据集快照写入磁盘, 即 Snapshot 快照,恢复时是将快照文件直接读到内存里。

Redis 会单独创建一个子进程(fork)来进行持久化。

先将数据写入到一个临时文件中,待持久化过程完成后,再将这个临时文件内容覆盖到 dump.rdb

整个过程中,主进程是不进行任何 IO 操作的,这就确保了极高的性能。如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那 RDB 方式要比 AOF 方式更加的高效。

RDB 的缺点是最后一次持久化后的数据可能丢失

Fork

  • 作用是复制一个与当前进程一样的进程。新进程的所有数据(变量、环境变量、程序计数器等) 数值都和原进程一致,但是是一个全新的进程,并作为原进程的子进程
  • 在 Linux 程序中,*fork() * 会产生一个和父进程完全相同的子进程,但子进程在此后多会 exec 系统调用,出于效率考虑,Linux 中引入了 写时复制技术
  • 一般情况父进程和子进程会共用同一段物理内存,只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程

配置

dump 文件名字

在 redis.conf 中配置文件名称,默认为 dump.rdb

dump 保存位置

rdb 文件的保存路径可以修改。默认为 Redis 启动时命令行所在的目录下。

stop-writes-on-bgsave-error

即当 redis 无法写入磁盘,关闭 redis 的写入操作。

rdbcompression

持久化的文件是否进行压缩存储。

rdbchecksum

完整性的检查,即数据是否完整性、准确性。

save

表示写操作的次数。

| 1 | ``` 格式:save 秒 写操作次数 Copy

  • 适合大规模的数据恢复;
  • 对数据完整性和一致性要求不高更适合使用;
  • 节省磁盘空间;
  • 恢复速度快。

dump.rdb文件什么时候生成

1.根据配置文件里的save 60(秒) 3(3次) 60秒key修改三次自动触发生成dump.rdb

2.shutdown命令 关闭redis自动触发

3.flushdb,flushall自动触发

缺点

  • Fork 的时候,内存中的数据被克隆了一份,大致 2 倍的膨胀性需要考虑;
  • 虽然 Redis 在 fork 时使用了写时拷贝技术,但是如果数据庞大时还是比较消耗性能;
  • 在备份周期在一定间隔时间做一次备份,所以如果 Redis 意外 down 掉的话,就会丢失最后一次快照后的所有修改。

AOF

以日志的形式来记录每个写操作(增量保存),将 Redis 执行过的所有写指令记录下来(读操作不记录), 只许追加文件但不可以改写文件,Redis 启动之初会读取该文件重新构建数据,换言之,如果 Redis 重启就会根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。

执行流程

  • 客户端的请求写命令会被 append 追加到 AOF 缓冲区内;
  • AOF 缓冲区根据 AOF 持久化策略 [always,everysec,no] 将操作 sync 同步到磁盘的 AOF 文件中;
  • AOF 文件大小超过重写策略或手动重写时,会对 AOF 文件 Rewrite 重写,压缩 AOF 文件容量;
  • Redis 服务重启时,会重新 load 加载 AOF 文件中的写操作达到数据恢复的目的。

AOF 和 RDB 同时开启时,系统默认读取 AOF 的数据(数据不会存在丢失)

配置

AOF 默认不开启

文件名字

AOF 同步频率设置

appendfsync always

​ 始终同步,每次 Redis 的写入都会立刻记入日志;

​ 性能较差但数据完整性比较好。

appendfsync everysec

​ 每秒同步,每秒记入日志一次,如果宕机,本秒的数据可能丢失。

appendfsync no

​ Redis 不主动进行同步,把同步时机交给操作系统。

Rewrite 压缩

当 AOF 文件的大小超过所设定的阈值时,Redis 就会启动 AOF 文件的内容压缩,只保留可以恢复数据的最小指令集。可以使用命令 bgrewriteaof

优点

  • 备份机制更稳健,丢失数据概率更低;
  • 可读的日志文本,通过操作 AOF 稳健,可以处理误操作。

缺点

  • 比起 RDB 占用更多的磁盘空间;
  • 恢复备份速度要慢;
  • 每次读写都同步的话,有一定的性能压力;
  • 存在个别 Bug,造成不能恢复。

缓存穿透

在redis中查询一个不存在的key,黑客利用这点可以压垮mysql

解决方案:

  • 对空值缓存

    如果一个查询返回的数据为空(不管是数据是否不存在),仍然把这个空结果(null)进行缓存,设置空结果的过期时间会很短,最长不超过五分钟。

  • 布隆过滤器、bitmaps 把存在的key,或者黑名单,白名单,放进布隆过滤器,布隆过滤器可以判断key是否存在,存在一定误差性。

缓存击穿

key对应数据存在,但在redis过期,短时间有大量请求访问,比如双11,微博热点 解决方案:

  • 预先设置热门数据

在 redis 高峰访问之前,把一些热门数据提前存入到 redis 里面,加大这些热门数据 key 的时长。

  • 使用setnx分布式锁 获得锁才能访问mysql

缓存雪崩

key 对应的数据存在,但在 redis 中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB 加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端 DB 压垮。

缓存雪崩与缓存击穿的区别在于这里针对很多 key 缓存,前者则是某一个 key

将缓存失效时间分散开:

比如我们可以在原有的失效时间基础上增加一个随机值,比如 1~5 分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件

  • 使用锁或队列:

    用加锁或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。不适用高并发情况。

集群

参考tsuiraku.com/2021/08/19/… ,这里不作记录