redis常用结构和奇淫技巧

Redis可以做什么

  1. 记录帖子的点赞数、评论数和点击数 (hash)。

  2. 记录用户的帖子 ID 列表 (排序),便于快速显示用户的帖子列表 (zset)。

  3. 记录帖子的标题、摘要、作者和封面信息,用于列表页展示 (hash)。

  4. 记录帖子的点赞用户 ID 列表,评论 ID 列表,用于显示和去重计数 (zset)。

  5. 缓存近期热帖内容 (帖子内容空间占用比较大),减少数据库压力 (hash)。

  6. 记录帖子的相关文章 ID,根据内容推荐相关帖子 (list)。

  7. 如果帖子 ID 是整数自增的,可以使用 Redis 来分配帖子 ID(计数器)。

  8. 收藏集和帖子之间的关系 (zset)。

  9. 记录热榜帖子 ID 列表,总热榜和分类热榜 (zset)。

  10. 缓存用户行为历史,进行恶意行为过滤 (zset,hash)。

  11. 数据推送去重Bloom filter

  12. pv,uv统计


Redis5种基本数据对象


他们是:字符串+列表+集合+哈希+有序集合

String字符串
比如缓存用户信息,需要使用JSON序列化成字符串存入缓存。取用户信息时又会经过一次反序列化的过程。

redis是动态字符串,最大512M,小于1M时加倍现有空间,大于1M时加倍1M的空间

set key userinfo;
get key;


批量对多个字符串读写,节省网络耗时

mset key1 value1 key2 value2
mget key1 key2

设置key的过期时间-涉及到过期策略

##  设置5秒过期时间
set key1 value1
expire key1 5
##  合并set+expire
setex key2 5 value2
##  如果存在key3就不执行
setnx  key3 value3

计数功能
超过sigined long的最大值最小值,会报错

## 自增key1的值
set key1 50;
incr key1; 
## 合并指令
incrby key1 50;


list列表-相当于LinkedList
插入删除块,查找慢
作用:常用作异步队列使用。将需要延后处理的任务序列化成字符串放入redis,另一个线程从列表轮询进行执行。

队列-右边进左边出

rpush key1 value1 value2 value3
lpop key1
"value1"
lpop key1
"value2"
llen key1

栈-右边进右边出

rpush key1 value1 value2 value3
rpop key1
"value3"
lpop key1
"value2"

ltrim截取
##截取这个区间的值(0表示第一个数 -1表示最后一个数)
ltrim start_index end_index   

快速例表:ziplist<——>ziplist<——>ziplist<——>ziplist<——>ziplist

hash字典-相当于hashmap
redis字典只能是字符串,与hashmap的重哈希方式不一样,redis为了提高性能,采用了渐进式的rehash策略
渐进式的rehash会保留新旧两个hash结构,然后在后续定时任务以及hash指令中,逐渐将旧内容迁移到新hash结构。
hash结构也可以存储用户信息,不同于String串一次性序列化整个对象,hash字典只序列化某些字段实现部分获取。

hset object key1 value1;
hset object key2 value2;
hgetall object;
hlen object;
hget object key1;
hincry object key1 1; //字典中单个key也可单独自增

set集合-相当于hashSet
用来存储中奖用户的id,因为有去重的功能,保证一个用户不会中将两次

sadd key1 value1;
sadd key1 value2;
spop key1;
smembers key1          //查询键为key1的所有值
"value1"
sismember key1 value1; //查询某value是否存在
scard key1             //查询键为key1值得个数

zset有序集合-相当于sortedSet
它是一个set保证value得唯一性,而且它可以给每个value赋一个排序权重。
存储一对多得数据,并且对多的一方按照权重进行排序

存储粉丝列表,value值是用户ID,score是关注时间。这样可以对粉丝列表按关注时间进行排序。
存储班级学生成绩,value值是学生ID,score是他的考试成绩。对成绩按分数进行排序

