现在多数秒杀,抽奖,抢红包等大并发高流量的功能一般都是基于 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. 利用Conda尝鲜Python 3.10

    1 简介 就在几天前,Python3.10的第一个正式版本3.10.0发布,之前我们只是从其各个测试版本中捕风捉影地知晓了一些可能加入的新特性,而在正式版本中,我们得以一睹其正式加入的诸多新特性. 本 ...

  2. Electron+Vue+ElementUI开发环境搭建

    Node环境搭建 本文假定你完成了nodejs的环境基础搭建: 镜像配置(暂时只配置node包镜像源,部分包的二进制镜像源后续讨论).全局以及缓存路径配置,全局路径加入到了环境变量 $ node -v ...

  3. ubuntu20.04安装网易云音乐

    Ubuntu20.04安装网易云 进入网易云音乐下载地址 下载对应客户端 进入终端,安装 sudo dpkg -i 软件名.deb

  4. Python笔记_1语法总结

    前言导读 本章知识点是我在最初期听python视频教程的时候整理总结的笔记 对python语法的认识对以后代码的解读有着很大的帮助. 1 新建python命名规则 新建项目名 :数字编号 项目名称 新 ...

  5. SpringBoot入门08-整合Mabatis

    整合所需的依赖 注解方式和映射文件方式的mybatis都可以被整合进springboot 创建springboot的web项目后,在pom加入spring-mybatis和mysql-jdbc和thy ...

  6. 简单的 Go 入门教程

    Go(又称 Golang )是 Google 开发的一种静态强类型.编译型.并发型,并具有垃圾回收功能的编程语言 Docker 和 Kubernetes 都是使用 Go 进行开发的,这几年 Go 越来 ...

  7. flask操作(增删改查操作)

    增加数据 from .models import Goods from app.extensions import db goods1 = Goods(name='魅族18s', price=3400 ...

  8. Kubernetes client-go 源码分析 - Reflector

    概述入口 - Reflector.Run()核心 - Reflector.ListAndWatch()Reflector.watchHandler()NewReflector()小结 概述 源码版本: ...

  9. 初识HTML01

    什么是页面? 页面是基于浏览器的应用程序 页面是数据展示的载体,由浏览器和服务器共同执行产物. 浏览器的功能 向服务器发送用户请求指令 接收并解析数据展示给用户 服务器的功能 存储页面资源 处理并响应 ...

  10. vue3.x新特性之setup函数,看完就会用了

    最近有小伙伴跟我聊起setup函数,因为习惯了vue2.x的写法导致了,setup用起来觉得奇奇怪怪的,在一些api混编的情况下,代码变得更加混乱了,个人觉得在工程化思想比较强的团队中使用setup确 ...