[ibd2sql] MYSQL JSON数据类型在磁盘上的存储结构并使用py3去解析 (修改时间:2024.01.05)

2024-01-05 08:52:23 浏览数 (1)

mysql支持json格式的数据类型, json格式的数据binary为mysql层实现的, 所以字节序是小端. 解析的时候要注意下

innodb存储它的时候是当作big类型来处理的, 所以innodb只要读出该二进制数据即可, 剩下的就交给Mysql我们来处理.

JSON磁盘上的格式

mysql官方还是介绍得很详细的. 文件: sql/json_binary.h

说明: 下面出现的2/4字节 表示: 如果是小small object/array 就使用2字节, large object/array就使用4字节

第一部分, 使用字节表示 数据类型

代码语言:c复制
  type ::=
      0x00 |       // small JSON object
      0x01 |       // large JSON object
      0x02 |       // small JSON array
      0x03 |       // large JSON array
      0x04 |       // literal (true/false/null) 1:true  2:false  0:null
      0x05 |       // int16
      0x06 |       // uint16
      0x07 |       // int32
      0x08 |       // uint32
      0x09 |       // int64
      0x0a |       // uint64
      0x0b |       // double
      0x0c |       // utf8mb4 string
      0x0f         // custom data (any MySQL data type)

简化下就是这样的

代码语言:c复制
  value ::=
      object  |
      array   |
      literal |
      number  |
      string  |
      custom-data

第二部分 使用2/4字节表示 元素数量

第三部分 使用2/4字节 表示这个json obj/arr大小(不含json类型所使用的1字节)

第三部分 是key-entry 和 vaue-entry (array不需要key-entry)

第四部分 是value 可以是上面的那些数据类型, 也就是还可以是json obj/arr 如果是json obj/arr 就继续第二步到第四部 (递归), 如果是utf8mb4字符串的话, 会有一字节记录数据大小(毕竟value_entry只记录了偏移量....)

画图的话就是这样

代码语言:py复制
                                                               - -----------------
                                                              | JSON OBJECT/ARRAY |
                                                               - -----------------
                                                                      |
 -------------------------------------------------------------------------
| TYPE | ELEMENT_COUNT | KEY-ENTRY(if object) | VALUE-ENTRY | KEY | VALUE |
 -------------------------------------------------------------------------
                               |                    |          |
                               |                    |     --------------
                   --------------------------       |    | UTF8MB4 DATA |
                  | KEY-OFFSET |  KEY-LENGTH |      |     --------------
                   --------------------------       |
                                                    |
                                         --------------------------------
                                         | TYPE | OFFSET/VALUE(if small) |
                                         --------------------------------



OBJECT VALUE = KEY_ENTRY   VALUE_ENTRY   KEY   VALUE  #KEY肯定是字符串, 所以不需要记录数据类型
ARRAY  VALUE = VALUE_ENTRY   VALUE #不需要KEY

KEY_ENTRY   = KEY_OFFSET(2/4bytes)   KEY_LNGTH(2 bytes)
VALUE_ENTRY = TYPE(1byte)   OFFSET(2/4 bytes)/VALUE  (如果类型是int,literal之类的,就直接是值了, 否则就走OFFSET)

困难点

解析为dict对象很简单, 但有true,false,null的时候, 拼接为sql就有点难度了....

正当我准备人工拼接字符串的时候, 我想起了还有json包, 可以直接使用json.dumps 来做(........)

使用脚本解析

从ibd文件解析出json对象的过程这里就省略了. (感兴趣的可以看下之前讲解的ibd文件解析)

就假设你已经解析出来如下json原始数据了