zadd banji stu1 56;
zadd banji stu2 69;
zadd banji stu3 80;
zrem banji 80; //删除value
zrange banji 0 -1; //按score进行排序
zrevrange banji 0 -1; //按score逆序排序
zrangebyscore banji 60 90; //根据分值区间
zcard banji;   //相当于count

容器型数据结构通用规则
list/set/hash/zset

create if not exists
drop if no elements

redis其它常用功能点

分布式锁

分布式锁本质上要实现的目标就是在 Redis 里面占一个“茅坑”,当别的进程也要来占时,发现已经有人蹲在那里了,就只好放弃或者稍后再试。

占坑一般是使用 setnx(set if not exists) 指令,只允许被一个客户端占坑,先来先占, 用完了,再调用 del 指令释放茅坑。

setnx aobing
expire aobing
del aobing

但是引入后会有问题,原子性,怎么解决,就比如setnx成功,设置失效时间expire的时候失败,怎么办?

当时出现了贼多的第三方插件,作者为了解决这个乱象,就在Redis 2.8 版本中作者加入了 set 指令的扩展参数:

set  aobing ture  ex 5 nx
del  aobing

但是这样还是有问题超时问题,可重入问题等等,这个时候,第三方的一些插件就横空出世了,Redission ,Jedis,他们的底层我就不过多描述了,都是通过lua脚本去保证的,大致逻辑跟我们代码实现是差不多的。

就比如去删除的时候,去校验是否当前线程锁定的,就把比较和删除这样一些动作都放到一起了:

# delifequals
if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

延时队列

我们平时习惯于使用 Rabbitmq 、RocketMQ和 Kafka 作为消息队列中间件,来给应用程序之间增加异步消息传递功能。这两个中间件都是专业的消息队列中间件,特性之多超出了大多数人的理解能力。

使用过 Rabbitmq 的同学知道它使用起来有多复杂,发消息之前要创建 Exchange,再创建 Queue,还要将 Queue 和 Exchange 通过某种规则绑定起来,发消息的时候要指定 routing-key,还要控制头部信息。消费者在消费消息之前也要进行上面一系列的繁琐过程。

但是绝大多数情况下,虽然我们的消息队列只有一组消费者,但还是需要经历上面这些繁琐的过程。

有了 Redis,它就可以让我们解脱出来,对于那些只有一组消费者的消息队列,使用 Redis 就可以非常轻松的搞定。

Redis 的消息队列不是专业的消息队列,它没有非常多的高级特性,没有 ack 保证,如果对消息的可靠性有着极致的追求,那么它就不适合使用。

> rpush notify-queue apple banana pear
(integer) 3
> llen notify-queue
(integer) 3
> lpop notify-queue
"apple"
> llen notify-queue
(integer) 2
> lpop notify-queue
"banana"
> llen notify-queue
(integer) 1
> lpop notify-queue
"pear"
> llen notify-queue
(integer) 0
> lpop notify-queue
(nil)

但是这样有问题大家发现没有?队列会空是吧,那怎么解决呢?

客户端是通过队列的 pop 操作来获取消息,然后进行处理,处理完了再接着获取消息,再进行处理。

如此循环往复,这便是作为队列消费者的客户端的生命周期。

可是如果队列空了,客户端就会陷入 pop 的死循环,不停地 pop,没有数据,接着再 pop,又没有数据,这就是浪费生命的空轮询。

空轮询不但拉高了客户端的 CPU,redis 的 QPS 也会被拉高,如果这样空轮询的客户端有几十来个,Redis 的慢查询可能会显著增多。

解决方式很简单,让线程睡一秒 Thread.sleep(1000)

Redis在我开发的一些简易后台我确实有用到,因为我觉得没必要接入消息队列中间件,大家平时开发小系统可以试试。

位图bitmap

在我们平时开发过程中,会有一些 bool 型数据需要存取,比如用户一年的签到记录,签了是 1,没签是 0,要记录 365 天。如果使用普通的 key/value,每个用户要记录 365 个,当用户上亿的时候,需要的存储空间是惊人的。

