现在多数秒杀,抽奖,抢红包等大并发高流量的功能一般都是基于 redis 实现,然而在选择 redis 的时候,我们也要了解 redis 如何保证服务正确运行的原理

前言

  • redis 如何实现高性能和高并发
  • reids 事务的 ACID 原理
  • WATCH、EXEC 命令实现 redis 事务
  • lua 实现 redis事务
  • 抢红包方案

关注公众号,一起交流,微信搜一搜: 潜行前行

redis 如何实现高性能和高并发

  • redis 是一个内存数据库,读写非常高效。除了开启 AOF,RDB 异步线程去持久化数据,基本没有磁盘I/O消耗,性能方面是比 mysql,oracle 快很多
  • redis 自己实现一套简单高效的基础数据结构:动态字符串(SDS),链表,字典,跳跃链表,整数集合和压缩列表。然后在这个基础上去实现用户能操作的对象:字符串,列表,哈希,集合,有序集合等对象
  • reactor 模式的网络事件处理器。它使用了 I/O 多路复用去同时监控多个套接字,这是一种高效的I/O模型。reactor 相关知识可以看下这篇文章框架篇:见识一下linux高性能网络IO+Reactor模型
  • 事件处理器是单线执行的,这大大减少CPU的上下文切换,和对资源锁的竞争问题,极大提高redis服务处理速度(至于为啥使用单线程,因为CPU够用了,它的性能瓶颈在内存而不是CPU)
  • Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求

reids 事务的 ACID 原理

  • redis 的事务需要先划分出三个阶段

    • 事务开启,使用 MULTI 可以标志着执行该命令的客户端从非事务状态切换至事务状态redis> MULTI
    • 命令入队,MULTI开启事务之后,非 WATCH、EXEC、DISCARD、MULTI 等特殊命令;客户端的命令不会被立即执行,而是放入一个事务队列
    • 执行事务或者丢弃。如果收到 EXEC 的命令,事务队列里的命令将会被执行。如果是 DISCARD 则事务被丢弃
  • 命令入队过程如果出错(如使用了不存在的命令),则事务队列会被拒接执行
  • 执行事务期间出现了异常(如命令和操作的数据类型不匹配),事务队列的里的命令还是继续执行下去,直到全部命令执行完。不会回滚
  • WATCH 可用于监控 redis 变量值,在命令 EXEC 之前;redis 里的数据是有机会被其他客户端的命令修改的。使用 WATCH,监控的变量被修改后,执行 EXEC 时则会返回执行失败的 nil 回复
redis> WATCH "name"
OK
redis> MULTI ### 此时name已被其他客户端的命令修改
OK
redis> SET "name" "lwl"
QUEUED
redis> EXEC
(nil)
  • 从严格意义上来说,redis 是没有事务的。因为事务必须具备四个特点:原子性(Atomicity),一致性(Consistency),隔离性(Isolation),持久性(Durability)。然后 redis 是做不到这四点,只是具备其中一些特征,redis的事务是个伪事务,而且不支持回滚。下面将为各位同学一一道来

原子性

从上面可以,事务的异常会发生在EXEC命令执行前、后

  • EXEC命令执行前:在命令入队时就报错,(如内存不足,命令名称错误),redis 就会报错并且记录下这个错误。此时,客户还能继续提交命令操作;等到执行EXEC时,redis 就会拒绝执行所有提交的命令操作,返回事务失败的结果 nil
  • EXEC命令执行后:命令和操作的数据类型不匹配,但 redis 实例没有检查出错误。在执行完 EXEC 命令以后,redis 实际执行这些指令,就会报错。此时事务是不会回滚的,但事务队列的命令还是继续被执行。事务的原子性无法保证
  • EXEC执行时,发生故障:如果 redis 开启了 AOF 日志,那么,只会有部分的事务操作被记录到 AOF 日志中。需要使用 redis-check-aof 工具检查 AOF 日志文件,这个工具可以把未完成的事务操作从 AOF 文件中去除。事务的原子性得到保证

