【项目】MyRedis+秒杀系统

目录

一、MyRedis开发

二、秒杀系统

一、MyRedis开发

1.Redis的数据序列化协议是RESP,即client和server之间的通信协议,5种数据格式如下:

  • 正常回复:以”+”开头,以”\r\n”结尾,例如+OK\r\n
  • 错误回复:以”-“开头,以”\r\n”结尾,例如-Error message\r\n
  • 整数:以”:”开头,以”\r\n”结尾,例如:123456\r\n
  • 多行字符串:以”$”开头后跟字符数量,数据由”\r\n”隔断,例如:$6\r\n123456\r\n
  • 数组:以”*“开头后跟成员数量,每个成员由”\r\n”隔断,例如:*2\r\n$4\r\nstr1\r\n$4\r\nstr2\r\n

2.myRedis的解析异步化

  • 对外公布的Parser()方法会返回一个<-ch *PayLoad,因为方法里面用协程去调用了执行解析的实际方法,当解析完成时,会吐到这个channel里面
    1
    2
    3
    4
    5
    func ParseStream(reader io.Reader) <-chan *Payload{
    ch := make(chan *Payload, 0)
    go parse0(reader, ch)
    return ch
    }

3.k-v和dict的实现

  • k-v的val用interface{},是因为可以适应各种数据类型的值
  • dict里面的方法:get、put、len等等的实现,其实就是底层对sync.map的业务调用,而因为这样封装了一层,以后改用不是sync.map的数据结构实现也方便

4.数据命令解析过程

  • 主线程监听端口,用handle去处理
  • parse0里面根据传的命令第一个字符是’*’还是’$’分别调不用的函数进行处理,当前读取的状态用state结构体记录
  • 解析得到args数组后吐到channel里
  • handle接受了这个channel里面的数据args数组后,去调数据库内核的Exce方法
    • echo_database这个数据库内核的Exce()实现的是,传什么进来就返回什么回去,所以会把解析好的数据再重新包装

5.核心链路

  • tcp包里监听接口的数据,然后拿去给resp协议层handle()处理
  • handle()去调parseStream解析命令
  • parseStream将翻译后的命令转给内核database去处理
    • 这里是异步执行,通过go parse0异步往channel里写数据,然后主程序去获取这个channel的产出往下执行
  • 每个内核database里面有一个*DB列表
  • 每个db里面有一个它的序号和一个dict
  • 每个dict底层用sync.map实现了Get()、PUT()等一系列方法

6.aof落盘逻辑

  • database内核在初始化时,也会初始化aof,先根据aof文件恢复之前状态
    • 恢复依然是走的parseStram()方法
  • 初始化有buffer的管道,然后起一个协程去异步从管道里拿操作命令写aof文件(落盘)
    • 每次出现set、rename这种写命令时,就调一次addAof()把命令塞入管道

7.碰到的问题

  • aof落盘的时候,明明应该是db0,但是落盘成db15
    • 原因:
      • 初始化database内核时,用匿名函数赋予里面的成员db的addAof()方法出现闭包问题
      • 因为闭包去调addAof()的方法会使用到局部变量db.index,所以这个局部变量db会逃逸到堆上,然后每次for遍历的时候都会去更新这个堆上的db值
      • 最后实际堆上是遍历的最后一个db成员
    • 解决办法:
      • for循环里新定义一个临时变量来放db
    • 原理:
      • for循环头部的变量每次循环中在内存上的地址其实是一样的,只是每次的值不一样(所以有时候这个地址逃逸到堆上就会产生闭包问题)
      • 而for循环内部定义的变量每次循环都会是一个新的内存地址,单次循环结束后就销毁

8.集群化

  • 采用一致性哈希,好处是:增加一个新节点只用迁移部分数据
  • 每个节点运行有一个database内核和一个连接池
      - 连接池是自己作为客户端与其他节点的连接(客户端用的是github上的开源库)
    
  • 对于数据不存在自己本地上的命令,会把本机当做一个redis客户端去访问别的节点

二、秒杀系统

1.系统架构设计

  • 需求
    • 前端页面承载大流量
    • 在并发量大的情况下解决超卖问题
    • 后端接口要满足可横向扩展
  • 解决方案
    • CDN->阿里提供的流量负载器->流量拦截系统->Go服务器集群->消息队列->DB数据库

