记使用sjson的一次小事故

1. 前言

之前在设计一个兼容函数的时候,使用了sjson动态设入参数,从而实现一些参数的兼容。大致的逻辑如下所示:

sql 复制代码
// 有一堆不规则的json数据
{
	"a":"aaa",
	"b":"bbb",
	"any_key1":{"key":"value"},
	"any_key2":{"key":"value"}
}
// 因为any_key1和any_key2这样的数字字符串的key还会有新增,所以这个是无法固定的,没有办法给到一个具体的结构体解析,于是就人为的兼容,给它们在处理的时候包一个外层的key
{
	"a":"aaa",
	"b":"bbb",
	"number_key_data":{
		"any_key1":{"key":"value"},
	  "any_key2":{"key":"value"}
	}
}
// 这样处理之后,我只要定义number_key_data,我就可以获取到具体的字符串数字为key的数据

2. 实现

采用sjson对对应的非特定key进行新的结构值设入,然后做一个兼容即可实现上面的逻辑,于是有这段代码出来了。

go 复制代码
package gjson_set_study

import (
	"encoding/json"
	"fmt"
	"github.com/tidwall/sjson"
	"testing"
)

func TestGjsonSet(t *testing.T) {
	data := `{"a":"aaa","b":"bbb","any_key1":{"key":"value"},"any_key2":{"key":"value"}}`
	dataMap := map[string]interface{}{}
	err := json.Unmarshal([]byte(data), &dataMap)
	if err != nil {
		panic(err)
	}
	fmt.Println(adaptNumberKey(data, dataMap))
	// output
	// {"a":"aaa","b":"bbb","number_key_data":{"any_key1":{"key":"value"},"any_key2":{"key":"value"}}} <nil>
}

type Data struct {
	A             string `json:"a"`
	B             string `json:"b"`
	NumberKeyData map[string]struct {
		Key   string `json:"key"`
		Value string `json:"value"`
	} `json:"number_key_data"`
}

var specificKeys = map[string]bool{
	"a": true,
	"b": true,
}

func adaptNumberKey(data string, dataMap map[string]interface{}) (string, error) {
	var err error
	for k, v := range dataMap {
		if _, ok := specificKeys[k]; ok {
			continue
		}
		data, err = sjson.Delete(data, k) // remove first
		if err != nil {
			return "", fmt.Errorf("delete key data error, key=%s, err=%w", k, err)
		}
		data, err = sjson.Set(data, "number_key_data."+k, v)
		if err != nil {
			// log...
			return "", fmt.Errorf("set number_key_data error, key=%s, err=%w", k, err)
		}
	}
	return data, nil
}

上面的实现,不出意外的话是不会有任何的问题,但是不出意外的意外出现了,当类似的逻辑代码上线之后,我们发现一个问题:容器会爆内存。这代码也实现了自测,也过了QA的测试,为什么会突然爆内存呢?

3. 问题

容器爆内存的问题出现了,但并不是所有的数据都爆内存,有一组数据100%会爆内存,它们的数据类似:

go 复制代码
{"a":"aaa","b":"bbb","1000000":{"key":"value"},"50000000":{"key":"value"}}

比较明显的是出现了数字字符串的key,然后结合代码看了一下,刚开始也没看出啥异常,觉得这个修改后的数据应该是:

go 复制代码
{"a":"aaa","b":"bbb","number_key_data":{"1000000":{"key":"value"},"50000000":{"key":"value"}}}

但后面发现爆内存的问题,又想起了设置数组的方式,当前的代码逻辑如果遇到数字key,就会被认为是在设置数组,开辟几百万甚至上千万长度的数组?(细思极恐) 于是就发现了爆内存的问题所在:数字key在未经特殊标识的情况下,会被认定为数组,于是这个设置key的过程,就变成了对一个key的长为1000000的数组设置值(后者是50000000),可怕

4. 解决方法

于是参看源码,照着sjson的set方法一路向下看,可以发现如果在parsePath 中我们对路径添加了: 的前缀,sjson会强制把这个key当做string key,而在atoui中不会将其解析为一个具体的数字,进而导致对字符串key的设置,变成对数组的设值。

go 复制代码
func parsePath(path string) (res pathResult, simple bool) {
	var r pathResult
	if len(path) > 0 && path[0] == ':' { // 如果含有:符号,这个key会被强制认定为key
		r.force = true
		path = path[1:]
	}
	for i := 0; i < len(path); i++ { // 对path进行分解
		if path[i] == '.' {
			r.part = path[:i]
			r.gpart = path[:i]
			r.path = path[i+1:]
			r.more = true
			return r, true
		}
		if !isSimpleChar(path[i]) {
			return r, false
		}
		if path[i] == '\\' {
			// go into escape mode. this is a slower path that
			// strips off the escape character from the part.
			// ...
		}
	return r, true
}

// atoui does a rip conversion of string -> unigned int.
func atoui(r pathResult) (n int, ok bool) {
	if r.force {
		return 0, false
	}
	for i := 0; i < len(r.part); i++ {
		if r.part[i] < '0' || r.part[i] > '9' {
			return 0, false
		}
		n = n*10 + int(r.part[i]-'0')
	}
	return n, true
}

于是修改代码逻辑,将所有key的前缀都加上:的标识。

go 复制代码
func adaptNumberKey(data string, dataMap map[string]interface{}) (string, error) {
	var err error
	for k, v := range dataMap {
		if _, ok := specificKeys[k]; ok {
			continue
		}
		data, err = sjson.Delete(data, k) // remove first
		if err != nil {
			return "", fmt.Errorf("delete key data error, key=%s, err=%w", k, err)
		}
		data, err = sjson.Set(data, "number_key_data."+":"+k, v)
		if err != nil {
			// log...
			return "", fmt.Errorf("set number_key_data error, key=%s, err=%w", k, err)
		}
	}
	return data, nil
}
// Output: {"a":"aaa","b":"bbb","number_key_data":{"1000000":{"key":"value"},"50000000":{"key":"value"}}} <nil>

5. 小结

忽然想到遇到的这个小问题,当时就觉得还是自己单测的场景不够全面,导致了这次爆内存的问题发生,还好有临时解决方案,不然对线上服务造成的影响还真不小。通过这个事例,再一次告诫自己在后续的代码编写中,对于通用功能的逻辑代码,要尽可能的思考一些边缘case,从而避免在上线后边缘case导致代码崩溃的现象出现。

相关推荐
2401_85760095几秒前
SpringBoot框架的企业资产管理自动化
spring boot·后端·自动化
NiNg_1_2344 小时前
SpringBoot整合SpringSecurity实现密码加密解密、登录认证退出功能
java·spring boot·后端
Chrikk5 小时前
Go-性能调优实战案例
开发语言·后端·golang
幼儿园老大*6 小时前
Go的环境搭建以及GoLand安装教程
开发语言·经验分享·后端·golang·go
canyuemanyue6 小时前
go语言连续监控事件并回调处理
开发语言·后端·golang
杜杜的man6 小时前
【go从零单排】go语言中的指针
开发语言·后端·golang
customer087 小时前
【开源免费】基于SpringBoot+Vue.JS周边产品销售网站(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·java-ee·开源
Yaml48 小时前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理