一致性

  • EXEC命令执行前:入队报错事务会被放弃执行,具有一致性
  • EXEC命令执行后:实际执行时报错,错误的执行不会执行,正确的指令可以正常执行,一致性可以保证
  • EXEC执行时,发生故障:RDB 模式,RDB 快照不会在事务执行时执行,事务结果不会保存在RDB;AOF 模式,可以使用 redis-check-aof 工具检查 AOF 日志文件,把未完成的事务操作从 AOF 文件中去除。可以保证一致性

隔离性

  • EXEC 命令前执行,隔离性需要通过 WATCH 机制保证。因为 EXEC 命令执行前,其他客户端命令可以被执行,相关变量会被修改;但可以使用  WATCH 机制监控相关变量。一旦相关变量被修改,则 EXEC 后则事务失败返回;具有隔离性
  • EXEC 命令之后,隔离性可以保证。因为 redis 是单线程执行,事务队列里的命令和其他客户端的命令只能二选一被顺序执行,因此具有隔离性

持久性

  • 如果 redis 没有使用 RDB 或 AOF,事务的持久化是不存在的
  • 使用 RDB 模式,那么在一个事务执行后,而下一次的 RDB 快照还未执行前,如果发生了实例宕机,数据丢失,这种情况下,事务修改的数据也是不能保证持久化
  • AOF 模式,因为 AOF 模式的三种配置选项 no、everysec 和 always 都会存在数据丢失的情况。所以,事务的持久性属性也还是得不到保证

总结

  • redis 的事务机制可以保证一致性和隔离性;但是无法保证持久性;具备了一定的原子性,但不支持回滚

WATCH、EXEC 命令实现 redis 事务

redis> WATCH "map"
OK
redis> MULTI
OK
redis> HSET map "csc" "lwl"
QUEUED
redis> HGET map "csc"
QUEUED
redis> EXEC
1) OK
2) "lwl"

lua 实现 redis 事务

除了 MULTI、WATCH、EXEC 命令,还有其他的方式可做到 redis 原子性和隔离性吗?有的,lua 脚本;redis 内置了lua的执行环境,并自带了一些 lua 函数库。redis 执行 lua 时,会启动一个伪客户端去执行脚本里的 redis 命令

  • 一致性,原子性,持久性 和 MULTI,EXEC 过程相似:如果 lua 存在错误的命令名称,事务会执行失败。如果在执行 redis 命令过程出现异常,之前正常执行的命令也不会回滚
  • lua 脚本被当做一命令集合一起被执行,且 redis 是单线处理机制,因此不需要 WATCH 保证隔离性,天然具备隔离性
  • Lua调用Redis指令: redis.call("命令名称",参数1,参数2)

优点

  • 减少网络开销:可以将多个请求通过脚本的形式一次发送,减少网络时延
  • 原子操作:Redis会将整个脚本作为一个整体执行,中间不会被其他请求插入。在脚本运行过程中无需担心会出现竞态条件
  • 可重复使用:客户端发送的脚本会永久存在 redis 中,这样其他客户端可以复用这一脚本,而不需要使用代码完成相同的逻辑

抢红包方案

  • 问题关键点

    • 一:用户是否参与过活动,不可重复参与
    • 二:红包数量有限;而且一个可抢的红包,保证不能让多个人同时抢到
    • 三:持久化存储红包与用户的关系
    • 四:如何保证 步骤一到步骤三的原子性和隔离性

关键点一

  • redis 的集合对象 set 是无序且唯一的。set 集合由整数集合或字典实现的,添加,删除,查找的复杂度基本视为 O(1),存放的最大对象个数是2^32 - 1 (4294967295)
  • 使用 set 集合保存参加过的用户,每次用户参与活动时先判断是否在 set 里。不在则可以抢红包
  • 如果是用户可以重复参与多次的场景,则使用哈希对象,key存用户对象,value 存放参与次数。使用 INCR 原子操作增加 value,如果返回数值 > 上限,说明抢的次数用完

