2022年 redis学习笔记

SDS设计与实现: 一个带长度信息的字节数组

Redis 全称 Remote Dictionary Server(即远程字典服务)

与其他内存型数据库相比,Redis 具有以下特点:

  • Redis 不仅可以将数据完全保存在内存中,还可以通过磁盘实现数据的持久存储;
  • Redis 支持丰富的数据类型,包括 string、list、set、zset、hash 等多种数据类型,因此它也被称为“数据结构服务器”;
  • Redis 支持主从同步,即 master-slave 主从复制模式。数据可以从主服务器向任意数量的从服务器上同步,有效地保证数据的安全性;
  • Redis 支持多种编程语言,包括 C、C++、Python、Java、PHP、Ruby、Lua 等语言。

Redis 不适合存储较大的文件或者二进制数据,否则会出现错误,Redis 适合存储较小的文本信息。理论上 Redis 的每个 key、value 的大小不超过 512 MB。
Redis最大可以存储1GB,而memcache只有1MB

优点

  1. 速度快
  2. 丰富的数据类型
  3. 原子性,支持事务
  4. 丰富特性:可以用作分布式锁;可以持久化数据;可以用作消息队列、排行榜、计数器;还支持publish/subscribe、通知、key过期等

三大用途

  • 数据库
    Redis本质是内存数据库,所以自然可以当做数据库来使用,但要注意的是内存空间是极其有限的,可不像硬盘那样浩瀚无垠,所以大多数情况下我们还是用关系型数据库+Redis缓存的方式运用Redis
  • 缓存
    比如Mysql,可承担的并发访问量有多大呢?答案是几百左右就会扛不住了,所以我们为了支持更高的并发,会使用缓存,为数据库筑起一道护盾,让大多数请求都发生在缓存这一层。Redis是把数据存储在内存上的,访问数据速度相当快,很适合做缓存。
  • 消息中间件
    Redis支持发布/订阅消息,当然真正的MQ我们一般在Rabbit,Rocket,kafka之间选一个,这并不是Redis的强项

0124

Key的操作

redis的key不是二进制安全的,key中不允许包含_边界字符_. key最大为512MB.

keys *                      # 查询当前库的所有键
keys pattern                # 返回匹配指定模式的所有key
exists runoob               # 判断某个键是否存在
type runoob                 # 查看键的类型
del runoob                  # 删除某个键
randomkey                   # 随机返回一个key

dump key                    # 序列化给定key数据,并返回被序列化的值

rename oldkey newkey        # 重命名key,1成功,0失败
nenamenx oldkey newkey      # 重命名key,如果newkey存在返回失败

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

expire runoob 23            # 为键值设置过期时间,单位秒。
expireat runoob timeStamp   # 给定时间戳过期
ttl runoob                  # 查看还有多少秒过期,-1表示永不过期 (-2表示已过期)
persist runoob              # 移除key的过期时间,将永久保持

move key db                 # 将当前数据库中的key移动到给定数据库db中

scan 0 Match s*             # 在0分页中查找s开头的key

Flushdb                     # 清空当前库(慎用!)
Flushall                    # 通杀全部库(删库跑路!!!忘了这个命令吧)

基础数据类型

1. String(字符串) [get,set,del,append]

String类型是二进制安全的,即可以包含任何数据,甚至jpg、序列化对象; 最大可存储512MB

  • 采用预分配冗余空间的方式来减少内存的频繁分配,
  • 内部为当前字符串实际分配的空间 capacity 一般要高于实际字符串长度 len。
  • 当字符串长度小于 1M 时,扩容都是加倍现有的空间,
  • 如果超过 1M,扩容时**一次只会多扩 1M **的空间。
  • 需要注意的是字符串最大长度为 512M。
    **注意:创建字符串时 len 和 capacity 一样长,不会多分配冗余空间,这是因为绝大多数场景下我们不会使用 append 操作来修改字符串。
