数据设计
E-R图
数据主体是活动(game),内置活动策略(game_rules),通过关联表(game_product)和奖品(product)联动,和用户(user)表一起查中奖记录。
数据表
活动表 card_game
会员表 card_user
奖品表 card_product
奖品活动关联 card_game_product
策略表 card_game_rules
中奖记录 card_user_hit
视图
在其基础上添加视图,相当于提前写好了一些多表关联的sql来方便查询。
中奖信息 view_card_user_hit
奖品数统计 view_game_curinfo
概要设计
系统拓扑
业务架构
管理后台
抽奖接口层
抽奖前台⻚面
软件架构
管理后台可以采用快速开发平台完成增删改查
接口层采用Springboot
前端使用Vue
设计原则
1)动静分离
1.后台springboot启动微服务模块
2.静态文件分离,nginx直接响应
2)微服务化
1.可以将接口层搭建两个Springboot
2.主流程和中奖后的处理流程拆分成不同的服务
3)负载均衡
1.接口层可以启动多个实例,通过nginx做负载均衡,提升并发性能
2.开发期间可以启动本地1台节点。生产部署会涉及多台机器,用nginx实现。
4)异步消息
1.中奖后,中奖人及奖品信息要持久化到数据库。引入rabbitmq,将抽奖操作与数据库操作异步隔离。
2.抽奖中奖后,只需要将中奖信息放入rabbitmq,并立即返回中奖信息给前端用户。 3·后端msg模块消费rabbitmq消息,缓慢处理。
5)缓存预热
1.每隔1分钟扫描一次活动表,查询未来1分钟内将要开始的活动。
2.将扫到的活动加载进redis,包括活动详细信息,中奖策略信息,奖品信息,抽奖令牌。 3·活动正式开始后,基于redis数据做查询,不必再与数据库打交道。
缓存体系
1)活动基本信息 k-v,以活动id为key,活动对象为value,永不超时
redisUtil.set(RedisKeys.INFO+game.getId(),game,-1);
2)活动策略信息
使用hset,以活动id为group,用户等级为key,策略值为value
redisUtil.hset(RedisKeys.MAXGOAL + game.getId(),r.getUserlevel()+"",r.getGoalTimes());
redisUtil.hset(RedisKeys.MAXENTER +
game.getId(),r.getUserlevel()+"",r.getEnterTimes());
3)抽奖令牌桶 双端队列,以活动id为key,在活动时间段内,随机生成时间戳做令牌,有多少个奖品就生成多少个令牌。令牌即奖品发放的时间点。从小到大排序后从右侧入队。
redisUtil.rightPushAll(RedisKeys.TOKENS + game.getId(),tokenList);
4)奖品映射信息
k-v , 以活动id_令牌为key,奖品信息为value,会员获取到令牌后,如果令牌有效,则用令牌token值,来这里获取 奖品详细信息
redisUtil.set(RedisKeys.TOKEN + game.getId() +"_"+token,cardProduct,expire);
5)令牌设计技巧 假设活动时间间隔太短,奖品数量太多。那么极有可能产生的时间戳发生重复。
解决技巧:额外再附加一个随机因子。将 (时间戳 * 1000 + 3位随机数)作为令牌。抽奖时,将抽中的令牌/1000 ,还原真实的时间戳。
//活动持续时间(ms)
long duration = end - start;
long rnd = start + new Random().nextInt((int)duration);
//为什么乘1000,再额外加一个随机数呢? - 防止时间段奖品多时重复
long token = rnd * 1000 + new Random().nextInt(999);
6)中奖计数 k-v,以活动id_用户id作为key,中奖数为value,利用redis原子性,中奖后incr增加计数。 抽奖次数计数也是同样的道理
redisUtil.incr(RedisKeys.USERHIT+gameid+"_"+user.getId(),1);
7)中奖逻辑判断 : 抽奖时,从令牌桶左侧出队和当前时间比较,如果令牌时间戳小于等于当前时间,令牌有效,表示中奖。大于当前 时间,则令牌无效,将令牌还回,从左侧压入队列。
业务时序图