关键点二

使用 list 或者 set 存放事先创建好的有限个红包; 因为 redis 是单线程操作,同一时间,多人抢红包,只会有一个人成功。而红包是事先生成的,消费用完即止,不存在超发的可能

  • 使用 list 列表存放红包

    • 因为红包金额大小不一,为增加抢到红包大小的随机性,需要先shuffle一次,再 LPUSH 入队列
    • RPOP 出队列一个红包,如果返回不为nil,则代表获取成功,继续下一步,反之则说明已抢完,返回
  • set 集合中有两个指令非常适合在抢红包、抽奖的场景使用
    • SPOP key [count] 移除并返回集合中的一个随机元素
    • SRANDMEMBER key [count] 返回集合中一个或多个随机数;需要再调 SREM 移除一遍
    • 将所有的红包通过 SADD 添加到 set 中,然后通过随机命令获取对应的红包即可
  • 如果有谢谢惠顾之类的落空选项,生成对应的无效红包、奖品放入 set 或 list 即可
  • 抢红包一般是有时效性,正好可以配合 redis 的 key 的失效时间使用。使得抢红包功能很完美的解决

关键点三

  • 使用额为的 list 列表保存用户与红包的关系,用户抢到红包后,将对应的关系 LPUSH 入队列,然后服务去消费拉取数据批量保存到数据库即可

关键点四

使用 lua 脚本实现即可

-- 参数:KEYS[1]-红包list,KEYS[2]-用户和红包的消费list,KEYS[3]-去重的哈希对象,KEYS[4]-用户ID
-- 函数:尝试获得红包,如果成功,则返回json字符串,如果不成功,则返回nil
-- 返回值:nil 或者 json字符串,{"userId":"用户ID","id":"红包ID"}
-- 如果用户已抢过红包,则返回nil -- 步骤一,拦截重复参与
if redis.call('hexists', KEYS[3], KEYS[4]) == 1 then
return nil
else
-- 步骤二,先取出一个红包
local lunkMoney = redis.call('rpop', KEYS[1]);
if luckMoney then
local data = cjson.decode(luckMoney);
data['userId'] = KEYS[4]; -- 加入用户ID信息
local re = cjson.encode(data);
-- 把用户ID放到去重的哈希,value设置为 1
redis.call('hset', KEYS[3], KEYS[4], 1);
-- 步骤三: 用户和红包放到已消费队列里
redis.call('lpush', KEYS[2], re);
return re;
end
end
return nil

欢迎指正文中错误

参考文章

Redis篇:事务和lua脚本的使用的更多相关文章

  1. Redis | 第9章 Lua 脚本与排序《Redis设计与实现》

    目录 前言 1. Lua 脚本 1.1 Redis 创建并修改 Lua 环境的步骤 1.2 Lua 环境协作组件 1.3 EVAL 命令的实现 1.4 EVALSHA 命令的实现 1.5 脚本管理命令 ...

  2. Redis进阶之使用Lua脚本自定义Redis命令

    [本文版权归微信公众号"代码艺术"(ID:onblog)所有,若是转载请务必保留本段原创声明,违者必究.若是文章有不足之处,欢迎关注微信公众号私信与我进行交流!] 1.在Redis ...

  3. Redis进阶之使用Lua脚本开发

    1.在Redis中使用Lua 在Redis中执行Lua脚本有两种方法:eval和evalsha. (1)eval eval 脚本内容 key个数 key列表 参数列表 下面例子使用了key列表和参数列 ...

  4. Redis分布式锁—SETNX+Lua脚本实现篇

    前言 平时的工作中,由于生产环境中的项目是需要部署在多台服务器中的,所以经常会面临解决分布式场景下数据一致性的问题,那么就需要引入分布式锁来解决这一问题. 针对分布式锁的实现,目前比较常用的就如下几种 ...

  5. redis集群搭建+lua脚本的使用

    详细参考这篇文章(windows) https://blog.csdn.net/qiuyufeng/article/details/70474001 一.使用JAVA代码操作redis集群 publi ...

  6. Redis(六)Lua脚本的支持

    Redis为什么需要Lua脚本的支持 当应用需要Redis完成一些Redis命令不支持的特性时,要么扩展Redis client或者更甚至编写c扩展Redis server.这都大大造成了应用的实现的 ...

  7. redis集群+JedisCluster+lua脚本实现分布式锁(转)

    https://blog.csdn.net/qq_20597727/article/details/85235602 在这片文章中,使用Jedis clien进行lua脚本的相关操作,同时也使用一部分 ...

  8. redis中的事务、lua脚本和管道的使用场景

    参考文章 : https://blog.csdn.net/fangjian1204/article/details/50585080

  9. Redis进阶实践之十九 Redis如何使用lua脚本

    一.引言               redis学了一段时间了,基本的东西都没问题了.从今天开始讲写一些redis和lua脚本的相关的东西,lua这个脚本是一个好东西,可以运行在任何平台上,也可以嵌入 ...