set runoob "菜鸟教程"
setnx key value                 # key不存在时设置key的值
set key value ex 10             # set key value的同时,设置超期时间
get runoob
del runoob                      # 删除成功返回1, 否则返回0
setex key n_seconds value       # 设置键值的同时,指定过期时间,单位秒

getset key value                # 将key的值设为value, 并返回key的旧值
mget key1 [key2..]              # 获取多个key的值
mset key value [key value..]    # 设置多个键值对
msetnx key value 

strlen key                      # 值字符串长度

## 原子操作
incr key                        # 将key中存储数字值+1
decr key                        # -1, 只能怼数字值操作,如果为空,新增值为-1
incrby key increment            # 增加指定值increment
decrby key increment            # -increment

incrbyfloat key increment       # 增加浮点值increment

append key value                # 追加

getrange key start end          # 获取[start,end]范围的值,类似substring
setrange key start value        # 用value覆盖 start开始是字符串值
2. Hash(哈希) [hset, hget, hgetall, hdel]

一个 string 类型的 field(字段) 和 value(值) 的映射表,特别适合用于存储对象, 每个hash可存储2^32-1键值对

HMSET runoobkey name "redis tutorial" description "redis basic commands for caching" likes 20 visitors 2    # hashMapSet
HMGET runoobkey name likes      # 获取多个字段的值

HSET runoobkey age 32
HGET runoobkey name
HGET runoobkey likes
HSETNX runoobkey name "cplus"   # 只有在field[name]不存在时,设置哈希表字段的值

HGETALL runoobkey               # 获取指定key中所有字段[field-value]
HKEYS runoobkey                 # 获取哈希表中所有字段[field]
HVALS runoobkey                 # 获取哈希表中所有值[value]
HLEN runoobkey                  # 获取哈希表中字段field的数量

HDEL runoobkey name likes       # 删除一个或多个哈希表字段
HEXISTS runoobkey name          # 获取指定字段是否存在

HINCRBY key field increment     # 成功后返回变更后的值
HINCRBYFLOAT key field increment