为了解决这个问题,Redis 提供了位图数据结构,这样每天的签到记录只占据一个位,365 天就是 365 个位,46 个字节 (一个稍长一点的字符串) 就可以完全容纳下,这就大大节约了存储空间。

位图不是特殊的数据结构,它的内容其实就是普通的字符串,也就是 byte 数组。我们可以使用普通的 get/set 直接获取和设置整个位图的内容,也可以使用位图操作 getbit/setbit 等将 byte 数组看成「位数组」来处理。

当我们要统计月活的时候,因为需要去重,需要使用 set 来记录所有活跃用户的 id,这非常浪费内存。

这时就可以考虑使用位图来标记用户的活跃状态。每个用户会都在这个位图的一个确定位置上,0 表示不活跃,1 表示活跃。然后到月底遍历一次位图就可以得到月度活跃用户数。

这个类型不仅仅可以用来让我们改二进制改字符串值,最经典的就是用户连续签到。

key 可以设置为 前缀:用户id:年月    譬如 setbit sign:123:1909 0 1

代表用户ID=123签到,签到的时间是19年9月份,0代表该月第一天,1代表签到了

第二天没有签到,无需处理,系统默认为0

第三天签到  setbit sign:123:1909 2 1

可以查看一下目前的签到情况,显示第一天和第三天签到了,前8天目前共签到了2天

127.0.0.1:6379> setbit sign:123:1909 0 1
0
127.0.0.1:6379> setbit sign:123:1909 2 1
0
127.0.0.1:6379> getbit sign:123:1909 0
1
127.0.0.1:6379> getbit sign:123:1909 1
0
127.0.0.1:6379> getbit sign:123:1909 2
1
127.0.0.1:6379> getbit sign:123:1909 3
0
127.0.0.1:6379> bitcount sign:123:1909 0 0
2

HyperLogLog

如果统计 PV 那非常好办,给每个网页一个独立的 Redis 计数器就可以了,这个计数器的 key 后缀加上当天的日期。这样来一个请求,incrby 一次,最终就可以统计出所有的 PV 数据。

但是 UV 不一样,它要去重,同一个用户一天之内的多次访问请求只能计数一次。

这就要求每一个网页请求都需要带上用户的 ID,无论是登陆用户还是未登陆用户都需要一个唯一 ID 来标识。

你也许已经想到了一个简单的方案,那就是为每一个页面一个独立的 set 集合来存储所有当天访问过此页面的用户 ID。

当一个请求过来时,我们使用 sadd 将用户 ID 塞进去就可以了。

通过 scard 可以取出这个集合的大小,这个数字就是这个页面的 UV 数据。没错,这是一个非常简单的方案。

但是,如果你的页面访问量非常大,比如一个爆款页面几千万的 UV,你需要一个很大的 set 集合来统计,这就非常浪费空间。

如果这样的页面很多,那所需要的存储空间是惊人的。为这样一个去重功能就耗费这样多的存储空间,值得么?其实老板需要的数据又不需要太精确,105w 和 106w 这两个数字对于老板们来说并没有多大区别,So,有没有更好的解决方案呢?

HyperLogLog 提供了两个指令 pfadd 和 pfcount,根据字面意义很好理解,一个是增加计数,一个是获取计数。

pfadd 用法和 set 集合的 sadd 是一样的,来一个用户 ID,就将用户 ID 塞进去就是,pfcount 和 scard 用法是一样的,直接获取计数值。

127.0.0.1:6379> pfadd codehole user1
(integer) 1
127.0.0.1:6379> pfcount codehole
(integer) 1
127.0.0.1:6379> pfadd codehole user2
(integer) 1
127.0.0.1:6379> pfcount codehole
(integer) 2
127.0.0.1:6379> pfadd codehole user3
(integer) 1
127.0.0.1:6379> pfcount codehole
(integer) 3
127.0.0.1:6379> pfadd codehole user4
(integer) 1
127.0.0.1:6379> pfcount codehole
(integer) 4
127.0.0.1:6379> pfadd codehole user5
(integer) 1
127.0.0.1:6379> pfcount codehole
(integer) 5
127.0.0.1:6379> pfadd codehole user6
(integer) 1
127.0.0.1:6379> pfcount codehole
(integer) 6
127.0.0.1:6379> pfadd codehole user7 user8 user9 user10
(integer) 1
127.0.0.1:6379> pfcount codehole
(integer) 10