2.RabbitMQ

  • 定义:面向消息的中间件,用于组件之间的解耦,主要体现在消息的发送者和消费者之间无强依赖关系
  • 使用场景:流量削峰,异步处理,应用解耦等
  • RabbitMQ安装命令
    • 安装erlang,因为RabbitMQ是用erlang编写的
      1
      2
      3
      sudo apt install erlang

      wget http://www.rabbitmq.com/releases/erlang/erlang-19.0.4-1.el7.centos.x86_64.rpm
    • 检测erlang是否安装成功
      1
      erl
    • 安装RabbitMQ
      1
      sudo apt install rabbitmq-server
    • 检测RabbitMQ是否安装成功
      1
      rabbitmqctl status
    • 启动RabbitMQ
      1
      systemctl start rabbitmq-server
    • 启动RabbitMQ的插件,要用web控制台必须启动这个
      1
      rabbitmq-plugins enable rabbitmq_management
    • 访问web控制页面
      1
      2
      本地部署直接访问127.0.0.1:15672(默认用账户guest密码guest访问)
      远程部署先开放防火墙15672端口,然后再用公网ip:15672访问(需要新建一个用户)
    • 远程部署需要创建admin用户并赋权限
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      # 创建一个账户为admin,密码为admin的新用户
      rabbitmqctl add_user admin admin

      # 给这个admin用户赋管理员权限
      rabbitmqctl set_user_tags admin administrator

      # 给这个admin用户赋virtual host的所以配置、读、写权限
      # virtual host权限是管理虚拟机的权限
      rabbitmqctl set_permissions -p "/" admin ".*" ".*" ".*"

      # 查看当前存在的用户和他们对应的权限
      rabbitmqctl list_users
  • RabbitMQ使用命令
    • 启动RabbitMQ
      1
      systemctl start rabbitmq-server
    • 停止RabbitMQ
      1
      rabbitmqctl stop
    • 插件管理命令
      1
      2
      3
      4
      5
      6
      7
      8
      # 表示查看插件列表
      rabbitmq-plugins list

      # 表示启用rabbitmq_management
      rabbitmq-plugins enable rabbitmq_management

      # 表示卸载rabbitmq_management
      rabbitmq-plugins disable rabbitmq_management
  • RabbitMQ核心概念
    • virtual host虚拟机
      • 每个账户需要绑定一个virtual host才能使用
      • 每个virtual host起的是一个数据隔离的作用
    • connection连接
      • 连接的进程
    • exchange交换机
      • 消息要经过交换机
      • 把消息经过对应的规则处理绑定到特定的key上
    • channel通道
      • 一个connection里面有多个channel
    • queue队列
      • 临时存储信息
    • binding绑定
      • 把队列绑定到交换机上
  • RabbitMQ工作模式(重点)
    • Simple简单模式:生产者把消息放到队列里,消费者从队列里取出来消费
      • 一个生产者,一个队列,一个消费者
      • 只用queue来做区分,exchange和key都为空
    • Work工作模式:一个消息只能被一个消费者获取,即一个消息只能被消费一次
      • 一个生产者,一个队列,多个消费者
      • 适用于生产效率高于消费效率的情境,来提高系统运行速度
      • 用于起到负载均衡的作用
    • Publish/Subscribe订阅模式:一个消息可以被多个消费者获取
      • 一个生产者,一个交换机,多个队列,多个消费者
    • Routing路由模式:一个消息可以被多个消费者获取,并且可以通过key来指定哪个队列接受消息
      • 一个生产者,一个交换机,多个队列,多个消费者

go mod tidy -go=1.16 && go mod tidy -go=1.17

3.不同项目对比

  • 趣约课、校友平台
    • 架构:用户->Web服务器->Mysql
    • 缺点:
      • Web服务器既要验证安全性,又要处理请求,负载大
      • 用户每发起一次请求就要去查一次数据库,数据库有压力
      • 高并发的情况下无法保证数据一致性
  • 秒杀系统
    • 架构:用户->SLB负载均衡器->分布式安全验证->秒杀数量控制->Web服务器->RabbitMQ->Mysql

4.CDN流程

  • 用户产生访问url的请求
  • 请求打到DNS服务器
  • CDN网络的智能DNS解析系统会把这个请求解析得到离用户最近的CDN节点并返回用户
  • 用户再对这个CDN节点直接请求
  • CDN节点
    • 如果本身有静态文件缓存的话会直接返回给用户(这一步就大大减少了对Web的请求流量)
    • 如果没有的话会对我们的Web站点发起一次请求获得静态文件,本次及下次就可以直接用了
  • 前四步都由CDN的厂商做,我们做项目只关注Web站点这一步