代码语言:python代码运行次数:0复制
(venv) 14:34:40 [root@ddcw21 ibd2sql_v1.0]#python test.py /data/mysql_3314/mysqldata/ibd2sql/t20240102_js.ibd 
REPLACE INTO `db2`.`t20240102_js`(`id`,`name`,`aa`) VALUES (1, 'aa', 'x00x01x00rx00x0bx00x02x00x05{x00t1');
REPLACE INTO `db2`.`t20240102_js`(`id`,`name`,`aa`) VALUES (2, 'aa', 'x00x01x00,x00x0bx00x02x00x0crx00t1x1eAAAAAAAAAAAAAAAAACBBBBBBBBBBBB');
REPLACE INTO `db2`.`t20240102_js`(`id`,`name`,`aa`) VALUES (3, 'aa', 'x00x02x00)x00x12x00x02x00x14x00x02x00x00x16x00x0c&x00a1a2x01x00x10x00x0bx00x02x00x0crx00b1x02b1x02a6');
REPLACE INTO `db2`.`t20240102_js`(`id`,`name`,`aa`) VALUES (4, 'bb', 'x00x01x00x11x00x0bx00x02x00x0crx00t1x03ABC');
REPLACE INTO `db2`.`t20240102_js`(`id`,`name`,`aa`) VALUES (5, 'bb', 'x00x01x00rx00x0bx00x02x00x04x02x00t1');
REPLACE INTO `db2`.`t20240102_js`(`id`,`name`,`aa`) VALUES (6, 'bb', 'x02x02x00*x00x00nx00x00x1ax00x01x00x10x00x0bx00x02x00x0crx00AAx02AAx01x00x10x00x0bx00x02x00x0crx00BBx02BB');
REPLACE INTO `db2`.`t20240102_js`(`id`,`name`,`aa`) VALUES (7, 'bb', "x02x02x007x00x00nx00x00'x00x01x00x1dx00x0bx00x02x00x00rx00AAx01x00x10x00x0bx00x02x00x0crx00CCx02CCx01x00x10x00x0bx00x02x00x0crx00BBx02BB");
REPLACE INTO `db2`.`t20240102_js`(`id`,`name`,`aa`) VALUES (8, 'bb', 'x02x02x00Dx00x00nx00x004x00x01x00*x00x0bx00x02x00x00rx00AAx01x00x1dx00x0bx00x02x00x00rx00CCx01x00x10x00x0bx00x02x00x0crx00DDx02DDx01x00x10x00x0bx00x02x00x0crx00BBx02BB');
REPLACE INTO `db2`.`t20240102_js`(`id`,`name`,`aa`) VALUES (9, 'bb', 'x02x03x00Tx00x00rx00x007x00x00Gx00x01x00*x00x0bx00x02x00x00rx00AAx01x00x1dx00x0bx00x02x00x00rx00CCx01x00x10x00x0bx00x02x00x0crx00DDx02DDx01x00x10x00x0bx00x02x00x0crx00BBx02BBx01x00rx00x0bx00x02x00x05x02x00FF');
REPLACE INTO `db2`.`t20240102_js`(`id`,`name`,`aa`) VALUES (10, 'bb', 'x02x03x00Tx00x00rx00x007x00x00Gx00x01x00*x00x0bx00x02x00x00rx0013x01x00x1dx00x0bx00x02x00x00rx00CCx01x00x10x00x0bx00x02x00x0crx00DDx02DDx01x00x10x00x0bx00x02x00x0crx00BBx02BBx01x00rx00x0bx00x02x00x05x02x00FF');
REPLACE INTO `db2`.`t20240102_js`(`id`,`name`,`aa`) VALUES (11, 'bb', 'x00x03x00x1fx00x19x00x02x00x1bx00x02x00x1dx00x02x00x04x01x00x04x02x00x04x00x00aabbcc');
REPLACE INTO `db2`.`t20240102_js`(`id`,`name`,`aa`) VALUES (12, 'cc', 'x00x03x00x1fx00x19x00x02x00x1bx00x02x00x1dx00x02x00x05x01x00x05x02x00x05x00x00aabbcc');
REPLACE INTO `db2`.`t20240102_js`(`id`,`name`,`aa`) VALUES (13, 'cc', 'x02x01x00x14x00x00x07x00x01x00rx00x0bx00x02x00x05x01x00aa');

