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

文章目录

    • 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 controllersimport ("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.RedisUtilfunc 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_" + ipvalue, err := redis_util.Get(key)if err == redis.Nil || value != "n" {return true} else {return false}
}
func setVoteComplete(ip string) {key := "complete_" + ipredis_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]int64if 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 servicesimport ("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.SqlDaofunc 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.CityIdcity_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.ClientIpdelVoteComplete(client_ip)}filename := "city_count.json"count_json, err_read := utils.ReadFile(filename)var total_count map[string]int64if 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_" + ipredis_util.Del(key)
}

1.5、路由配置

package routesimport ("fmt""github.com/gin-gonic/gin""go-vote/controllers"
)var Router *gin.Enginefunc 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.RouterRouter.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.mzph.cn/diannao/678.shtml

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

相关文章

java中MD5加密

MD5加密 MD5加密是不可逆的加密方式,A可以根据MD5加密转换成B,但是B不能再转换成A String passwordDigestUtils.md5DigestAsHex(password.getBytes());完成密码的加密

【情侣博客网站】

效果图 PC端 建塔教程 第一步:下载网站源码(在文章下方有下载链接) 第二步:上传到服务器或虚拟主机,解压。 第三步:这一步很关键,数据库进行连接,看图 admin/connect.php就是这…

[Android]Jetpack Compose设置颜色

在 Kotlin 和 Jetpack Compose 中设置颜色是一个非常直接的过程,涉及到使用 Color 类来定义和使用颜色。 Jetpack Compose 提供了多种方式来定义和应用颜色,包括预定义颜色、RGB 值、十六进制值等。下面是一些常用的设置颜色的方法: 1. 使用…

python-基础(4)-list

python专栏地址 上一篇:python-基础(3)-字符串操作 List结构 本节将学习以下内容 list初识list的操作 一、List初识 创建 通过[]/list([])创建 ,两者的区别可以参考python中用list和中括号创建列表有什么区别?(在创建时相同,但一个的实质时…

Spring Data Jpa的save方法更新未传值的字段被更新为空的处理方法

Spring Data Jpa的save()方法通过主键是否为空来判断insert或是update操作,但更新方法和以往使用的mybatis-plus存在一定的差异,特别记录处理方法。 Resourceprivate Dao dao;/*** 更新操作* param data 前端传入存在更新的字段值的对象*/public void up…

pat乙-1020月饼

贪心:既然有存货量一定,利润要最高; 这个贪心就在于我看“单价”最高,这个单价也是要把存货量算进去的,所以按“单价”排序,再遍历,优选选择“单价”最高的,不够的再补,…

el-menu 该有的页面显示不出来第一个应该想到的问题首先就算检查是否多写了一个 , 导致显示不出来原有的页面

问题描述 el-menu 该有的页面显示不出来第一个应该想到的问题首先就算检查是否多写了一个 , 导致显示不出来原有的页面 如图所示多写了一个,就会导致该有的页面显示不出来。

Python 天气预测

Python天气预测通常涉及到数据采集、数据预处理、选择和训练模型、以及预测和可视化等步骤。以下是使用Python进行天气预测的一般流程: 数据采集 使用爬虫技术从天气网站(如Weather Underground、中国天气网等)爬取历史天气数据&#xff0c…

my.cnf配置文件调优

mysql数据库的性能调优首先要考虑的就是表结构设计,一个糟糕的设计模式即使在性能强劲的服务器上运行时,也会表现得很差。与设计模式相似,查询语句也会影响mysql的性能,应该避免写出低效的sql查询语句。最后要考虑的就是参数优化,mysql数据库默认设置的性能非常差,只能起…

LeetCode 628. 三个数的最大乘积 java版

1. 官网: . - 力扣(LeetCode) 2. 题目: 给你一个整型数组 nums ,在数组中找出由三个数组成的最大乘积,并输出这个乘积。 示例 1: 输入:nums [1,2,3] 输出:6示例 2&…

Java中的递归方法:初学者的简明指南

Java中的递归方法:初学者的简明指南 递归是编程中的一个重要概念,它指的是一个方法直接或间接地调用自身。递归方法在处理某些问题时,特别是那些可以分解为更小、更简单的子问题时,非常有用。虽然递归的概念初看起来可能有些复杂…

QEMU_v8搭建OP-TEE运行环境

文章目录 一、依赖下载二、设置网络三、安装下载四、运行OP-TEE 一、依赖下载 更新依赖包,下载一系列依赖。比如Python需要Python3.x版本,需要配置git的用户名和邮箱等。这里不详细展开了,很多博客都有涉及到。 二、设置网络 这一点非常重…

【nginx代理和tengine的启动-重启等命令】

在nginx成功启动后[任务管理器有nginx.exe进程],运行vue项目,在浏览器访问http://localhost:10001/,提示:访问拒绝(调试中network某些地址403); 解决方案: localhost改为ip&#xff…

已解决:用 Pyinstaller 模块将整个GUI项目打包成 单独exe 文件、移到没有python的环境依旧可以运行(全网完美解决的方法)

1.环境 我的项目是pyside2实现的GUI项目,涉及文件读写、panda操作等,项目包括ui文件、ico文件、项目模块(项目中有多个不同的模块、每个模块里面有代码)。项目内容是可以使用该GUI框架对别的数据文件进行读取、加工处理、保存等一系列操作。win11+python3.8+pyside2+其它库…

【Flutter】GetX状态管理及路由管理用法

目录 一、安装二、使用1.安装GetX插件,快捷生成模版代码2.主入口MaterialApp改成GetMaterialApp3.定义路由常量RoutePath类、别名映射页面RoutePages类4. 初始initialRoute,getPages。5.调用 总结 一、安装 dependencies: get: ^4.6.6二、使用 1.安装G…

MDK-ARM Keil5.38 下载安装环境搭建

一、keil软件介绍 KEIL是公司的名称,有时候也指KEIL公司的所有软件开发工具,目前2005年Keil由ARM公司收购,成为ARM的公司之一。 MDK(Microcontroller Development Kit) 也称MDK-ARM、KEIL MDK、RealView MDK、KEIL For…

[最新]访问/加速StackOverFlow的方法

但是有很多问题都是在StackOverFlow上有现成的解决方案,而某度搜索引擎…前一页的回答互相抄袭,看着实在胀眼睛。 话不多说,解决办法: 直接访问插件商店下载插件(最快捷方便,点点就行)&#x…

Python中的迭代器:深入理解与实用指南

文章目录 1. 迭代器的基本概念2. Python中的迭代器实例3. 自定义迭代器3.1 例子3.2 详细过程 4. 迭代器的高级应用5. 常见问题与解答 迭代器是Python中非常核心的概念之一,在面试中也会被问到。下面我会详细介绍什么是迭代器,使用方法,以及使…

python_表格处理_pandas_pd.read_csv输入输出参数说明

pd.read_csv 输入参数说明 pd.read_csv() 函数用于从 CSV 文件中读取数据,并返回一个 DataFrame 对象。它的常用输入参数说明如下: filepath_or_buffer:CSV 文件的路径或者文件对象,可以是本地文件路径、URL、文件类对象等。sep&…

怎么转行做产品经理?

小白转产品经理第一点要先学基础理论知识,学了理论再去实践,转行,跳槽! 学理论比较好的就是去报NPDP的系统班,考后也会有面试指导课、职场晋升课程,对小白来说非常合适了~(B站:不爱…