使用go和消息队列优化投票功能

news2025/9/18 5:49:14

文章目录

    • 1、优化方案与主要实现代码
      • 1.1、原系统的技术架构
      • 1.2、新系统的技术架构
      • 1.3、查看和投票接口实现
      • 1.4、数据入库MySQL协程实现
      • 1.5、路由配置
      • 1.6、启动程序入口实现
    • 2、压测结果
      • 2.1、设置Jmeter线程组
      • 2.2、Jmeter聚合报告结果,支持11240/秒吞吐量
      • 2.3、Jmeter TPS结果,支持15000/秒最大并发
      • 2.4、总CPU和总内存变化情况
      • 2.5、Redis和go进程占用资源

1、优化方案与主要实现代码

有一个每年都举行的投票活动,原系统是很多年前开发,系统的支持的并发数不高,在投票期间经常出现崩掉的情况。
投票规则为按IP限制,每24小时投1票。

1.1、原系统的技术架构

运行在4核8G服务器上,用了PHP+MySQL+Redis开发,运行在4核8G的服务器上。
投票页面的功能很简单:

  • 1、是投票页面的访问,涉及当前选项的投票结果显示;
  • 2、用户点击按钮进行投票,涉及数据入库保存和投票结果刷新问题。

旧投票系统虽然都用了缓存(有缓存时间),但是在持续流量下,缓存被击穿,访问页面或点击投票,出现数据库被读写的情况,系统崩掉。

1.2、新系统的技术架构

使用Go(gin、sqlx、go-redis)+Redis缓存+Redis队列+MySQL
实现逻辑图如下:
在这里插入图片描述

Jmeter压测投票接口:吞吐量在11240/秒,TPS最大值是大于15000/秒(压测结果在后面截图)。

1.3、查看和投票接口实现

查看接口/view和投票接口/vote接口实现

package controllers

import (
	"encoding/json"
	"fmt"
	"go-vote/config"
	"go-vote/models"
	"go-vote/utils"
	"math/rand"
	"strconv"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/redis/go-redis/v9"
)

var redis_util utils.RedisUtil

func init() {
	redis_util = utils.RedisUtil{Url: config.Cache.Url, Password: config.Cache.Password}
	redis_util.Connect()
}
/**
投票记录接口
 */
func View(c *gin.Context) {
	ip := c.ClientIP()
	// 获取已投票选项
	city_id, _ := getVotedId(ip)
	data_count := getVoteCount()
	// 检查投票是否入库
	is_vote := isVoteComplete(ip)
	if is_vote == false {
		data_count[city_id] = data_count[city_id] + 1
	}
	c.JSON(200, gin.H{
		"code":       200,
		"city_id":    city_id,
		"data_count": data_count,
	})
}
/*
用户提交投票
*/
func Vote(c *gin.Context) {
	ip := c.ClientIP()
	// 获取用户本次提交的选项
	city_idstr := c.PostForm("id")
	vote_id, err_int := strconv.Atoi(city_idstr)
	if err_int != nil {
		c.JSON(200, gin.H{
			"code":    201,
			"message": "id格式错误",
		})
	}
	// 获取投票记录
	value, err2 := getVotedId(ip)
	if err2 == redis.Nil || value == "" {
		setVoteId(ip, vote_id)

		// 标记投票未入库
		setVoteComplete(ip)
		// 添加到队列
		addQueues(ip, vote_id)
		c.JSON(200, gin.H{
			"code":    200,
			"message": "成功",
		})
	} else {
		c.JSON(200, gin.H{
			"code":    400,
			"message": "失败",
		})
	}
}

func getVotedId(ip string) (string, error) {
	return redis_util.Get(ip)
}

func setVoteId(ip string, id int) error {
	return redis_util.Set(ip, id, time.Hour*24)
}