HSCAN key cursor [match pattern # 迭代哈希表中的键值对
渐进式 rehash

redis中每一个增删改查命令中都会判断数据库字典中的哈希表是否正在进行渐进式rehash,如果是则帮助执行一次; 此外,redis还会在定时任务中对字典进行主动搬迁
扩容条件

  • 正常情况下,当 hash 表中元素的个数等于第一维数组的长度时,就会开始扩容,扩容的新数组是原数组大小的 2 倍
  • 不过如果 Redis 正在做 bgsave,为了减少内存页的过多分离 (Copy On Write),Redis 尽量不去扩容 (dict_can_resize),
  • 但是如果 hash 表已经非常满了,元素的个数已经达到了第一维数组长度的 5 倍 (dict_force_resize_ratio),说明 hash 表已经过于拥挤了,这个时候就会强制扩容。
    缩容条件
  • 当 hash 表因为元素的逐渐删除变得越来越稀疏时,Redis 会对 hash 表进行缩容来减少 hash 表的第一维数组空间占用
  • 缩容的条件是元素个数低于数组长度的 10%。
  • 缩容不会考虑 Redis 是否正在做 bgsave。
3. List(列表) [rpush, lrange, lindex, lpop]

简单的字符串列表,按照插入顺序排序。可以在头部尾部添加元素, 最多可存储 2^32-1 元素

底层实现:双向链表[ZipList +  QuickList]
  • 列表元素较少时采用Ziplist(压缩列表),类似数组:所有元素紧挨着一起存储,分配的是一块连续的内存
  • 数据量比较多时采用quickList(快速链表 = 链表 + ziplist)
    快速列表
rpush runoob cpp java golang python

lpush runoob redis          # 头部添加
rpush runoob mongodb        # 尾部添加
llen runoob                 # 返回对应list长度,不存在返回0
lset key index value        # 设置list指定下标的元素值

lrange runoob 0 10          # 列出前10个元素
lrange runoob 0 -1          # 读取所有数据,负值表示从后面计算,-1表倒数第一个元素,key不存在返回空列表

lindex runoob 1             # 读取指定位置数据

lpop runoob                 # 删除左端[头部]的值,并返回
rpop runoob

linsert key before|after pivot value    # 在列表的元素前或或插入元素

ltrim key start end         # 保留list指定区间内元素
4. Set(集合) [sadd, smembers, sismember, srem]

string类型的无序集合。 通过哈希表实现, 添加、删除、查找的复杂度都为O(1); 最多可存储 2^32-1 元素; 由于唯一性,二次插入的元素将会被忽略。

sadd runoob redis           # 成功返回1,如果元素已存在返回0
sadd runoob mongodb mysql

smembers runoob             # 返回集合runoob包含的所有元素
srandmember key count       # 随机选取key集合中count个元素,但不删除
sismember runoob php        # 检查给定元素php是否存在于集合runoob中,0代表不存在

scard runoob                # 返回集合中元素的个数

# 交inter、并union、差diff
sinter key1 key2 ... keyN           # 返回所有给定集合的交集
sinterstore dstkey key1 ... keyN    # 将所有给到你个结合的交集保存在dstkey下
sunion key1 key2 ... keyN           # 并集
sunionstore dstkey key1 ... keyN
sdiff key1 key2 ... keyN            # 差集
sdiffstore dstkey key1 ... keyN

srem runoob php [golang]    # 如果php存在集合runoob中,则移除; 可同时删除多个member
srem runoob java            # 如果不存在集合中,返回0,代表失败

spop key                    # 随机删除key中一个元素
spop key count              # 删除并返回key中随机的count个元素

smove srckey dstkey member  # 将srckey集合中的member移除并添加到dstkey中

sscan key cursor [match pattern]    # 迭代集合中的元素
5. zset(sorted set:有序集合) [zadd, zrange, zrangebyscore, zrem]

每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。
zset的成员是唯一的,但分数(score)却可以重复

zadd key score member

zadd runoob 0 redis
zadd runoob 3 mongodb

zcard key                               # 集合中元素个数
zscore key element                      # 返回key集合中元素element对应的score

zrange runoob 0 -1 withscores           # 获取数据, withscores 可有可无
zrevrange key start end

zrangebyscore key min max withscores    # 返回key中score在[min,max]间的元素
zrangebyscore runoob 0 800 withscores   # 获取分值范围的所有元素

zcount key min max                      # 返回集合中score在[min,max]间元素的数量

zincrby key incr member                 # 增加对应member的score值

zrank key member                        # 返回指定元素在集合中的排名[下标], 集合元素是按score从小到大排序的
zrevrank key member                     # 反序下标

zrem runoob redis                       # 移除元素  rem = remove
zremrangebyrank key min max             # 删除rank在[min,max]区间的元素
zremrangebyscore key min max            # 删除score在[min,max]区间的元素

zscan key cursor [match pattern]        # 迭代
6. HyperLogLog 用于计算基数 [pfadd, pfcount, pfmerge]

用来做基数统计的算法,
优点: 在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定 的、并且是很小的

  • 需要占据一定的存储空间12k, 所以不适合统计单个用户相关的数据
何为基数

比如数据集 {1, 3, 5, 7, 5, 7, 8}, 那么这个数据集的基数集为 {1, 3, 5 ,7, 8}, 基数(不重复元素)为5。 基数估计就是在误差可接受的范围内,快速计算基数。

pfadd runoobkey "redis"                 # 添加指定元素到 HyperLogLog 中
pfadd runoobkey "mongodb"

pfcount runoobkey                       # 返回给定元素的基数估算值[即,不重复元素个数]

pfmerge destkey sourcekey [sourceKey..] # 多个合并

发布 & 订阅

缺点: 消息无法持久化, 如出现网络断开、Redis宕机,消息会被丢弃

# 订阅方
subscribe SessInst
psubscribe cos.*            # 匹配所有以 cos.开头的频道
unsubscribe SessInst        # 退订

# 发布方
publish SessInst "hello world the msg"

事务 [可理解为一个打包的批量执行脚本,非原子化操作]

事务三大特性:

  1. 单独的隔离操作:执行过程中,不会被其他客户端发来的命令请求打断
  2. 没有隔离级别的概念:队列中的命令没有提交之前都不会被实际执行,会被放入队列缓存
  3. 不保证原子性:其中一条命令执行失败,后续命令仍会执行,没有回滚
multi                       # 开始一个事务
set book-name "cpp cook book"
get book-name
sadd tag "c++" "Programming"
smembers tag
exec                        # 触发事务,一并执行事务中所有命令

discard                     # 取消事务
watch tag                   # 监视一个或多个key, 如果在事务执行之前这个key被其他命令改动,则事务被打断
unwatch                     # 取消对所有 key 的监视

选择数据库

Redis默认支持16个数据库, (可以通过配置文件支持更多,无上限), 可以通过配置databases来修改这一数字。
客户端与Redis建立连接后会自动选择0号数据库,不过可以随时使用SELECT命令更换数据库

select 1    # 默认使用0号数据库

客户端连接数据库

语法redis-cli -h host -p port -a password

redis-cli               # 连接后使用 auth [passwd] 鉴权
redis-cli --raw         # 应对中文乱码
redis-cli -h 127.0.0.1 -p 6379 -a "mypass"

ping                    # 连接后测试可用性

auth "mypasswd"         # 鉴权

echo "hello world"      # 打印字符串

redis关闭

单实例关闭:redis-cli shutdown
也可以进入终端后再关闭: shutdown
多实例关闭,指定端口关闭: redis-cli -p 6379 shutdown

服务器端命令

info                    # 查看服务器统计信息

time                    # 服务器当前时间

客户端命令

client list             # 返回连接的客户端列表
client setname "clt1"   # 设置当前连接的名字
client getname          # 获取当前连接的名字

数据备份 与 恢复 [save]

数据备份
save                    # 在 redis 安装目录创建 dump.rdb 文件
bgsave                  # 在后台执行数据备份

config get dir          # 获取 dump.rdb 文件路径,即 redis 安装目录
数据恢复

如果需要恢复数据,只需将备份文件 (dump.rdb) 移动到 redis 安装目录并启动服务即可。

密码相关

config get requirepass  # 查看密码
config set requirepass  # 设置密码
author "mypasswd"       # 鉴权

redis 运维相关

# 1. 统计生产上比较大的key [--bigkeys]
redis-cli -p 7000 --bigkeys

# 2. 查看key的详细信息
debug object runoob
> Value at:0x7f30bd645600 refcount:1 encoding:quicklist serializedlength:50 lru:15637239 lru_seconds_idle:253930 ql_nodes:1 ql_avg_node:4.00 ql_ziplist_max:-2 ql_compressed:0 ql_uncompressed_size:48 

# 3. 分页查看redis中的key  【代替使用 keys * 】
scan 0      # 第一次迭代使用0作为游标,第二次迭代使用第一次迭代返回时的游标,直到命令再次返回游标0

# 4. 模糊查找key
scan 0 match t*     # 线上环境不建议使用keys *(没有分页,遍历所有key), 建议使用此处带分页的命令代替

# 5. 性能查询
redis-benchmark -n 10000

# 6. 找出拖慢 Redis 的原因
info commandstats   # 查看所有命令的统计快照
config resetstat    # 重置统计数据

持久化方法

redis拥有两种不同的持久化方法:

  • RDB(快照模式),即时间转储(在指定时间间隔进行快照存储)
  • AOF(日志追加),即将所有修改了数据库的命令都写入一个只追加文件

可同时开启两种持久化方式,此时,当 redis 重启时会优先载入AOF文件来恢复原始数据(通常情况下AOF比RDB保存到数据更完整)

安装 hiredis

sudo apt-cache search hiredis // 查看发现c语言开发库为libhiredis-dev
sudo apt-get install libhiredis-dev  //选择并安装

hiredis安装默认路径:

  • 库目录:/usr/lib/x86_64-linux-gnu/
  • 头文件:/usr/include/hiredis
API使用预览

hiredis头文件中定义了Redis的连接的方式redisConnect()等方法,连接信息存储在上下文redisContext的结构体对象中,通过redisCommand()等方法进行具体的数据库存取指令操作并返回相关信息在redisReply的结构体对象中,不要忘了freeReplyObject(void *reply)释放redisReply连接响应对象,redisFree()函数释放redisContext上下文对象

#include <hiredis/hiredis.h>
class Redis
{
public:
 
    Redis(){}
 
    ~Redis()
    {
        this->_connect = NULL;
        this->_reply = NULL;                
    }
 
    bool connect(std::string host, int port)
    {
        this->_connect = redisConnect(host.c_str(), port);
        if(this->_connect != NULL && this->_connect->err)
        {
            printf("connect error: %s\n", this->_connect->errstr);
            return 0;
        }
        return 1;
    }
 
    std::string get(std::string key)
    {
        this->_reply = (redisReply*)redisCommand(this->_connect, "GET %s", key.c_str());
        std::string str = this->_reply->str;
        freeReplyObject(this->_reply);
        return str;
    }
 
    void set(std::string key, std::string value)
    {
        redisCommand(this->_connect, "SET %s %s", key.c_str(), value.c_str());
    }
 
private:
 
    redisContext* _connect;
    redisReply* _reply;
 
};

注意:编译需指定使用redis库, 即-lhiredis
如:g++ redis.cpp -o redis -L/usr/local/lib/ -lhiredis

0107

redis C++ 使用

hiredis 常用4个API
/* 连接 redis 数据库 */
redisContext *redisConnect(const char *ip, int port);

/* 以带有超时的方式连接 redis 数据库 */
redisContext* redisConnectWithTimeout(const char *ip, int port, timeval tv);

/* 执行命令:
第一个参数为连接数据库时返回的redisContext,
剩下的参数为变参,就如C标准函数printf函数一样的变参
返回值为void*,一般强制转换成为redisReply类型,以便做进一步处理。
 */
void *redisCommand(redisContext *c, const char *format, ...);

/* 释放查询结果占用内存:释放redisCommand执行后返回的redisReply所占用的内存 */
void freeReplyObject(void *reply);

/* 释放redisConnect()所产生的连接 */
void redisFree(redisContext *c);
使用案例
#include <stdio.h>
#include <string.h>
//#include <assert.h>
#include <hiredis/hiredis.h>

void doTest()
{
    //redis默认监听端口为6387 可以再配置文件中修改
    redisContext *c = redisConnect("127.0.0.1", 6379);
    if (c->err)
    {
        redisFree(c);
        printf("Connect to redisServer faile\n");
        return;
    }
    printf("Connect to redisServer Success\n");

    redisReply *r = (redisReply *)redisCommand(c, "AUTH %s", "Itran_2430!@#3.1415926");
    if(r->type == REDIS_REPLY_ERROR)
    {
        printf("授权失败, reason:%s\n", r->str);
        redisFree(c);
        return;
    }

    const char *command1 = "set stest1 value1";
    r = (redisReply *)redisCommand(c, command1);

    if (NULL == r)
    {
        printf("Execut command1 failure\n");
        redisFree(c);
        return;
    }
    if (!(r->type == REDIS_REPLY_STATUS && strcasecmp(r->str, "OK") == 0))
    {
        printf("Failed to execute command[%s]\n", command1);
        freeReplyObject(r);
        redisFree(c);
        return;
    }
    freeReplyObject(r);
    printf("Succeed to execute command[%s]\n", command1);

    const char *command2 = "strlen stest1";
    r = (redisReply *)redisCommand(c, command2);
    if (r->type != REDIS_REPLY_INTEGER)
    {
        printf("Failed to execute command[%s]\n", command2);
        freeReplyObject(r);
        redisFree(c);
        return;
    }
    int length = r->integer;
    freeReplyObject(r);
    printf("The length of 'stest1' is %d.\n", length);
    printf("Succeed to execute command[%s]\n", command2);

    const char *command3 = "get stest1";
    r = (redisReply *)redisCommand(c, command3);
    if (r->type != REDIS_REPLY_STRING)
    {
        printf("Failed to execute command[%s]\n", command3);
        freeReplyObject(r);
        redisFree(c);
        return;
    }
    printf("The value of 'stest1' is %s\n", r->str);
    freeReplyObject(r);
    printf("Succeed to execute command[%s]\n", command3);

    const char *command4 = "get stest2";
    r = (redisReply *)redisCommand(c, command4);
    if (r->type != REDIS_REPLY_NIL)
    {
        printf("Failed to execute command[%s]\n", command4);
        freeReplyObject(r);
        redisFree(c);
        return;
    }
    freeReplyObject(r);
    printf("Succeed to execute command[%s]\n", command4);

    redisFree(c);
}

int main()
{
    doTest();
    return 0;
}

redis-py的使用

[准备工作] 安装python第三方库 redis-py

pip install redis

0. API说明
  • SELECT        : 没有实现
  • DEL           : del为python保留关键字,故使用delete代替
  • CONFIG GET|SET: 分别使用 config_getconfig_set 实现
  • MULTI/EXEC    : 作为 Pipeline 类的一部分实现, 调用pipeline方法时指定use_transaction=True
    **: 最好不要用 Redis,只是做兼容用的
通用API


1. python 直接操作 redis
import redis

r = redis.Redis(host="10.4.176.23", password="Itran_2430!@#3.1415926", port=6379, db=0)
# 或者 redis.StrictRedis(), 使用方法与上述相同, Redis是StrictRedis的子类

r.set("pycharm", "pyCharmValue...")
print(r.get("pycharm"))
2. 线程池ConnectionPool操作 redis

redis-py 使用connection pool 来管理对一个redis server的所有连接,避免每次建立,释放连接的开销。
默认,每个Redis实例都会维护一个自己的连接池。可以直接建立一个连接池,然后作为参数Redis,这样就可以实现多个Redis实例共享一个连接池。

import redis

pool = redis.ConnectionPool(host='192.168.11.122',password='123123',port=6379)

r = redis.Redis(connection_pool=pool)
r.set('name','Yu chao')
3. 管道pipeline操作 redis [事务]

redis-py默认在执行每次请求都会创建(连接池申请连接)和断开(归还连接池)一次连接操作。
如果想要在一次请求中指定多个命令,则可以使用pipline实现一次请求指定多个命令,并且默认情况下一次pipline 是原子性操作。

import redis
pool = redis.ConnectionPool(host='192.168.0.110', port=6379)
r = redis.Redis(connection_pool=pool)

pipe = r.pipeline(transaction=True)

r.set('name', 'zhangsan')
r.set('name', 'lisi')

pipe.execute()

所有缓冲到pipeline的命令返回pipeline对象本身,因此:
pipe.set('foo', 'bar').sadd('faz', 'baz').incr('auto_number').execute()

4. 发布与订阅

调用Redis客户端的 pubsub 方法返回一个 PubSub的实例,通过这个实例可以订阅频道或侦听消息
两个类(StrictRedis 和 PubSub 类)都可以发布(PUBLISH)消息

  • 首先定义一个RedisHelper类,连接Redis,定义频道为monitor,定义发布(publish)及订阅(subscribe)方法
import redis

class RedisHelper(object):
    def __init__(self):
        self.__conn = redis.Redis(host='192.168.0.110',port=6379)#连接Redis
        self.channel = 'monitor' #定义名称

    def publish(self,msg):#定义发布方法
        self.__conn.publish(self.channel,msg)   # 两个类(StrictRedis 和 PubSub 类)都可以发布(PUBLISH)消息
        return True

    def subscribe(self):#定义订阅方法
        pub = self.__conn.pubsub()              # 通过pubsub()方法返回一个 PubSub实例,用于定于频道、监听消息
        pub.subscribe(self.channel)
        pub.parse_response()
        return pub
  • 订阅者
from RedisHelper import RedisHelper

obj = RedisHelper()
redis_sub = obj.subscribe()#调用订阅方法

while True:
    msg= redis_sub.parse_response()
    print (msg)
  • 发布者
from RedisHelper import RedisHelper

obj = RedisHelper()
obj.publish('hello')#发布
参考资料

Python—redis 操作
Redis的Python客户端redis-py说明文档(转)
redis-py API 使用说明1
Redis for Python开发手册(重点讲解各API使用)
reids 常用API(python)

0126

布隆过滤器 [将一定不存在的对象过滤掉]

类似一个hash set, 用来判断某个元素(key)是否存在于某个集合中
和一般的hash set不同的是,这个算法无需存储key的值,对于每个key,只需要k个比特位,每个存储一个标志,用来判断key是否在集合中。

bloom filter原理图

优点:
  • 空间效率和查询时间都远远超过一般的算法,存储空间和插入/查询时间都是常数
  • Hash 函数相互之间没有关系,方便由硬件并行实现
两个缺点:
  1. 存在一定误判: 不同的两个对象可能得到相同的哈希值,存进布隆过滤器的元素越多,误判率越高
  2. 不能删除布隆过滤器里的元素: 删除会影响布隆过滤器里其他元素的判断结构
用途
  • 缓存穿透
  • 元素去重
  • web 拦截器
redis 官方插件 RedisBloom 使用
# 1. github拉取源码     # 推荐使用v2.2.1分支, 不推荐使用master版本,make会有问题 src/rm_tdigest.c:18:21: fatal error: tdigest.h: No such file or directory
git clone https://github.com/RedisBloom/RedisBloom.git

# 2. 编译生成 redisbloom.so
cd RedisBloom
make

# 3. 重启redis, 并加载布隆过滤器模块到服务器中
./src/redis-server --loadmodule /usr/local/RedisBloom/redisbloom.so
# 或将redisbloom写入配置文件
loadmodule /usr/local/redis-6.2.5/RedisBloom-2.2.1/redisbloom.so

# 4. 客户端连接测试
./src/redis-cli

bf.add userTbl sam
bf.add userTbl jack
bf.exists userTbl jack          # 1
bf.exists userTbl tom           # 0

bf.madd userTbl mike rose       # 1 1
bf.mexists userTbl blue mike    # 0 1
在Key-Value系统中布隆过滤器的典型使用

在Key-Value系统中布隆过滤器的典型使用

四、Redis存在的问题及解决方案

4.1 缓存数据库的双写一致性的问题
问题:一致性的问题是分布式系统中很常见的问题。一致性一般分为两种:强一致性和最终一致性,当我们要满足强一致性的时候,Redis也无法做到完美无瑕,因为数据库和缓存双写,肯定会出现不一致的情况,Redis只能保证最终一致性。

解决:我们如何保证最终一致性呢?

第一种方式是给缓存设置一定的过期时间,在缓存过期之后会自动查询数据库,保证数据库和缓存的一致性。
如果不设置过期时间的话,我们首先要选取正确的更新策略:先更新数据库再删除缓存。但我们删除缓存的时候也可能出现某些问题,所以需要将要删除的缓存的key放到消息队列中去,不断重试,直到删除成功为止。
4.2 缓存雪崩问题
问题: 我们应该都在电影里看到过雪崩,开始很平静,然后一瞬间就开始崩塌,具有很强的毁灭性。这里也是一样的,我们执行代码的时候将很多缓存的实效时间设定成一样,接着这些缓存在同一时间都会实效,然后都会重新访问数据库更新数据,这样会导致数据库连接数过多、压力过大而崩溃。

解决:

设置缓存过期时间的时候加一个随机值。
设置双缓存,缓存1设置缓存时间,缓存2不设置,1过期后直接返回缓存2,并且启动一个进程去更新缓存1和2。
4.3 缓存穿透问题
问题: 缓存穿透是指一些非正常用户(黑客)故意去请求缓存中不存在的数据,导致所有的请求都集中到到数据库上,从而导致数据库连接异常。

解决:

利用互斥锁。缓存失效的时候,不能直接访问数据库,而是要先获取到锁,才能去请求数据库。没得到锁,则休眠一段时间后重试。
采用异步更新策略。无论key是否取到值,都直接返回。value值中维护一个缓存失效时间,缓存如果过期,异步起一个线程去读数据库,更新缓存。需要做缓存预热(项目启动前,先加载缓存)操作。
提供一个能迅速判断请求是否有效的拦截机制。比如利用布隆过滤器,内部维护一系列合法有效的key,迅速判断出请求所携带的Key是否合法有效。如果不合法,则直接返回。
4.4 缓存的并发竞争问题
问题:

缓存并发竞争的问题,主要发生在多线程对某个key进行set的时候,这时会出现数据不一致的情况。

比如Redis中我们存着一个key为amount的值,它的value是100,两个线程同时都对value加100然后更新,正确的结果应该是变为300。但是两个线程拿到这个值的时候都是100,最后结果也就是200,这就导致了缓存的并发竞争问题。

解决

如果多线程操作没有顺序要求的话,我们可以设置一个分布式锁,然后多个线程去争夺锁,谁先抢到锁谁就可以先执行。这个分布式锁可以用zookeeper或者Redis本身去实现。
可以利用Redis的incr命令。
当我们的多线程操作需要顺序的时候,我们可以设置一个消息队列,把需要的操作加到消息队列中去,严格按照队列的先后执行命令。

五、Redis的过期策略

Redis随着数据的增多,内存占用率会持续变高,我们以为一些键到达设置的删除时间就会被删除,但是时间到了,内存的占用率还是很高,这是为什么呢?

Redis采用的是定期删除和惰性删除的内存淘汰机制。

5.1 定期删除
定期删除和定时删除是有区别的:

定时删除是必须严格按照设定的时间去删除缓存,这就需要我们设置一个定时器去不断地轮询所有的key,判断是否需要进行删除。但是这样的话cpu的资源会被大幅度地占据,资源的利用率变低。所以我们选择采用定期删除,。
定期删除是时间由我们定,我们可以每隔100ms进行检查,但还是不能检查所有的缓存,Redis还是会卡死,只能随机地去检查一部分缓存,但是这样会有一些缓存无法在规定时间内删除。这时惰性删除就派上用场了。
5.2 惰性删除
举个简单的例子:中学的时候,平时作业太多,根本做不完,老师说下节课要讲这个卷子,你们都做完了吧?其实有很多人没做完,所以需要在下节课之前赶紧补上。

惰性删除也是这个道理,我们的这个值按理说应该没了,但是它还在,当你要获取这个key的时候,发现这个key应该过期了,赶紧删了,然后返回一个’没有这个值,已经过期了!'。

现在我们有了定期删除 + 惰性删除的过期策略,就可以高枕无忧了吗?并不是这样的,如果这个key一直不访问,那么它会一直滞留,也是不合理的,这就需要我们的内存淘汰机制了。

5.3 Redis的内存淘汰机制
Redis的内存淘汰机制一般有6种,如下所示:

名称描述
volatile-lru从已设置过期时间的数据集中挑选最近最少使用的数据淘汰
volatile-lfu从已设置过期时间的数据集中挑选最不经常使用的数据淘汰
volatile-ttl从已设置过期时间的数据集中挑选将要过期的数据淘汰
volatile-random从已设置过期时间的数据集中挑选任意数据淘汰
allkeys-lru当内存不足写入新数据时淘汰最近最少使用的Key
allkeys-random当内存不足写入新数据时随机选择key淘汰
allkeys-lfu当内存不足写入新数据时移除最不经常使用的Key
no-eviction当内存不足写入新数据时,写入操作会报错,同时不删除数据

那么我们如何去配置Redis的内存淘汰机制呢?

在Redis.conf中我们可以进行配置

# 没有配置时,默认为no-eviction
maxmemory-policy allkeys-lru
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值