pfadd 这个 pf 是什么意思?

 

它是 HyperLogLog 这个数据结构的发明人 Philippe Flajolet 的首字母缩写。

他底层有点复杂,他是怎么做到这么小的结构,存储这么多数据的?也是很取巧大家有空可以看下我之前的文章。

布隆过滤器

HyperLogLog 数据结构来进行估数,它非常有价值,可以解决很多精确度不高的统计需求。

但是如果我们想知道某一个值是不是已经在 HyperLogLog 结构里面了,它就无能为力了,它只提供了 pfadd 和 pfcount 方法,没有提供 pfcontains 这种方法。

讲个使用场景,比如我们在使用新闻客户端看新闻时,它会给我们不停地推荐新的内容,它每次推荐时要去重,去掉那些已经看过的内容。问题来了,新闻客户端推荐系统如何实现推送去重的?

你会想到服务器记录了用户看过的所有历史记录,当推荐系统推荐新闻时会从每个用户的历史记录里进行筛选,过滤掉那些已经存在的记录。问题是当用户量很大,每个用户看过的新闻又很多的情况下,这种方式,推荐系统的去重工作在性能上跟的上么?

127.0.0.1:6379> bf.add codehole user1
(integer) 1
127.0.0.1:6379> bf.add codehole user2
(integer) 1
127.0.0.1:6379> bf.add codehole user3
(integer) 1
127.0.0.1:6379> bf.exists codehole user1
(integer) 1
127.0.0.1:6379> bf.exists codehole user2
(integer) 1
127.0.0.1:6379> bf.exists codehole user3
(integer) 1
127.0.0.1:6379> bf.exists codehole user4
(integer) 0
127.0.0.1:6379> bf.madd codehole user4 user5 user6
1) (integer) 1
2) (integer) 1
3) (integer) 1
127.0.0.1:6379> bf.mexists codehole user4 user5 user6 user7
1) (integer) 1
2) (integer) 1
3) (integer) 1
4) (integer) 0

布隆过滤器的initial_size估计的过大,会浪费存储空间,估计的过小,就会影响准确率,用户在使用之前一定要尽可能地精确估计好元素数量,还需要加上一定的冗余空间以避免实际元素可能会意外高出估计值很多。

布隆过滤器的error_rate越小,需要的存储空间就越大,对于不需要过于精确的场合,error_rate设置稍大一点也无伤大雅。比如在新闻去重上而言,误判率高一点只会让小部分文章不能让合适的人看到,文章的整体阅读量不会因为这点误判率就带来巨大的改变。

在爬虫系统中,我们需要对 URL 进行去重,已经爬过的网页就可以不用爬了。但是 URL 太多了,几千万几个亿,如果用一个集合装下这些 URL 地址那是非常浪费空间的。这时候就可以考虑使用布隆过滤器。它可以大幅降低去重存储消耗,只不过也会使得爬虫系统错过少量的页面。

布隆过滤器在 NoSQL 数据库领域使用非常广泛,我们平时用到的 HBase、Cassandra 还有 LevelDB、RocksDB 内部都有布隆过滤器结构,布隆过滤器可以显著降低数据库的 IO 请求数量。当用户来查询某个 row 时,可以先通过内存中的布隆过滤器过滤掉大量不存在的 row 请求,然后再去磁盘进行查询。

邮箱系统的垃圾邮件过滤功能也普遍用到了布隆过滤器,因为用了这个过滤器,所以平时也会遇到某些正常的邮件被放进了垃圾邮件目录中,这个就是误判所致,概率很低。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值