当前位置: 首页 > news >正文

Golang|抽奖相关

文章目录

    • 抽奖核心算法
    • 生成抽奖大转盘
    • 抽奖接口实现

抽奖核心算法

  • 我们可以根据 单商品库存量/总商品库存量 得到每个商品被抽中的概率,可以想象这样一条 0-1 的数轴,数轴上的每一段相当于一种商品,概率之和为1

在这里插入图片描述

  • 抽奖时,我们会生成 U(0,1) 上的一个随机数,这个数位于哪个线段上就对应着抽中了对应的商品。
  • 构建线段,时间复杂度 O(n)
  • 用二分查找算法查找随机数位于哪一段,时间复杂度 O(logn),采集k个样本需要再乘以k

  • 接下来介绍二分查找区间算法:
  • N 个点把实数域分割成 N+1 段,target 是随机生成的实数
  • target 应该落在哪一段上?
  • 定义 array[i-1] < target < array[i] 为落在第 i 条线段上,代表第 i 个奖品被抽中
    在这里插入图片描述
// BinarySearch 查找 >= target 的最小元素下标,arr单调递增(不能存在重复元素)
// 如果target比arr的最后一个元素还大,返回最后一个元素下标
func BinarySearch(arr []float64, target float64) int {
	if len(arr) == 0 {
		return -1
	}
	left := 0
	right := len(arr)
	for left < right {
		// 通用条件
		if target <= arr[left] {
			return left
		}
		if target > arr[right-1] {
			return right
		}
		// len(arr) == 2, mid在left和right之间, 选择left的概率值
		if left == right-1 {
			return right
		}
		// len(arr) >= 3
		mid := (left + right) / 2
		if target < arr[mid] {
			right = mid
		} else if target == arr[mid] {
			return mid
		} else {
			left = mid // NOTE: 这里不是找直接数值,而是区间
		}
	}
	return -1
}

在这里插入图片描述

生成抽奖大转盘

  • 首先看看我们对于抽奖大转盘所设计的 mysql 数据库表结构
-- ----------------------------
-- DataBase
-- ----------------------------
CREATE DATABASE lottery;

use lottery;