func isVoteComplete(ip string) bool {
	key := "complete_" + ip
	value, err := redis_util.Get(key)
	if err == redis.Nil || value != "n" {
		return true
	} else {
		return false
	}
}
func setVoteComplete(ip string) {
	key := "complete_" + ip
	redis_util.Set(key, "n", time.Hour*24)
}
func getVoteCount() map[string]int64 {
	filename := "city_count.json"
	count_json, err_read := utils.ReadFile(filename)
	var total_count map[string]int64
	if err_read == nil {
		if err_json := json.Unmarshal([]byte(count_json), &total_count); err_json != nil {
			panic(err_json)
		}
	} else {
		total_count = make(map[string]int64)
	}
	return total_count
}
func addQueues(ip string, id int) {
	vote_data := models.VoteData{ip, id, time.Now().Format("2006-01-02 15:04:05")}
	json_data, _ := json.Marshal(vote_data)
	err := redis_util.LPush("vote_topic", string(json_data))
	if err != nil {
		fmt.Println("addQueues======= err:", err)
	}
}

1.4、数据入库MySQL协程实现

package services

import (
	"encoding/json"
	"fmt"
	"go-vote/config"
	"go-vote/models"
	"go-vote/utils"
	"strconv"
	"time"
	"github.com/redis/go-redis/v9"
)

var redis_util utils.RedisUtil
var sqldao utils.SqlDao

func init() {
	redis_util = utils.RedisUtil{
		Url:      config.Cache.Url,
		Password: config.Cache.Password,
	}
	redis_util.Connect()
	sqldao = utils.SqlDao{
		Driver: config.Db.Driver,
		Dsn:    config.Db.Dsn,
	}
	sqldao.Connect()
}

func VoteJob() {
	layout := "2006-01-02 15:04:05"
	layout2 := "20060102"
	shanghaiZone, _ := time.LoadLocation("Asia/Shanghai")
	for {
		var list_data []interface{}
		for {
			value, err := redis_util.LPop("vote_topic").Result()
			if err == nil && value != "" {
				data := models.VoteData{}
				json.Unmarshal([]byte(value), &data)

				create_date, _ := time.ParseInLocation(layout, data.Date, shanghaiZone)
				create_day, _ := strconv.Atoi(create_date.Format(layout2))
				vote_log := models.VoteLog{CityId: data.Id, ClientIp: data.Ip, CreateDay: create_day, CreateDate: create_date}

				list_data = append(list_data, vote_log)

				if len(list_data) >= 1000 {
					saveLog(list_data)
					list_data = []interface{}{}
				}
			} else if err == redis.Nil {
				fmt.Println("break=======", time.Now().Format("2006-01-02 15:04:05"))
				break
			}
		}
		if len(list_data) > 0 {
			saveLog(list_data)
			list_data = []interface{}{}
		}
		time.Sleep(time.Second * 1)
	}
}
func saveLog(list_data []interface{}) {
	new_count := make(map[int]int64)
	count, err_insert := sqldao.InsertManyObj("insert into vote_log(city_id,client_ip,create_day,create_date) values(:city_id,:client_ip,:create_day,:create_date)", list_data)
	for _, v := range list_data {
		data := v.(models.VoteLog)
		city_id := data.CityId
		city_count, ok := new_count[city_id]
		if ok == false {
			new_count[city_id] = 1
		} else {
			new_count[city_id] = city_count + 1
		}
		client_ip := data.ClientIp
		delVoteComplete(client_ip)
	}
	filename := "city_count.json"
	count_json, err_read := utils.ReadFile(filename)
	var total_count map[string]int64
	if err_read == nil {
		if err_json := json.Unmarshal([]byte(count_json), &total_count); err_json != nil {
			panic(err_json)
		}
	} else {
		total_count = make(map[string]int64)
	}

	for k, v := range new_count {
		key := strconv.Itoa(k)
		count_total, ok := total_count[key]
		if ok == true {
			total_count[key] = count_total + v
		} else {
			total_count[key] = v
		}
	}
	datas_json, _ := json.Marshal(total_count)
	utils.WriteFile(filename, datas_json)
}
func delVoteComplete(ip string) {
	key := "complete_" + ip
	redis_util.Del(key)
}

1.5、路由配置

package routes

import (
	"fmt"
	"github.com/gin-gonic/gin"
	"go-vote/controllers"
)

var Router *gin.Engine

func init() {
	gin.SetMode(gin.ReleaseMode)
	Router = gin.Default()
	Router.Static("/static", "./static")
	Router.StaticFile("/vote.html", "./html/vote.html")
	Router.POST("/vote", controllers.Vote)
	Router.GET("/view", controllers.View)
}