好我们现在使用 jsonob去解析该二进制对象为dict/list

代码语言:python代码运行次数:0复制
>>> from ibd2sql.mysql_json import jsonob
>>> data = b'x02x03x00Tx00x00rx00x007x00x00Gx00x01x00*x00x0bx00x02x00x00rx0013x01x00x1dx00x0bx00x02x00x00rx00CCx01x00x10x00x0bx00x02x00x0crx00DDx02DDx01x00x10x00x0bx00x02x00x0crx00BBx02BBx01x00rx00x0bx00x02x00x05x02x00FF'
>>> 
>>> jsonob(data[1:],int.from_bytes(data[:1],'little'))
<ibd2sql.mysql_json.jsonob object at 0x7f3ef7509dd8>
>>> jsonob(data[1:],int.from_bytes(data[:1],'little')).init()
[{'13': {'CC': {'DD': 'DD'}}}, {'BB': 'BB'}, {'FF': 2}]
>>> 

注意该class 第一个参数为数据, 第二个参数为类型(int)

然后把该class 引入到我们的ibd2sql脚本中. 就能得到如下的sql了

代码语言:shell复制
(venv) 14:55:09 [root@ddcw21 ibd2sql_v1.0]#python test.py /data/mysql_3314/mysqldata/ibd2sql/t20240102_js.ibd 
REPLACE INTO `db2`.`t20240102_js`(`id`,`name`,`aa`) VALUES (1, 'aa', '{"t1": 123}');
REPLACE INTO `db2`.`t20240102_js`(`id`,`name`,`aa`) VALUES (2, 'aa', '{"t1": "AAAAAAAAAAAAAAAAACBBBBBBBBBBBB"}');
REPLACE INTO `db2`.`t20240102_js`(`id`,`name`,`aa`) VALUES (3, 'aa', '{"a1": {"b1": "b1"}, "a2": "a6"}');
REPLACE INTO `db2`.`t20240102_js`(`id`,`name`,`aa`) VALUES (4, 'bb', '{"t1": "ABC"}');
REPLACE INTO `db2`.`t20240102_js`(`id`,`name`,`aa`) VALUES (5, 'bb', '{"t1": false}');
REPLACE INTO `db2`.`t20240102_js`(`id`,`name`,`aa`) VALUES (6, 'bb', '[{"AA": "AA"}, {"BB": "BB"}]');
REPLACE INTO `db2`.`t20240102_js`(`id`,`name`,`aa`) VALUES (7, 'bb', '[{"AA": {"CC": "CC"}}, {"BB": "BB"}]');
REPLACE INTO `db2`.`t20240102_js`(`id`,`name`,`aa`) VALUES (8, 'bb', '[{"AA": {"CC": {"DD": "DD"}}}, {"BB": "BB"}]');
REPLACE INTO `db2`.`t20240102_js`(`id`,`name`,`aa`) VALUES (9, 'bb', '[{"AA": {"CC": {"DD": "DD"}}}, {"BB": "BB"}, {"FF": 2}]');
REPLACE INTO `db2`.`t20240102_js`(`id`,`name`,`aa`) VALUES (10, 'bb', '[{"13": {"CC": {"DD": "DD"}}}, {"BB": "BB"}, {"FF": 2}]');
REPLACE INTO `db2`.`t20240102_js`(`id`,`name`,`aa`) VALUES (11, 'bb', '{"aa": true, "bb": false, "cc": null}');
REPLACE INTO `db2`.`t20240102_js`(`id`,`name`,`aa`) VALUES (12, 'cc', '{"aa": 1, "bb": 2, "cc": 0}');
REPLACE INTO `db2`.`t20240102_js`(`id`,`name`,`aa`) VALUES (13, 'cc', '[{"aa": 1}]');
REPLACE INTO `db2`.`t20240102_js`(`id`,`name`,`aa`) VALUES (14, 'cc', '[{"13": {"CC": {"DD": "DD"}}}, {"BB": "BB"}, {"FF": 2}]');
REPLACE INTO `db2`.`t20240102_js`(`id`,`name`,`aa`) VALUES (15, 'cc', '{"a1": {"b1": "b1"}, "a2": "a6"}');