5.cookie和token

  • cookie存放在客户端,所以是不安全的,人为可以清除。
  • cookie有过期时间设定。如果不设置过期时间,说明这个cookie就是当前浏览器的会话时间,浏览器关了,cookie就不存在了。如果有过期时间,cookie就会存储到硬盘上,浏览器关闭不影响cookie。下次打开浏览器,cookie还存在
  • cookie有大小的限制,4KB。
  • 最后,对于一个分布式的web系统,通用的解决方案就是cookie+token。由服务端生成token,将用户信息与token进行关联,token返回给浏览器,存储到cookie中。后续请求都携带cooke或者将token从cookie取出以参数传递(其实把token放在header里更好,还易于解决跨域问题)

6.一致性Hash算法

  • 作用:用于分布式结构中,快速定位资源位置
  • 实现过程:
    • 把存储空间看成一个Hash圆环,出发地址0,终点地址2^32-1,超过终点地址就又从0开始
    • 把服务器的ip地址或者服务器的名称作为关键字进行Hash,然后均匀地分布在Hash圆环上
      • 哈希算法是把string转换成byte[]数组,长度小于64的用0补满到64,然后用IEEE多项式返回crc-32校验和,也就是哈希值
    • 把数据也按hash算法放到圆环上,然后以这个数据的位置为起点,顺时针去找最近的一个服务器,这个服务器即是这个数据应该放的物理地址
    • 当请求的数据不存在本机时,以本机作为代理去向其它主机发起http请求
  • 解决分布不均匀的办法
    • 虚拟节点:把每个服务器的值略作修改,生成多个虚拟节点,如果数据最终要放到这个虚拟节点上,那么直接把数据放到对应的服务器上,这样即使服务器数量过少或者服务器和数据分布不均也可以尽量均匀分布
  • 不考虑hash冲突的原因
    • 数据hash冲突,无影响。因为数据只用去找最近的节点,就算算出来的哈希位置是一样的,说明这两个数据应该放到同一个服务器上,且本项目中的数据是userId,唯一标识基本不考虑冲突情况
    • 节点hash冲突,节点是ip+虚拟节点编号,这个重复的概率极低,发生重复那么说明的是网络出现了问题

7.为什么不用redis

  • 如果用Redis的话要用redis cluster来提高性能,商品信息分散在各个cluster上
  • 但是这样在对单件商品并发请求流量大的话,流量还是会集中到某一个redis cluster,这样的话其实性能还是会受限
  • 而采用go语言写的数量控制接口可以解决以上问题

8.引入消息队列的作用

  • 异步
  • 解耦
  • 削峰

9.为什么用这么多组件来控制流量

  • 保障系统稳定的情况下,不浪费系统资源,即最大化系统利用
  • 因为如果不控制的话,在某个点流量全堆到接口上,导致暴库、接口挂掉等线上故障,而如果设置一个休眠时间的话,又很难利用好系统资源,所以就有以上的流量控制,接口做完自己手上的事才发起请求要下一个事来

10.一共需要启动四个程序

  • 拦截器(分布式在此实现)
  • 数量控制接口
  • 后端api接口
  • 消费端接口

11.流量控制措施

  • 同一用户抢购设置间隔
  • 对异常频繁请求的用户建立黑名单
  • 限流方法:
    • 漏桶算法
    • 令牌算法
  • 秒杀前
    • 增加冗余机器来保证稳定
    • 预估访问量等级和峰值时间
  • 秒杀中
    • 分批次秒杀

12.总结

  • 开发系统流程
    • 需求分析
    • 原型设计
    • 系统设计
    • 编码
    • 后期迭代优化
  • 秒杀难点
    • 消息队列RabbitMQ
    • Gin框架
    • 高并发分布式验证
    • token权限验证
    • 超卖问题(Redis会有瓶颈,采用go接口实现)
    • 横向扩展设计
  • 一个请求按顺序走过的节点
    • 前端CDN
    • SLB
    • 分布式权限验证
    • SLB
    • 分布式数量控制接口
    • 后端web服务器
    • 消息队列RabbitMQ
    • 数据库Mysql