1.6、启动程序入口实现

package main
import (
	"go-vote/routes"
	"go-vote/services"
)
func main() {
	go services.VoteJob()
	Router := routes.Router
	Router.Run(":8080")
}

2、压测结果

测试结果是在4核8G的Centos7虚拟机上压测。

2.1、设置Jmeter线程组

线程数1000,Ramp-up为1秒,循环次数1000,共产生100万条投票压测数据。

在这里插入图片描述

2.2、Jmeter聚合报告结果,支持11240/秒吞吐量

在这里插入图片描述

2.3、Jmeter TPS结果,支持15000/秒最大并发

在这里插入图片描述

2.4、总CPU和总内存变化情况

CPU从0%上升到31.2%最大值,随后在这个范围内上下浮动。
内存也在不断上升,压入100万数据后,内存从1.7GB上升到2.3GB,随后下降。
在这里插入图片描述

2.5、Redis和go进程占用资源

go应用./main:CPU从0%上升到280%;内存从0.3%上升到0.8%,变化不大;
redis-server:CPU从0%上升到81.2%;内存从10.3%上升到12.9%;
在这里插入图片描述
测试源码下载

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1598057.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

2022年电赛F题23年电赛D题-信号调制度测量装置说明中提到带通采样定律。

2022年电赛F题-信号调制度测量装置说明中提到带通采样定律。 23年电赛D题十分相似,但是22年载波达到了10M,根据奈奎斯特采样定理,我们知道想要分析出频谱不混叠的频谱图,采样率必须大于最大谐波的二倍。那么就意味着AD采样率要大…

2023年图灵奖揭晓,你怎么看?

Avi Wigderson——理论计算机科学的先锋,荣获2023年图灵奖 在科技界,图灵奖堪称与诺贝尔奖齐名的崇高荣誉,它每年授予对计算机行业的贡献达到重大突破的个人或团队。今年,这一声誉卓著的奖项被授予了普林斯顿大学的数学教授 Avi …

【攻防世界】lottery

