redis 抽奖 结构 redis做抽奖_redis抽奖并发

windsearcher同学

一蓑烟雨任平生。料峭春风吹酒醒,微冷,山头斜照却相迎。回首向来萧瑟处,归去,也无风雨也无晴。

1.redis是什么

reids是一个开源的缓存框架,基于K-V对的内存缓存框架,具有丰富的数据结构,支持分布式,可持久化。

2.为什么要用redis

也就是说redis 的适用场景,在什么情况下采用缓存呢?

1.存储的数据类型不适合用关系型数据库,以关注的人为例,对应的是一个用户ID列表,适用关系型数据库只能将列表拆成多行,然后再查询出来组装

2.单机情况下的MySQL已经优化到极致了,依旧无法支持日渐增加的并发量,从软件到硬件层次去优化MySQL。软件层面最常见的加索引、硬件层面的换成固态硬盘,读写速度更快。

此时才开始考虑是否引入缓存,那为什么选择redis?

  • 简单稳定,这点笔者觉得很重要,如果三天两头出问题,我想也没有哪个公司会用它
  • 速度快,官方称10W QPS
  • 支持分布式,redis3.x推出了redis cluster
  • 持久化,也就是数据会被落地到磁盘中
  • 丰富的数据结构,字符串、哈希、列表、集合、有序集合、bitmaps、geo、hyperloglog

3.redis基础

字符串

  • set key value [ex seconds] [px milliseconds] [nx|xx]设置值,可以用set key value ex seconds nx来实现分布式锁,nx表示只能为不存在的key设置值
  • get key 获取某个key的值
  • mset key1 value1 key2 value2 批量设置并且是原子的,可以用来减少网络时间消耗
  • mget key1 key2 批量获取并且是原子的,可以用来减少网络时间消耗

哈希

其实我们可以理解 hash 为 小型Redis ,Redis 在底层实现上和 Java 中的 HashMap 差不多,都是使用 数组 + 链表 的二维结构实现的。

下面我们来看一下关于 hash 的基本操作。

  • hset key field value 设置字典中某个key的值
  • hmset key field1 value1 field2 value2 ... 批量设置
  • hget key field 获取字典中某个key的值
  • hmget key field1 field2 批量获取
  • hgetall key 获取全部

公司有个项目其实用到了很多哈希结构,像帖子详细信息是保存在hash结构中,为啥这样设计呢?我觉得处于两个考虑,一、hash可以大大节省key的数量  二、可以便于后续业务的拓展

可以设计称这样:hset   blog.article.info  帖子ID  帖子详细,只需要一个key就可以缓存所有文章。

方便业务拓展,此话咋说?比如你要上线一个热榜业务、最新帖子业务等,这些业务的共性都是基于帖子来开发的,所以我们可以把帖子详情信息作为公共缓存存储在hash中,然后像热榜业务、最新帖子业务就缓存帖子ID,就不需要每一个业务都缓存一遍帖子详情信息,大大减少内存消耗和方便业务扩展。

列表

Redis 中的列表相当于 Java 中的 LinkedList(双向链表) ,也就是底层是通过 链表 来实现的,所以对于 list 来说 插入删除操作很快,但 索引定位非常慢。

下面我们来看一下关于 list 的基本操作。

  • lpush key item1 item2 item3... 从左入栈
  • rpush key item1 item2 item3... 从右入栈
  • lpop key 从左出栈
  • rpop key 从右出栈
  • lindex key index 获取指定索引的元素 O(n)谨慎使用
  • lrange key start end 获取指定范围的元素 O(n)谨慎使用
  • ltrim key start end 保留指定范围的元素,可以做定长列表

总结来说我们可以使用 左入右出或者右入左出 来实现队列,左入左出或者右入右出 来实现栈。

  • lpush + lpop = Stack
  • rpush + rpop = Stack
  • lpush + rpop = Queue
  • rpush + lpop = Queue
  • lpush/rpush + ltrim = Capped List (定长列表)
  • lpush + brpop = Message Queue (消息队列)
  • rpush + blpop = Message Queue (消息队列)

这里提一下ltrim,公司有个抽奖业务,用户抽中奖后并订阅需要返回给前端,用于弹幕展示,但产品要求返回一定数量的弹幕,这里就用到了这条命令。

集合

Redis 中的 set 相当于 Java 中的 HashSet(无序集合),其中里面的元素不可以重复,我们可以利用它实现一些去重的功能。我们还有对几个集合进行取交集,取并集等操作,这些操作就可以获取不同用户之间的共同好友,共同爱好等等。

下面我们就来看一下关于 set 的一些基本操作。

  • sadd key value [value ...] 添加元素
  • srem key value [value ...] 删除某个元素
  • sismember key value 判断是否是集合中的元素
  • spop key count 从集合中随机弹出元素(会破坏结合结构)
  • smembers key 获取集合所有元素 O(n)复杂度
  • scard key 获取集合个数
  • sinter set1 set2 ... 获取所有集合中的交集
  • sdiff set1 set2 ... 获取所有集合中的差集
  • sunion set1 set2 ... 获取所有集合中的并集

还是抽奖业务,要求每个用户只在活动期间只能抽取一次,在设计的时候我使用了set结构来判断用户是否抽过奖。主要分为是否抽奖、抽奖记录加入缓存、返回已抽奖三部分伪代码。

是否抽奖:sismember activity.newyears userid

抽奖记录加入缓存:sadd activity.newyears userid

有序集合

Redis 中的 zset 是一个 有序集合,通过它可以实现很多有意思的功能,比如学生成绩排行榜,视频播放量排行榜等等。