我这里做了schma替换, 所以要验证的话, 得再建一张表.

代码语言:sql复制
Query OK, 1 row affected (0.00 sec)

(root@127.0.0.1) [(none)]> checksum table db2.t20240102_js, ibd2sql.t20240102_js;
 ---------------------- ------------ 
| Table                | Checksum   |
 ---------------------- ------------ 
| db2.t20240102_js     | 3802174698 |
| ibd2sql.t20240102_js | 3802174698 |
 ---------------------- ------------ 
2 rows in set (0.00 sec)

(root@127.0.0.1) [(none)]> 

总结

1. mysql的json是mysql层实现的, 所以字节序是小端 (innodb的基本上都是大端)

2. mysql插入json数据的时候是严格要求json格式的, 即外面是单引号, 里面是双引号. 而且布尔类型和null都是小写.

3. mysql的json类型是标准json类型, 所以使用json包处理的数据是可以直接写入mysql数据库的

参考: mysql源码 sql/json_binary.h 和 RFC7159

注; ibd2sql 1.0 版本 1月底发布, 新增支持debug功能,分区表,json等 和 当前的v0.3版本有丢丢(指尖宇宙.jpg)区别

mysql变长类型读取

mysql的变长类型读取和innodb的变长类型记录有区别(innodb是256*256=64K)

当第一字节 小于128字节时, 就使用1字节存储大小, 直接 读那1字节即可

当第一字节 大于等于 128时候 (即第一bit为1) 就使用2字节表示. 此时 数据为 第一字节 减去 128 (去掉第一bit) 然后加上第二字节*128 即为最终大小.

代码语言:python代码运行次数:0复制
-----------------------------------------------------
| 1 bit flag | 7 bit data | if flag, 8 bit data*128 |
-----------------------------------------------------

相关解析代码可以参考如下:

代码语言:python代码运行次数:0复制
	def read_var(self,offset):
		_s = int.from_bytes(self.bdata[offset:offset 1],'little')
		size = 1
		if _s & (1<<7):
			size  = 1
			_s = self.bdata[offset:offset 2]
			_t = int.from_bytes(_s[1:2],'little')*128   int.from_bytes(_s[:1],'little')-128
		else:
			_t = _s
			
		return size,_t

附脚本

很多注释没有删, 调试的时候 有个t 写成了selt.t 导致满天的print 去找问题.... 后面也懒得删了. 方便后面接入debug

代码语言:python代码运行次数:0复制
#@mysql sql/json_binary.h
import struct
import sys

_ = """
                                                               - -----------------
                                                              | JSON OBJECT/ARRAY |
                                                               - -----------------
                                                                      |
 -------------------------------------------------------------------------
| TYPE | ELEMENT_COUNT | KEY-ENTRY(if object) | VALUE-ENTRY | KEY | VALUE |
 -------------------------------------------------------------------------
                               |                    |          |
                               |                    |     --------------
                   --------------------------       |    | UTF8MB4 DATA |
                  | KEY-OFFSET |  KEY-LENGTH |      |     --------------
                   --------------------------       |
                                                    |
                                         --------------------------------
                                         | TYPE | OFFSET/VALUE(if small) |
                                         --------------------------------

small 2 bytes   large 4 bytes
---------------------------------------------------
TYPE          1 byte
COUNT         2/4 bytes
SIZE          2/4 bytes
VALUE         VALUE/OBJECT/ARRAY
---------------------------------------------------

---------------------------------------------------
OBJECT VALUE = KEY_ENTRY   VALUE_ENTRY   KEY   VALUE  #KEY肯定是字符串, 所以不需要记录数据类型
ARRAY  VALUE = VALUE_ENTRY   VALUE #不需要KEY

KEY_ENTRY   = KEY_OFFSET(2/4bytes)   KEY_LNGTH(2 bytes)
VALUE_ENTRY = TYPE(1byte)   OFFSET(2/4 bytes)/VALUE  (如果类型是int,literal之类的,就直接是值了, 否则就走OFFSET)
---------------------------------------------------

"""

