使用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 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.mfbz.cn/a/557969.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

vue 一键更换主题颜色

这里提供简单的实现步骤,具体看自己怎么加到项目中 我展示的是vue2 vue3同理 在 App.vue 添加 入口处直接修改 #app { // 定义的全局修改颜色变量--themeColor:#008cff; } // 组件某些背景颜色需要跟着一起改变,其他也是同理 /deep/ .ant-btn-primar…

『FPGA通信接口』汇总目录

Welcome 大家好,欢迎来到瑾芳玉洁的博客! 😑励志开源分享诗和代码,三餐却无汤,顿顿都被噎。 😭有幸结识那个值得被认真、被珍惜、被捧在手掌心的女孩,不出意外被敷衍、被唾弃、被埋在了垃圾堆。…

【Linux学习】Linux编辑器-vim使用

这里写目录标题 1. 🌠vim的基本概念🌠2. vim的基本操作🌠3.vim异常处理🌠4. vim正常模式的相关命令🌠5. vim末(底)行模式相关命令 vi/vim都是多模式编辑器,不同的是vim是vi的升级版本…

开发与产品的战争之自动播放视频

开发与产品的战争之自动播放视频 起因 产品提了个需求,对于网站上的宣传视频,进入页面就自动播放。但是基于我对chromium内核的一些浅薄了解,我当时就给拒绝了: “浏览器不允许”。(后续我们浏览器默认都是chromium内核的&#…

2024年华中杯数模竞赛A题完整解析(附代码)

2024年华中杯数模竞赛A题 基于动态优化的太阳能路灯光伏板朝向以最大化能量收集研究摘要问题重述问题分析模型假设符号说明 代码问题一 完整资料获取 基于动态优化的太阳能路灯光伏板朝向以最大化能量收集研究 摘要 随着可再生能源技术的发展,太阳能作为一种清洁的…

C++类与对象(中)②

目录 1.赋值运算符重载 1.1运算符重载 1.2赋值运算符重载 1.2.1赋值运算符重载格式 1.2.2赋值运算符只能重载成成员函数不能重载成全局函数 1.2.3同拷贝函数一样,如果类是形如日期类这样变量全是内置类型的,赋值运算符就必须自己实现,…

Spectre-v1 简介以及对应解决措施

文章目录 前言一、Variant 1: Exploiting Conditional Branches.二、 BACKGROUND2.1 Out-of-order Execution2.2 Speculative Execution2.3 Branch Prediction2.4 The Memory Hierarchy2.5 Microarchitectural Side-Channel Attacks2.6 Return-Oriented Programming 三、 ATTAC…

大学生简历大赛演讲稿(6篇)

大学生简历大赛演讲稿(6篇) 以下是六篇大学生简历大赛演讲稿的范文,供您参考: 范文一:展现真我,点亮未来 尊敬的评委、亲爱的同学们: 大家好! 今天,我站在这里&#xf…

区块链实验室(35) - 编译solana for ARM64版

今天终于成功编译solana for arm64版,编译时间巨长。见下图所示。编译步骤详见solana网站https://github.com/solana-labs/solana和https://docs.solanalabs.com/。

【C语言】【数据结构】项目实践——贪吃蛇游戏(超详细)

前言 本篇博客我们来实现一个小游戏项目——贪吃蛇,相信肯定很多人都玩过,那么整个贪吃蛇是怎么实现出来的那,这个项目用到了很多方面的知识:C语言函数、枚举、结构体、动态内存管理、预处理指令、链表、Win32 API等。我们就通过这…

nodejs工具模块学习

util 是一个Node.js 核心模块,提供常用函数的集合; util.inspect(object,[showHidden],[depth],[colors]) 是一个将任意对象转换 为字符串的方法,通常用于调试和错误输出; 如果只有一个参数 object,是要转换的对象&…

网工内推 | 兴业银行总行正编,科技运维部,硕士以上学历

01 兴业银行 招聘岗位:安全渗透专家 职责描述: 1.负责牵头组织本行红蓝对抗、攻防演练等工作; 2.负责牵头制定有效的渗透测试方案,开展对本行防御体系的验证工作; 3.负责牵头组织本行各类应用系统的渗透测试与漏洞扫…

java的Spring XML和注解解析深入理解

正文 熟悉IOC体系结构 要学习Spring源码,我们首先得要找准入口,那这个入口怎么找呢?我们不妨先思考一下,在Spring项目启动时,Spring做了哪些事情。这里我以最原始的xml配置方式来分析,那么在项目启动时&a…

大型网站系统架构演化实例_5.使用反向代理和CDN加速网站响应

1.使用反向代理和CDN加速网站响应 随着网站业务不断发展,用户规模越来越大,由于区域的差别使得网络环境异常复杂,不同地区的用户访问网站时,速度差别也极大。有研究表明,网站访问延迟和用户流失率正相关,网…

二叉检索树(定义、意义、存储数据元素形式),二叉检索树插入方法的图解和实现

1、二叉检索树: (1)定义 二叉检索树的任意一个结点,设其值为k,则该节点左子树中任意一个结点的值都小于k;该节点右子树中任意一个节点的值都大于或等于k 这里的比较规则可以是针对数字的,也可…

js实现抽奖效果

<!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>随机抽奖</title> </head> <body>…

synchronized锁升级原理

锁升级过程 jdk1.6之后的优化 synchronized锁有四种状态&#xff0c;无锁&#xff0c;偏向锁&#xff0c;轻量级锁&#xff0c;重量级锁&#xff0c;这几个状态会随着竞争状态逐渐升级&#xff0c;锁可以升级但不能降级&#xff0c;但是偏向锁状态可以被重置为无锁状态。 1、偏…

C++ 类和对象(终篇)

初始化列表 就是给我们每一个成员变量找了一个定义的位置&#xff0c;不然像const这样的成员不好处理 所有的成员能在初始化列表初始化的都在里面初始化 拷贝构造函数和构造函数都允许初始化 构造函数体中的语句只能将其称作为赋初值&#xff0c;而不能称作初始化。 因为初始…

牛客NC314 体育课测验(一)【中等 图,BFS,拓扑排序 Java,Go、PHP】

题目 题目链接&#xff1a; https://www.nowcoder.com/practice/1a16c1b2d2674e1fb62ce8439e867f33 核心 图&#xff0c;BFS,拓扑排序&#xff0c;队列参考答案Java import java.util.*;public class Solution {/*** 代码中的类名、方法名、参数名已经指定&#xff0c;请勿修…

Scala 03 —— Scala Puzzle 拓展

Scala 03 —— Scala Puzzle 拓展 文章目录 Scala 03 —— Scala Puzzle 拓展一、占位符二、模式匹配的变量和常量模式三、继承 成员声明的位置结果初始化顺序分析BMember 类BConstructor 类 四、缺省初始值与重载五、Scala的集合操作和集合类型保持一致性第一部分代码解释第二…
最新文章