zset 中是使用 跳表 来实现的,我们知道只有数组这种连续的空间才能使用二分查找进行快速的定位,而链表是不可以的。跳表帮助链表查找的时候节省了很多时间(使用跳的方式来遍历索引来进行有序插入),如果不了解跳表的同学可以补习一下。

下面我们来看一下关于 zset 的一些基本操作。

  • zadd key score element  [score element]添加,score用于排序,value需要唯一,由于使用的跳表,时间复杂度为 O(logn)。
  • zrem key element  [element]删除某元素 O(1)时间复杂度
  • zscore key element 获取某个元素的分数
  • zincrby key incrScore element 增加某个元素的分数
  • zrange key start end [withscores] 获取指定索引范围的元素 加上withscores则返回分数 O(logn + m)时间复杂度
  • zrangebyscore key minScore maxScore [withscores] 获取指定分数范围的元素 加上withscores则返回分数,O(logn + m)时间复杂度
  • zcard key 获取有序集合长度

只需要通过zrange key start end [withscores]就可以轻松获取你需要的排行榜,比如我要展示热搜前十的,start和end就是[0,9]。当一个成员插入后,zset就会对它排序,根据score,这里score大家可以根据实际业务来设计。

bitmaps

现代计算机以二进制作为信息的基础单位,1个字节8位,可以把bitmaps想成一个以位为单位的数组,数组的单位只能存储0或1.

下面我们来看一下关于 bitmaps 的一些基本操作。

  • setbit  key  offset  value,设置键的第offset位的值(从第0位开始)
  • getbit  key  ofset 
  • bitcount  key  [start]  [end],这里start,end代表字节,需要换算成位,获取[start,end]内值为1的个数

思考一个场景题,如何获取一段时间内用户的登录次数,以天为单位?当用户登录后,肯定能拿到用户名和登录时间,比如"2020-11-04",先把它转换成整数,调用redis.setbit(username,day),如果给定了需要计算登录时间段,先转成整数,然后/8,使用bitcount  username  start  end即可得到。

4.Pipeline和事务

管道 Pipeline

Redis提供了批量操作命令(例如mget、mset),但大部分命令不支持批量操作。在某些场景下我们在一次操作中可能需要执行多个命令,而如果我们只是一个命令一个命令去执行则会浪费很多网络消耗时间,如果将命令一次性传输到 Redis 中去再执行,则会减少很多开销时间。但是需要注意的是 pipeline 中的命令并不是原子性执行的,也就是说管道中的命令到达 Redis 服务器的时候可能会被其他的命令穿插。

比如set username jack和set username ma,两条命令在管道中执行,可能在两条命令之中会有其他客户端发送过来的命令执行,也就是说无法保证原子性,要么都做,要么都不做。

事务

关系型数据库具有 ACID 特性,Redis 能保证A(原子性)和I(隔离性),D(持久性)看是否有配置 RDB或者 AOF 持久化操作,但无法保证一致性,因为 Redis 事务不支持回滚

但实际上大部分业务也不需要严格遵循ACID原则。以微博关注操作为例,即使系统没有将A加入B的粉丝列表,其实业务影响也很小,因此我们设计方案时,需要根据业务特性和要求来确定是否可用Redis,而不能因为不遵循ACID就直接放弃。

我们可以简单理解为 Redis 中的事务只是比 Pipeline 多了个原子性操作,也就是不会被其他命令给分割。

  • multi 事务开始的标志
  • exec 事务执行

redis 抽奖 结构 redis做抽奖_redis抽奖并发_02

可以看到sadd命令此时返回的结果是QUEUED,代表命令并没有真正执行,而是暂存在Redis,其他客户端是查询此结果的。只有当exec执行后,A关注B的行为才算完成。

  • discard 清除在这个事务中放入队列的所有命令,即解除整个事务。
  • watch key 在事务开始前监控某个元素,如果在提交事务的时候发现这个元素的值被其他客户端更改了则事务会运行失败。(有些应用场景需要在事务之前,确保事务中的key没有被其他客户端修改过,才执行事务,否则不执行,类似乐观锁)
  • unwatch key 解除监控

如果事务中的命令出现错误,Redis的处理机制也不尽相同。

  • 命令错误

在事务操作中,如果是语法错误,会造成整个事务无法执行。

  • 运行错误

比如在执行事务操作中突然服务器宕机,此时命令不会回滚,也就是执行到哪结果就是哪。

5.常见面试题

01

keys和scan的区别

keys会阻塞多路复用的io主线程,如果这个线程阻塞,在此执行期间其他发送给redis服务器的命令都会被阻塞;scan不会阻塞主线程,并支持游标按批次迭代返回数据,每次返回的数据,都会返回下一次游标应该传的值,我们根据这个值再去进行下一次访问,当然scan返回的数据有可能重复,需要在业务层去重。

02

redis分布式锁

先拿setnx抢夺锁,抢到后,再用expire给锁加一个过期时间防止忘记释放。
如果setnx之后执行exipre前进程意外crash或者重启维护,可以把setnx和exipre合并。

加锁:*setnx。key按照业务来命名,value可以是当前线程ID。为啥value是线程ID呢?加入线程A拿到锁,但由于种种原因,A直到锁过期也没有执行完毕,此时线程B拿到锁,然而线程A正好del锁了,把B拿到的锁给删除了,所以需要在删除前判断是否是其拿到的锁,才能去删除。

解锁:当得到锁的线程执行完任务,需要释放锁。最简单就是del。

锁超时:如果一个得到锁的线程执行任务中崩了,来不及显式释放锁,就需要用setnx key + expire。