#  type ::=
#      0x00 |       // small JSON object
#      0x01 |       // large JSON object
#      0x02 |       // small JSON array
#      0x03 |       // large JSON array
#      0x04 |       // literal (true/false/null)
#      0x05 |       // int16
#      0x06 |       // uint16
#      0x07 |       // int32
#      0x08 |       // uint32
#      0x09 |       // int64
#      0x0a |       // uint64
#      0x0b |       // double
#      0x0c |       // utf8mb4 string
#      0x0f         // custom data (any MySQL data type)


#  value ::=
#      object  |
#      array   |
#      literal |
#      number  |
#      string  |
#      custom-data

class jsonob(object):
	def __init__(self,bdata,t):
		"""
		bdata = json data
		t 类型 json类型
		"""
		self.bdata = bdata
		self.t = t
		self.offset = 0
		self.ssize = 2 if self.t == 0x00 or self.t == 0x02 else 4
		self._type = None
		self._bdata = b''
		#print("BEGIN JSON TO B, CURRENT TYPE:",self.t)

	def read_key_entry(self):
		"""
		read key-entry
		"""
		#print("READ KEY ENTRY")
		key_entry = []
		for x in range(self.element_count):
			key_offset = self.read_little()
			key_length = self.read_little(2)
			key_entry.append((key_offset,key_length))
		self.key_entry = key_entry

	def read_value_entry(self):
		#print("READ VALUE ENTRY")
		value_entry = []
		for x in range(self.element_count):
			t = self.read_little(1)
			#print("t entry: type:",t)
			data = None
			if t < 0x04:
				#print("READ VALUE ENTRY JSON object/array")
				data  = self.read_little()
			elif t == 0x04: #literal
				#print("READ VALUE ENTRY literal")
				_data = self.read_little()
				if _data == 1:
					data = True
				elif _data == 2:
					data = False
				elif _data == 0:
					data = None
				else:
					data = ''
			elif t >= 0x05 and t <= 0x0a: #inline data
				#print("READ VALUE ENTRY Inline data for INT",t,0x05,0x0a)
				data = self.read_inline_data(t)
			elif t == 0x0b: #double
				#print("READ VALUE ENTRY Double")
				#data = struct.unpack('d',self.read(8))[0]
				data = self.read_little()
			elif t == 0x0c: #string
				#print("READ DATA ENTRY STRING",self.offset)
				data = self.read_little() #OFFSET
			value_entry.append((t,data))
		self.value_entry = value_entry
		#print("VALUE ENTRY LIST ---------",self.value_entry)

	def read_key(self):
		#print("READ KEY")
		key = []
		for x in self.key_entry:
			key.append(self.bdata[x[0]:x[0] x[1]].decode()  )
		self.key = key

	def read_value(self):
		#print("READ VALUE")
		value = []
		for x in self.value_entry:
			#print("VALUE TYPE:xxxxxxx",x[0])
			if x[0] == 0x0c: #字符串
				_s,size = self.read_var(x[1])
				#size = int.from_bytes(self.bdata[x[1]:x[1] 1],'little') #先都按1字节计算
				value.append(self.bdata[x[1] _s:x[1] _s size].decode()) 
			elif x[0] == 0x0b:
				value.append(struct.unpack('d',self.bdata[x[1]:x[1] 8])[0])
			elif x[0] <= 0x03: #json对象, 又递归
				s = self.ssize
				size = int.from_bytes(self.bdata[x[1] s: x[1] s s ], 'little')
				data = self.bdata[x[1]:x[1] size]
				_aa = jsonob(data,x[0])
				value.append(_aa.init())
			else:
				value.append(x[1])
		self.value = value
				
	def read_var(self,offset):
		"""
		读mysql的varchar的 记录长度的大小, 范围字节数量和大小
		如果第一bit是1 就表示要使用2字节表示:
			后面1字节表示 使用有多少个128字节, 然后加上前面1字节(除了第一bit)的数据(0-127) 就是最终数据
-----------------------------------------------------
| 1 bit flag | 7 bit data | if flag, 8 bit data*128 |
-----------------------------------------------------
		"""
		_s = int.from_bytes(self.bdata[offset:offset 1],'little')
		size = 1
		if _s & (1<<7):
			size  = 1
			_s = self.bdata[offset:offset 2]
			_t = int.from_bytes(_s[1:2],'little')*128   int.from_bytes(_s[:1],'little')-128
		else:
			_t = _s
			
		return size,_t


	def init(self,):
		#print(self.bdata)
		self.element_count = self.read_little()
		#print("ELEMENT COUNT:",self.element_count)
		#print(self.read_little())
		self._size = self.read_little()
		#print(f"THIS OBJECT SIZE:",self._size, "ACTUAL SIZE:",len(self.bdata))
		if self._size != len(self.bdata):
			return None
		#print("WILL INIT")
		if self.t == 0x00 or self.t == 0x01: #object
			self._type = "JSON Object"
			#print(f"THIS TYPE IS {self._type}")
			self.data = {}
			self.read_key_entry()
			self.read_value_entry()
			self.read_key()
			self.read_value()
			self.data = {k:v for k,v in zip(self.key,self.value)}

		elif self.t == 0x02 or self.t == 0x03: #array
			self._type = "JSON Array"
			#print(f"THIS TYPE IS {self._type}")
			self.data = []
			self.read_value_entry()
			self.read_value()
			self.data = self.value
		return self.data
			

	def read_little(self,ssize=None):
		ssize = self.ssize if ssize is None else ssize
		s = int.from_bytes(self.read(ssize),'little')
		#print(f"READ LITTLE SIZE: {ssize} bytes  bdata:{self._bdata} value:{s} ")
		return s

	def read(self,n):
		_t = self.bdata[self.offset:self.offset n]
		self.offset  = n
		self._bdata = _t
		return _t

	def _read_int(self,n):
		data = self.read(n)
		return int.from_bytes(data,'big')

	def read_uint(self,n,is_unsigned=True):
		_t = self._read_int(n)
		_s = n*8 - 1
		#print("read uint",self._bdata,_t,_s)
		return (_t&((1<<_s)-1))-2**_s if _t < 2**_s and not is_unsigned else (_t&((1<<_s)-1))

	def read_int(self,n):
		return self.read_uint(n,False)

	def read_inline_data(self,t):
		n = 0
		is_unsigned = True
		#print("tread_inline_data TYPE:",t)
		if t == 0x05: #int16
			n = 2
		elif t == 0x06: #uint16
			n = 2
			is_unsigned = True
		elif t == 0x07: #int32
			n = 4
		elif t == 0x08: #uint32
			n = 4
			is_unsigned = True
		elif t == 0x09: #int64
			n = 8
		elif t == 0x0a: #uint64
			n = 8
			is_unsigned = True
		#return self.read_uint(n,is_unsigned)
		signed = False if is_unsigned else True
		rs = int.from_bytes(self.read(n),'little',signed=signed)
		#print("tINLINE DATA:",rs)
		return rs

0 人点赞