1 背景介绍
并发量非常大的系统,例如秒杀、抢红包、抢票等操作,都是存在溢出现象,比如秒杀超卖、抢红包超额、一票多单等溢出现象,如果采用数据库锁来控制溢出问题,效率非常低,在高并发场景下,很有可能直接导致数据库崩溃,因此针对高并发场景下数据溢出解决方案我们可以采用Redis缓存提升效率。
1.1 设计分析微信抢红包
用户抢红包的时候,我们会分批次发放红包,每次发放红包会先根据发放红包的金额和红包的个数把每个红包计算好,然后存入到Redis队列中,存入到Redis队列后,用户每次抢红包都直接从Redis队列中获取一个红包即可,由于Redis是单线程,在这里可以有效的避免多个人同时抢到了一个红包,类似一种超卖现象。
表结构设计:
CREATE TABLE `money_package` ( `id` int(11) NOT NULL COMMENT '主键ID', `money` int(11) NOT NULL COMMENT '红包总金额', `count` int(11) NOT NULL COMMENT '红包数量', `sort` int(2) DEFAULT NULL COMMENT '发红包顺序', `type` int(11) DEFAULT '1' COMMENT '红包发放类型 1:延时发放 2:立即发放', `hasload` int(1) DEFAULT '1' COMMENT '加载位置 1:未加载 2:加载到程序 3:加载到Redis缓存', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
1.2 红包定时导入缓存队列
抢红包缓存队列溢出控制
上面已经实现将红包存入到Redis缓存队列中,用户每次抢红包的时候,只需要从Redis缓存队列中获取即可。
2 队列术限流
2.1 高并发场景分析
用户抢红包的高并发场景下,如果让后端服务器直接处理所有抢红包操作,服务器很有可能会崩溃,就像高铁站如果很多人蜂拥挤进站而不排队,很有可能导致整个高铁站秩序混乱,高铁站服务崩溃,程序也是如此。
普通模式:
队列术:
解决大量并发用户蜂拥而上的方法可以采用队列术将用户的请求用队列缓存起来,后端服务从队列缓存中有序消费,可以防止后端服务同时面临处理大量请求。缓存用户请求可以用RabbitMQ、Kafka、RocketMQ、ActiveMQ,我们这里采用RabbitMQ即可。
预备知识:MQ引入项目后的优缺点
2.2 队列削峰实战
用户抢红包的时候,我们用Lua脚本实现将用户抢红包的信息以生产者角色将消息发给RabbitMQ,后端应用服务以消费者身份从RabbitMQ获取消息并抢红包,再将抢红包信息以WebSocket方式通知给用户。如果想使用Lua识别用户令牌,我们需要引入 lua-resty-jwt 模块,是用于 ngx_lua 和 LuaJIT 的 Lua 实现库,在该模块能实现Jwt令牌生成、Jwt令牌校验,依赖库的地址: https://github.com/SkyLothar/lua-resty-jwt
3 设计原则
3.1 动静分离
1·后台springboot启动微服务模块
2.静态文件分离,nginx直接响应,不要再绕后台应用机器
3.2 微服务化
1·将模块细粒度拆分,微服务化
2·借助docker swarm的容器管理功能,实现不同服务的副本部署,滚动更新
3·在本项目中,api模块就部署了3份,以适应前端的高并发
3.3 负载均衡
1·多个实例之间通过nginx做负载均衡,提升并发性能
2·本项目为大家展示的模块均部署在1台节点。生产环境涉及多台机器,用upstream实现。
3.4 异步消息
1·中奖后,中奖人及奖品信息要持久化到数据库。引入rabbitmq,将抽奖操作与数据库操作异步隔离。
2·抽奖中奖后,只需要将中奖信息放入rabbitmq,并立即返回中奖信息给前端用户。
3·后端msg模块消费rabbitmq消息,缓慢处理。
3.5 缓存预热
1·每隔1分钟扫描一次活动表,查询未来1分钟内将要开始的活动。
2·将扫到的活动加载进redis,包括活动详细信息,中奖策略信息,奖品信息,抽奖令牌。
3·活动正式开始后,基于redis数据做查询,不必再与数据库打交道。
4 Nginx通过LUA脚本访问RabbitMQ消息队列
配置设置:OpenRestry安装
local strlen = string.len local json = require "json" local rabbitmq = require "rabbitmqstomp" local mq, err = rabbitmq:new() if not mq then return end mq:set_timeout(60000) local ok, err = mq:connect { host = "127.0.0.1", port = 61613, username = "guest", password = "guest", vhost = "/" } if not ok then return end ngx.log(ngx.INFO, "Connect: " .. "OK") local msg = {key="value1", key2="value2"} local headers = {} headers["destination"] = "/queue/my_queue" headers["receipt"] = "msg#1" headers["app-id"] = "luaresty" headers["persistent"] = "true" headers["content-type"] = "application/json" local ok, err = mq:send(json.encode(msg), headers) if not ok then return end ngx.log(ngx.INFO, "Published: " .. json.encode(msg)) local headers = {} headers["destination"] = "/queue/my_queue" headers["persistent"] = "true" headers["id"] = "123" local ok, err = mq:subscribe(headers) if not ok then return end local data, err = mq:receive() if not data then return end ngx.log(ngx.INFO, "Consumed: " .. data) ngx.header.content_type = "text/plain"; ngx.say(data); local headers = {} headers["persistent"] = "true" headers["id"] = "123" local ok, err = mq:unsubscribe(headers) local ok, err = mq:set_keepalive(10000, 10000) if not ok then return end
location /rabbitmq { lua_code_cache off; content_by_lua_file conf/rabbitmq.lua; }