likes
comments
collection
share

JSON解析-如何处理的又快又好?

作者站长头像
站长
· 阅读数 74

“我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第1篇文章,点击查看活动详情

在考虑MQ消费模式的优化方案期间,发现有很多关于JSON解析的相关操作,针对存在json解析的场景写了篇小小滴看法~

概述:原生json库的分析、使用事项及业务场景优化方向

包含对JSON 库 Unmarshal、sync 库 pool 的剖析

版本: go 1.18.3

How to do it well?

Golang官方自带一个 encoding/json 库,可以用于JSON的加密与解析,其中Marshal、Unmarshal也是最常使用的两个方法,那么如何让解析做得更好?

Unmarshal的解析步骤

下图用于阐述Unmarshal的解析过程:

暂时无法在飞书文档外展示此内容

精度丢失的坑

这是 json 与 go 对应的数据类型

GoJSON
bool对应JSON布尔类型
float64对应JSON数字类型
string对应JSON字符串类型
[]interface{}对应JSON数组
map[string]interface{}对应JSON对象
nil对应JSON的null

json的规范中,对于数字类型是不区分整形和浮点型的,所以go采用float64来映射,但有个隐患,如果超出float64的安全整数范围,那么就会精度丢失。

当使用 interface{} 接收整数,再次 Marshal 需要注意精度丢失的问题。

  • float64最大的安全整数是52位尾数全为1且指数部分为最小 0x001F FFFF FFFF FFFF
  • float64可以存储的最大整数是52位尾数全位1且指数部分为最大 0x07FEF FFFF FFFF FFFF
  • 十进制有效数字在16位(max = 9007199254740991),超过就很可能精度丢失

解决办法:

  1. 明确使用int接收,不使用interface{}
  1. 使用decoder的方式,设置useNumber=true,解析大数
decoder := json.NewDecoder(bytes.NewReader(request))
decoder.UseNumber() // set useNumber = true
err := decoder.Decode(&req)

设置了UseNumber后,会将数字当作json.Number类型处理,底层是string。

// A Number represents a JSON number literal.
type Number string
// String returns the literal text of the number.
func (n Number) String() string { return string(n) }
// Float64 returns the number as a float64.
func (n Number) Float64() (float64, error) {
   return strconv.ParseFloat(string(n), 64)
}
// json.newTypeEcnode -- 记录对应类型的序列化方法

使用tag来实现额外的需求(在指定名称后添加 , 与 参数):

  • omitempty:json:"name,omitempty" 可以在数值为0或空的时候忽略
  • string/number/boolean:json:"name,string" 可以制定序列化的时候类型转为string,除此以外还可以选择 number、boolean

小结

综上可得,我们使用接受者要注意以下几点:

  1. 接受者需要是指针类型
  1. 如果明确了类型,使用struct & 设置json标签为“-”,有助于过滤无需字段,同时相比map可以确定字段类型,减少判断类型的步骤,效果更好
  1. 默认使用的decodeStateuseNumber为false,且接受者用的类型是interface{},有精度丢失的情况(当超出float64的安全整数范围时)
  1. decoder 与 Unmarshal选择:
  • json.Decoder适合io.Reader流,或者需要从数据流中解码多个值
  • json.Unmarshal适合内存中已有 JSON 数据
  1. 通过编写MarshalJSON()([]byte, error)UnmarshalJSON(b []byte) error 来实现自定义JSON序列化和反序列化
  1. 不要频繁的序列化和反序列化,尽量一次解决
  1. 大量的递归和反射影响性能,可以优化(类似protobuf生成代码解析)

How to get faster?

大多情况下,反序列化对象的使用过程可以类比做下面的漫画(拿出小学毕生功力所绘~🐶):

(一群小朋友吃完就把饭盒扔了,这样倒垃圾的次数就很多了)

JSON解析-如何处理的又快又好?

(如果能把饭盒重复利用下,那就完美了,记得把餐盘洗干净)

JSON解析-如何处理的又快又好?

短时间反序列化大量对象会对GC带来很大压力,我们可以使用sync.pool 来优化这一现象,实现临时对象的复用,像gin框架就是复用了context

sync.pool介绍

type Pool struct {  
   noCopy noCopy  // A Pool must not be copied after first use.
   // 实际指向 []poolLocal,数组大小等于 Process 的数量    local     unsafe.Pointer 
   localSize uintptr    
   // GC 时,victim 和 victimSize 会分别接管 local 和 localSize
   // 延缓一次GC
   victim     unsafe.Pointer 
   victimSize uintptr       
   New func() interface{} // 需要实现的创建对象的方法
}

sync.Pool 是以缓存池的形式,实现对象复用,减少 GC 次数