弱比较代码审计 本题已提供源码,如果没提供,输入/robots.txt,发现/.git function buy($req){require_registered();require_min_money(2);$money $_SESSION[money];//接受用户原有money$numbers $req[numbers];//接受输入的数字$win_num…

面试八股——JVM★

类加载 类加载器的定义 类加载器的类别 类装载的执行过程 类的装载过程: 加载: 验证: 准备: 这里设置初始值并不是传统意义的设置初始值(那个过程在初始化阶段)。 解析: 初始化: …

树莓派安装Nginx服务结合内网穿透实现无公网IP远程访问

文章目录 1. Nginx安装2. 安装cpolar3.配置域名访问Nginx4. 固定域名访问5. 配置静态站点 安装 Nginx(发音为“engine-x”)可以将您的树莓派变成一个强大的 Web 服务器,可以用于托管网站或 Web 应用程序。相比其他 Web 服务器,Ngi…

【BlueDroid】Android BLE 蓝牙开发入门

1. 精讲蓝牙协议栈(Bluetooth Stack):SPP/A2DP/AVRCP/HFP/PBAP/IAP2/HID/MAP/OPP/PAN/GATTC/GATTS/HOGP等协议理论 2. 欢迎大家关注和订阅,【精讲蓝牙协议栈】和【Android Bluetooth Stack】专栏会持续更新中.....敬请期待&#…

在线视频下载工具lux(原annie)安装及使用教程

安装教程 下载ffmpeg,参考这篇文章:Python——Windows下载ffmpeg由于博主的系统为windows,所以选择不安装lux,直接下载.exe文件,进入lux的github网站后,选择右侧的Releases,下载下图的windows …

Maven:<dependencyManagement>:依赖集中管理

dependencyManagement Maven &#xff1c;dependencyManagement&#xff1e;&#xff0c;请介绍一下 在Apache Maven构建工具中&#xff0c;<dependencyManagement> 是一个非常重要的元素&#xff0c;用于在一个项目或一组项目的顶级POM&#xff08;Project Object Model…

MapReduce原理简介

MapReduce 是一种用于处理大规模数据集的编程模型和计算框架&#xff0c;最初由 Google 提出&#xff0c;并被 Hadoop 等开源项目广泛应用。它主要包括两个阶段&#xff1a;Map 阶段和 Reduce 阶段。下面是 MapReduce 的基本原理&#xff1a; 图示不错 MapReduce 的基本原理&…

HarmonyOS4-网络连接-http请求数据

使用Axios发送请求&#xff1a; 详细资料来源于官方文档。

消息队列RabbitMQ入门学习

目录 1.初识MQ 1.1.同步调用 1.2.异步调用 1.3.技术选型 2.RabbitMQ 2.1.收发消息 2.1.1.交换机 2.1.2.队列 2.1.3.绑定关系 2.1.4.发送消息 3.SpringAMQP 3.1WorkQueues模型 3.1.1消息接收 3.1.2测试 3.1.3.能者多劳 3.1.3.总结 3.2.交换机类型 3.3.Fanout交…

Linux学习之路 -- 进程篇 -- PCB介绍 -- 进程的孤儿和僵尸状态

前面介绍了进程的各种状态&#xff0c;下面介绍比较特殊的两种状态 -- 孤儿和僵尸&#xff08;僵死&#xff09;。 一、僵尸状态 我们创建进程的目的其实就是想要进程帮我们执行一些任务&#xff0c;当任务被执行完后&#xff0c;进程的使命其实就已经完成了。此时我们就需要…

用html写文本变形动画

<!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>文本变形动画</title><link rel"stylesheet" href"./style.css"> </head> <body> <!-- 两个文本部分…

Github 2024-04-16Python开源项目日报 Top10

根据Github Trendings的统计,今日(2024-04-16统计)共有10个项目上榜。根据开发语言中项目的数量,汇总情况如下: 开发语言项目数量Python项目10TypeScript项目1Vue项目1系统设计指南 创建周期:2507 天开发语言:Python协议类型:OtherStar数量:241693 个Fork数量:42010 次…

Java获取调用当前方法的类名或方法名(栈堆信息)的4种方式

在java代码中&#xff0c;是可以在运行时通过某种方式获取到当前方法被谁调用了&#xff08;调用链路&#xff09;。目前我所知道的有四种方式&#xff08;通过Thread、Throwable、SecurityManager获取&#xff09;&#xff0c;下面逐个列出&#xff0c;附有代码和截图。 Threa…

c# 服务创建

服务 创建服务 编写服务 可以对server1.cs重新命名&#xff0c;点击你的server按F7进入代码编辑模式&#xff0c;编写脚本 双击你的server.cs右击空白位置&#xff0c;添加安装程序&#xff0c;此时会生成“serviceInstaller1”及“serviceProcessInstaller1” 后续可以点击P…

51单片机-LED模块

文章目录 1.点亮一个LED灯2.LED闪烁3.LED流水灯 1.点亮一个LED灯 #include <REGX52.H> void main() {P20xFE; //1111 1110while(1){} }2.LED闪烁 增加延时&#xff0c;控制LED的亮灭间隙 延时函数的添加依靠STC-ISP软件的延时函数功能代码自动生成&#xff0c;如图 #i…

Springboot引入外部jar包并打包jar包

前言 spring boot项目开发过程中难免需要引入外部jar包&#xff0c;下面将以idea为例说明操作步骤 将需要的jar包导入到项目中 2.在maven中引入jar包 <dependency><groupId>com</groupId><!--随便填的文件夹名称--><artifactId>xxx</artif…

基于Material Design风格开源、易用、强大的WPF UI控件库

前言 今天大姚给大家分享一款基于Material Design风格开源、免费&#xff08;MIT License&#xff09;、易于使用、强大的WPF UI控件库&#xff1a;MaterialDesignInXamlToolkit。 项目介绍 MaterialDesignInXamlToolkit 是一个开源、易于使用、强大的 WPF UI 控件库&#x…

【MySQL】20. 使用C语言链接

mysql connect mysql的基础&#xff0c;我们之前已经学过&#xff0c;后面我们只关心使用 要使用C语言连接mysql&#xff0c;需要使用mysql官网提供的库&#xff0c;大家可以去官网下载 我们使用C接口库来进行连接 要正确使用&#xff0c;我们需要做一些准备工作&#xff1a; …