随机推荐

  1. 多图详解万星 Restful 框架原理与实现

    rest框架概览 我们先通过 go-zero 自带的命令行工具 goctl 来生成一个 api service,其 main 函数如下: func main() { flag.Parse() var ...

  2. MyCat的快速搭建

    1. 概述 老话说的好:一个好汉三个帮,一个人再聪明.再有本事,也要借助他人的力量,才能成功. 言归正传,今天我们来聊聊 MyCat的快速搭建. 2. 场景介绍 服务器A IP:192.168.1.2 ...

  3. SpringBoot之网站的登陆注册逻辑

    网站的登录注册实现逻辑 该文章主要是为了整理之前学习项目中的知识点,并进行一定程度的理解. 技术列表: SpringBoot MySQL redis JWT 用户登录逻辑: 首先打开前端登录页面,F1 ...

  4. CSharp委托与匿名函数

    CSharp委托与匿名函数 场景 面对事件处理,我们通常会通过定义某一个通用接口,在该接口中定义方法,然后在框架代码中,调用实现该接口的类实例的方法来实现函数的回调.可能这样来说有些抽象,那我们提供一 ...

  5. mysql中一半会选择什么样的字段为索引?(含索引创建删除查看公式)

    一.数据量庞大的数据做索引 二.该字段经常出现在where的后面,以条件形式存在,经常被用户搜索的字段 三.很少被增删改的字段,因为增删改后,索引会重新排序 索引的创建 create index 索引 ...

  6. 2021-06-27 & 2021-06-28 集训题解

    西克 题目传送门 Description Solution 跟 2021年省选A卷D2T1 一模一样,懒得讲了 不过这个题似乎有点卡空间,所以卡不过去 Code #include <bits/s ...

  7. 洛谷3809 SA模板 后缀数组学习笔记(复习)

    其实SA这个东西很久之前就听过qwq 但是基本已经忘的差不多了 嘤嘤嘤 QWQ感觉自己不是很理解啊 所以写不出来那种博客 QWQ只能安利一些别人的博客了 小老板 真的是讲的非常好 不要在意名字 orz ...

  8. JavaScript常用的Hook脚本

    JavaScript常用的Hook脚本 本文Hook脚本 来自 包子 页面最早加载代码Hook时机 在source里 用dom事件断点的script断点 然后刷新网页,就会断在第一个js标签,这时候就 ...

  9. Java正则中"\\\\"表示普通反斜杠

    Java中"\"用于转义字符,"\\"表示普通无转义功能的反斜杠. 如果将字符串当做正则表达式来解析,那么"\\"也有了特殊意义,它与其后的 ...

  10. 第三次Alpha Scrum Meeting

    本次会议为Alpha阶段第三次Scrum Meeting会议 会议概要 会议时间:2021年4月26日 会议地点:线上会议 会议时长:20min 会议内容简介:本次会议主要由每个人展示自己目前完成的工 ...