(大致内部结构如下图)

JSON解析-如何处理的又快又好?

  • poolDequeue 是一个环形队列 ,使用位运算区分 head 和 tail 的值,采取 atomic 和 CAS 对值进行操作,使用了原子操作来替代锁机制。
  • 生产者可以从 head 写/插入、从 head 读/删除。
  • 消费者仅可从 tail 读/删除。

JSON解析-如何处理的又快又好?

详细学习参考官方文档,这里只是简单说了其用法。


看一下效果:

这是一个小测试就能看出添加了sync.pool在面对大量反序列化时候的优势:

package main

import (
   "encoding/json"
   "sync"
   "testing"
)

type Say struct {
   ID   int               `json:"id"`
   Name string            `json:"name"`
   Dmap map[string]string `json:"dmap"`
}

var pool = sync.Pool{
   New: func() interface{} {
      return &Say{}
   },
}

var j, _ = json.Marshal(
   Say{
      Name: "dag",
      ID:   25,
      Dmap: map[string]string{
         "cool": "hot",
      },
   })

func BenchmarkUnmarshalWithPool(b *testing.B) {
   g := sync.WaitGroup{}
   g.Add(b.N)
   for i := 0; i < b.N; i++ {
      go func() {
         s := pool.Get().(*Say)
         _ = json.Unmarshal(j, s)
         pool.Put(s)
         g.Done()
      }()
   }
   g.Wait()
}

func BenchmarkUnmarshalWithStruct(b *testing.B) {
   g := sync.WaitGroup{}
   g.Add(b.N)
   for i := 0; i < b.N; i++ {
      go func() {
         s := &Say{}
         _ = json.Unmarshal(j, s)
         g.Done()
      }()
   }
   g.Wait()
}

执行 go test -bench . -benchmen

JSON解析-如何处理的又快又好?

Say 结构体内存占用较小,内存分配耗时短,如果是个大对象,则差距会更加明显。

需要注意的是,由于sync.pool存储的是临时对象,所以不能控制被回收的时间,不建议存储连接类型的。如果想要存储,则可以选择借助SetFInalize进行收尾工作。

SetFinalizer 的定义:func SetFinalizer(x, f interface{})

  • 参数 x 必须是一个指向通过 new 申请的对象的指针,或者通过对复合字面值取址得到的指针。
  • 参数 f 必须是一个函数,可以直接用 x 类型值赋值的参数,也可以有任意个被忽略的返回值,比如func funcName(x *X)
  • SetFinalizer 函数可以将 x 的终止器设置为 f,当垃圾收集器发现 x 不能再直接或间接访问时,它会清理 x 并调用 f(x)。

下面这个例子可以看出对象回收前的行为

package main

import (
   "fmt"
   "runtime"
   "testing"
   "time"
)

type Conn struct {
   Name string `json:"name"`
}

func (c *Conn) Start() {
   fmt.Println("start:", c.Name)
}

func (c *Conn) close() {
   fmt.Println("close:", c.Name)
}

func closeConn(c *Conn) {
   c.close()
}

func InitConn(name string) *Conn {
   c := &Conn{Name: name}
   runtime.SetFinalizer(c, closeConn)
   return c
}

func TestConn(t *testing.T) {
   c := InitConn("hello")
   c.Start()
   time.Sleep(time.Second)
   runtime.GC() // 手动GC
}

结果:

=== RUN   TestConn
start: hello
close: hello
--- PASS: TestConn (1.01s)

使用建议

  1. 取出使用的对象要先进行 resetPut回去的对象要确定后续逻辑不再使用到
  1. 设计的new方法要保证并发安全
  1. 建议不要存放链接等对象,防止协程泄漏,如果要的话,请设置收尾工作

总结

  1. Unmarshal的接受体需要传指针,变量是可导出的(即首字母大写)
  1. 不要频繁的序列化和反序列化,尽量一次解决
  1. 了解Json与Go的数据类型映射关系,避免踩坑大数的情况
  1. 根据情况选择 decoder 与 Unmarshal,如果明确了类型,使用struct & 设置json标签为“-”,有助于过滤无需字段,同时相比map可以确定字段类型,减少判断类型的步骤,效果更好
  1. 巧用sync.pool让对象实现复用,减少GC次数,需遵循使用建议

第三方JSON库拓展

从一些第三方库能学习到对原有JSON库的优化方向,

  • 通过声明IDL,预先生成序列化、反序列化代码,但是对业务侵入性强:

  • 分配优化(避免复制、切片重用)、流式拉取(避免一次读入大数组)、特殊处理字符串

转载自:https://juejin.cn/post/7139082554304364581
评论
请登录