文章目录
- 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%;
测试源码下载