-- ----------------------------
-- Table structure for inventory
-- ----------------------------
DROP TABLE IF EXISTS `inventory`;
CREATE TABLE `inventory` (
    `id` int(11) NOT NULL AUTO_INCREMENT COMMENT "奖品id, 自增",
    `created_at` DATETIME(3) NULL DEFAULT CURRENT_TIMESTAMP COMMENT "创建时间",
    `updated_at` DATETIME(3) NULL DEFAULT NULL COMMENT "更新时间",
    `deleted_at` DATETIME(3) NULL DEFAULT NULL COMMENT "删除时间",
    `name` varchar(20) NOT NULL COMMENT "奖品名称",
    `description` varchar(100) NOT NULL DEFAULT "" COMMENT "奖品描述",
    `picture` varchar(200) NOT NULL DEFAULT "0" COMMENT "奖品图片",
    `price` int(11) NOT NULL DEFAULT "0" COMMENT "价值",
    `count` int(11) NOT NULL DEFAULT "0" COMMENT "库存量",
    PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=20 DEFAULT CHARSET=utf8 COMMENT="奖品库存表";

insert into `inventory` (id,name,picture,price,count) values (1,'谢谢参与','img/face.png',0,0);
insert into `inventory` (name,picture,price,count) values ('篮球','img/ball.jpeg',100,1000),('水杯','img/cup.jpeg',80,1000),('电脑','img/laptop.jpeg',6000,200),('平板','img/pad.jpg',4000,300),('手机','img/phone.jpeg',5000,400),('锅','img/pot.jpeg',120,1000),('茶叶','img/tea.jpeg',90,1000),('无人机','img/uav.jpeg',400,100),('酒','img/wine.jpeg',160,500);

-- ----------------------------
-- Table structure for order
-- ----------------------------
DROP TABLE IF EXISTS `order`;
CREATE TABLE `order` (
    `id` int(11) NOT NULL AUTO_INCREMENT COMMENT "订单id, 自增",
    `created_at` DATETIME(3) NULL DEFAULT CURRENT_TIMESTAMP COMMENT "创建时间",
    `updated_at` DATETIME(3) NULL DEFAULT NULL COMMENT "更新时间",
    `deleted_at` DATETIME(3) NULL DEFAULT NULL COMMENT "删除时间",
    `gift_id` int(11) NOT NULL COMMENT "商品id",
    `user_id` int(11) NOT NULL COMMENT "用户id",
    `count` int(11) NOT NULL DEFAULT "1" COMMENT "购买数量",
    PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=189549 DEFAULT CHARSET=utf8mb4 COMMENT="订单表";

在这里插入图片描述
在这里插入图片描述

  • 上面这一段,在数据量不大的时候还是可以的,但是数据量一大,在千万级以上大表的场景下就不行啦,会导致长时间的阻塞,而且读出来内存也不够

在这里插入图片描述

  • V2是对其优化,主要是一个分页查询思路,每次把一页的数据放入一个channel中,然后用一个go协程每次从channel中读出数据写入redis
    在这里插入图片描述

  • 对于每个商品的 count 字段,每次被抽中都应该 count 对应的值减1,但是在高并发的情况下mysql可能扛不住这么大并发量下的频繁写入,考虑先记录在redis里面,真正的减1操作是在redis里面实现的。

  • 在前端页面初始化的时候我们需要把整个大转盘的页面通过后端服务器返回的数据去渲染出整个大转盘出来,所以需要一开始通过 InitInventory 函数获得所有奖品的初始库存,存入redis。

在这里插入图片描述

  • 看看前端的代码:
<body>
    <div class="center" id="my-lucky"></div>
    <script>
        var giftMap = new Map();  //维护奖品ID和转盘里奖品index的对应关系
        $(document).ready(function () {
            $.ajax({
                type: "GET",
                url: "api/v1/gifts",
                success: function (data) {
                    console.log(data)
                    let gifts = data["data"]
                    var prizes=new Array();
                    $.each(gifts,function(index,gift){
                        giftMap[gift.Id]=index;
                        prizes[index]= { background: '#e9e8fe', fonts: [{ text: gift.Name }], imgs:[{src:gift.Picture,top:30,width:80,height:80}] };
                    })
                    // 直接使用luch-canvas抽奖插件  https://100px.net/usage/js.html
                    const myLucky = new LuckyCanvas.LuckyWheel('#my-lucky', {
                        width: '600px',
                        height: '600px',
                        blocks: [{ padding: '10px', background: '#869cfa' }],
                        prizes: prizes,
                        buttons: [
                            { radius: '40%', background: '#617df2' },
                            { radius: '35%', background: '#afc8ff' },
                            {
                            radius: '30%', background: '#869cfa',
                            pointer: true,
                            fonts: [{ text: '抽奖', top: '-10px' }]
                            },
                        ],
                        start: function() {
                            $.ajax({
                                type: "GET",
                                url: "api/v1/lucky",
                                success: function (giftId) {
                                    if(giftId=="0"){
                                        alert("抽奖结束")
                                    }else{
                                        myLucky.play();
                                        idx=giftMap[giftId];
                                        myLucky.stop(idx);
                                    }
                                }
                            }).fail(function (result, result1, result2) {
                                alert("出错了");
                            });
                        },
                        end: function(prize) { // 游戏停止时触发
                            alert('恭喜中奖: ' + prize.fonts[0].text)
                        }
                    })
                }
            }).fail(function (result, result1, result2) {
                $('#my-lucky').html("数据加载失败");
            });
        });
      </script>
</body>
  • 我们可以看到前端的代码逻辑是先放一个空的div,然后页面加载好之后通过js代码发起一个请求去请求"/gifts"这个接口获得数据渲染生成大转盘。
  • 这里有个小细节,我们后端返回给前端gifts数据的时候要记得抹掉敏感信息,也就是说我们的抽奖概率是通过商品的库存量来决定的,我们不希望前端拿到json字符串后看到库存量,所以我们的gifts返回给前端的时候记得把所有的库存量都置为0。

在这里插入图片描述

type Inventory struct {
	ID 			uint 	`gorm:"column:id"`
	Name 		string 	`gorm:"column:name"`
	Description string  `gorm:"column:description"`
	Picture 	string 	`gorm:"column:picture"`
	Price 		int 	`gorm:"column:price"`
	Count 		int		`gorm:"column:count"`
}

在这里插入图片描述

  • 上面这段代码就是从redis上获取所有奖品的库存量,其中商品id作为key的时候会统一加一个前缀prefix

抽奖接口实现

在这里插入图片描述

  • 当我们按下抽奖这个按钮后,前端会用js代码请求"/lucky"接口,由后端返回本次抽奖抽中了哪个商品id

在这里插入图片描述
在这里插入图片描述

  • ids和probs两个是一一对应的,一个存的是奖品id一个存的是奖品的库存量
  • 如果奖品已经count为0了,说明已经抽没了,不应该再参与抽奖
  • 为什么要给前端传一个0,因为这个0有特殊的意思,前端收到0后会告诉用户抽奖已经结束
  • 有可能同时对一个库存量为1的商品去执行减1操作,会导致库存量为负数,这个时候我们会执行新一轮的抽奖,重新再抽一遍,如果执行指定次数后还是失败,则会返回最后一行代码,谢谢参与

在这里插入图片描述

  • redis Decr 是支持原子性支持并发的

相关文章:

  • 路由器端口映射的意思、使用场景、及内网ip让公网访问常见问题和解决方法
  • 项目部署-(二)Linux下SpringBoot项目自动部署
  • MySQL流程控制
  • 如何管理“灰色时间”导致的成本漏洞
  • SOLID原则详解:提升软件设计质量的关键
  • 【DDR 内存学习专栏 1.3.1 -- DDR 的 Bank 及 burst 访问】
  • 空间信息可视化——WebGIS前端实例(一)
  • 基于Nacos+动态线程池的分布式系统弹性设计:投行交易与风控场景实战
  • 安当TDE透明加密技术:企业机密文件和数据库加密解决方案
  • 大数据学习栈记——Redis安装及其使用
  • MVCC是什么?MVCC的作用是什么?MVCC实现方式有哪些?
  • 批量将多个文件转成压缩包,支持批量设置压缩密码
  • CSI-external-provisioner
  • 从零开始学A2A一:A2A 协议概述与核心概念
  • 人工智能驱动的科研新范式及学科应用研究
  • 【Java学习笔记】Java初级阶段代码规范
  • 基于项目管理的轻量级目标检测自动标注系统【基于 YOLOV8】
  • 打造可控可测的星座网络:IPLOOK低轨通信仿真平台搭建实践
  • 小葱桌面电视版下载_小葱桌面app免费下载最新版
  • 关于 软件开发模型 的分类、核心特点及详细对比分析,涵盖传统模型、迭代模型、敏捷模型等主流类型
  • 理想汽车副总裁刘杰:不要被竞争牵着鼻子走,也不迷信护城河
  • 游戏论|迟来的忍者与武士:从《刺客信条:影》论多元话语的争议
  • 李家超称香港将部署为内地企业提供供应链服务,突破美国封锁
  • 吏亦有道|秦汉的发明家与技术传承
  • 经济日报:美离间国际关系注定徒劳无功
  • 加拿大财长:加拿大需要抗击美国关税