2024年2月

一:nacos

https://nacos.io/docs/latest/what-is-nacos/

https://github.com/alibaba/nacos

二:consul

https://developer.hashicorp.com/consul/docs?product_intent=consul

https://github.com/hashicorp/consul

服务发现的框架常用的还有zookeeper eureka等,这里准备使用nacos

前置条件准备 docker,yaml


version: "3.8"networks:
caseor_bridge:
driver: bridge
ipam:
config:
- subnet: 172.0.10.0/24services:

mysql:
container_name: mysql
image: mysql
privileged:
truecommand:--character-set-server=utf8mb4 --collation-server=utf8mb4_general_ci --max_connections=2000 --max_allowed_packet=64M
environment:
- TZ=Asia/Shanghai- MYSQL_ROOT_PASSWORD=123456volumes:- ./mysql:/var/lib/mysql
ports:
- "3306:3306"healthcheck:
test: [
"CMD", "mysqladmin" ,"ping", "-h", "localhost"]
interval: 5s
timeout: 10s
retries:
10networks:
caseor_bridge:
ipv4_address:
172.0.10.3redis:
image: redis
container_name:
"redis"ports:- "6379:6379"volumes:- ./redis/data:/data- ./redis/conf:/usr/local/etc/redis
networks:
caseor_bridge:
ipv4_address:
172.0.10.4nacos1:
container_name: nacos1
hostname: nacos1
image: nacos
/nacos-server
environment:
- MODE=cluster- PREFER_HOST_MODE=hostname- NACOS_SERVERS=nacos1:8848 nacos2:8848 nacos3:8848 - SPRING_DATASOURCE_PLATFORM=mysql- MYSQL_SERVICE_HOST=172.0.10.3 - MYSQL_SERVICE_PORT=3306 - MYSQL_SERVICE_USER=root- MYSQL_SERVICE_PASSWORD=123456 - MYSQL_SERVICE_DB_NAME=nacos- MYSQL_SERVICE_DB_PARAM=characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true - JVM_XMS=128m- JVM_XMX=128m- JVM_XMN=128m
volumes:
- ./nacos/cluster-logs/nacos1:/home/nacos/logs- ./nacos/init.d:/home/nacos/init.d
ports:
- 8850:8848 - 7850:7848 - 9870:9848 - 9852:9849depends_on:-mysql
networks:
caseor_bridge:
ipv4_address:
172.0.10.5nacos2:
container_name: nacos2
hostname: nacos2
image: nacos
/nacos-server
environment:
- MODE=cluster- PREFER_HOST_MODE=hostname- NACOS_SERVERS=nacos1:8848 nacos2:8848 nacos3:8848 - SPRING_DATASOURCE_PLATFORM=mysql- MYSQL_SERVICE_HOST=172.0.10.3 - MYSQL_SERVICE_PORT=3306 - MYSQL_SERVICE_USER=root- MYSQL_SERVICE_PASSWORD=123456 - MYSQL_SERVICE_DB_NAME=nacos- MYSQL_SERVICE_DB_PARAM=characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true - JVM_XMS=128m- JVM_XMX=128m- JVM_XMN=128m
volumes:
- ./nacos/cluster-logs/nacos2:/home/nacos/logs- ./nacos/init.d:/home/nacos/init.d
ports:
- 8849:8848 - 7849:7848 - 9869:9848 - 9851:9849depends_on:-mysql
networks:
caseor_bridge:
ipv4_address:
172.0.10.6nacos3:
container_name: nacos3
hostname: nacos3
image: nacos
/nacos-server
environment:
- MODE=cluster- PREFER_HOST_MODE=hostname- NACOS_SERVERS=nacos1:8848 nacos2:8848 nacos3:8848 - SPRING_DATASOURCE_PLATFORM=mysql- MYSQL_SERVICE_HOST=172.0.10.3 - MYSQL_SERVICE_PORT=3306 - MYSQL_SERVICE_USER=root- MYSQL_SERVICE_PASSWORD=123456 - MYSQL_SERVICE_DB_NAME=nacos- MYSQL_SERVICE_DB_PARAM=characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true - JVM_XMS=128m- JVM_XMX=128m- JVM_XMN=128m
volumes:
- ./nacos/cluster-logs/nacos3:/home/nacos/logs- ./nacos/init.d:/home/nacos/init.d
ports:
- 8848:8848 - 7848:7848 - 9848:9848 - 9849:9849depends_on:-mysql
networks:
caseor_bridge:
ipv4_address:
172.0.10.7etcd:
container_name: etcd
hostname: etcd
image: bitnami
/etcd
volumes:
- ./etcd/data:/bitnami/etcd
environment:
ETCD_ENABLE_V2:
"true"ALLOW_NONE_AUTHENTICATION:"yes"ETCD_ADVERTISE_CLIENT_URLS:"http://etcd:2379" #https://github.com/apache/apisix-dashboard/issues/2756 需要更换为host域名不能使用0.0.0.0 ETCD_LISTEN_CLIENT_URLS: "http://0.0.0.0:2379"ports:- "2379:2379/tcp"networks:
caseor_bridge:
ipv4_address:
172.0.10.8apisix:
container_name: apisix
hostname: apisix
image: apache
/apisix
volumes:
- ./apisix/log:/usr/local/apisix/logs- ./apisix/conf/config.yaml:/usr/local/apisix/conf/config.yaml:ro
depends_on:
-etcd
ports:
- "9088:9088/tcp" - "9180:9180/tcp" - "127.0.0.1:9090:9090/tcp"networks:
caseor_bridge:
ipv4_address:
172.0.10.9apisix-dashboard:
container_name: apisix
-dashboard
image: apache
/apisix-dashboard
depends_on:
-etcd
ports:
- "9188:9188"volumes:- ./apisix/conf/dashboard.yaml:/usr/local/apisix-dashboard/conf/conf.yaml
networks:
caseor_bridge:
ipv4_address:
172.0.10.10rabbitmq01:
image: rabbitmq
container_name: rabbitmq01
hostname: rabbitmq01
environment:
- TZ=Asia/Shanghai- RABBITMQ_DEFAULT_USER=root #自定义登录账号- RABBITMQ_DEFAULT_PASS=123456#自定义登录密码- RABBITMQ_ERLANG_COOKIE='secret_cookie'ports:- "15672:15672" - "5672:5672"volumes:- ./rabbitmq/mq1/data:/var/lib/rabbitmq- ./rabbitmq/mq1/conf:/etc/rabbitmq
command: bash
-c "sleep 10; rabbitmq-server;"networks:
caseor_bridge:
ipv4_address:
172.0.10.11rabbitmq02:
image: rabbitmq
container_name: rabbitmq02
hostname: rabbitmq02
environment:
- TZ=Asia/Shanghai- RABBITMQ_DEFAULT_USER=root #自定义登录账号- RABBITMQ_DEFAULT_PASS=123456#自定义登录密码- RABBITMQ_ERLANG_COOKIE='secret_cookie'ports:- "15673:15672" - "5673:5672"depends_on:-rabbitmq01
volumes:
- ./rabbitmq/mq2/data:/var/lib/rabbitmq- ./rabbitmq/mq2/conf:/etc/rabbitmq
command: bash
-c "sleep 10; rabbitmq-server;"networks:
caseor_bridge:
ipv4_address:
172.0.10.12rabbitmq03:
image: rabbitmq
container_name: rabbitmq03
hostname: rabbitmq03
environment:
- TZ=Asia/Shanghai- RABBITMQ_DEFAULT_USER=root #自定义登录账号- RABBITMQ_DEFAULT_PASS=123456#自定义登录密码- RABBITMQ_ERLANG_COOKIE='secret_cookie'ports:- "15674:15672" - "5674:5672"depends_on:-rabbitmq01
volumes:
- ./rabbitmq/mq3/data:/var/lib/rabbitmq- ./rabbitmq/mq3/conf:/etc/rabbitmq
command: bash
-c "sleep 10; rabbitmq-server;"networks:
caseor_bridge:
ipv4_address:
172.0.10.13# 开启web管理
# rabbitmq
-plugins enable rabbitmq_management

# # 加入rabbitmq集群

# # rabbit1
# rabbitmqctl stop_app
# rabbitmqctl reset
# rabbitmqctl start_app

# # rabbit2
# rabbitmqctl stop_app
# rabbitmqctl reset
# rabbitmqctl join_cluster
--ram rabbit@rabbit1
# rabbitmqctl start_app

# # rabbit3
# rabbitmqctl stop_app
# rabbitmqctl reset
# rabbitmqctl join_cluster
--ram rabbit@rabbit1
# rabbitmqctl start_app

View Code

使用docker启动mysql


mysql:
container_name: mysql
image: mysql
privileged:
truecommand:--character-set-server=utf8mb4 --collation-server=utf8mb4_general_ci --max_connections=2000 --max_allowed_packet=64M
environment:
- TZ=Asia/Shanghai- MYSQL_ROOT_PASSWORD=123456volumes:- ./mysql:/var/lib/mysql
ports:
- "3306:3306"healthcheck:
test: [
"CMD", "mysqladmin" ,"ping", "-h", "localhost"]
interval: 5s
timeout: 10s
retries:
10networks:
caseor_bridge:
ipv4_address:
172.0.10.3

View Code

在数据库创建nacos数据库

https://github.com/alibaba/nacos/blob/master/config/src/main/resources/META-INF/nacos-db.sql

准备三个nacos

nacos1:
container_name: nacos1
hostname: nacos1
image: nacos
/nacos-server
environment:
- MODE=cluster- PREFER_HOST_MODE=hostname- NACOS_SERVERS=nacos1:8848 nacos2:8848 nacos3:8848 - SPRING_DATASOURCE_PLATFORM=mysql- MYSQL_SERVICE_HOST=172.0.10.3 - MYSQL_SERVICE_PORT=3306 - MYSQL_SERVICE_USER=root- MYSQL_SERVICE_PASSWORD=123456 - MYSQL_SERVICE_DB_NAME=nacos- MYSQL_SERVICE_DB_PARAM=characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true - JVM_XMS=128m- JVM_XMX=128m- JVM_XMN=128m
volumes:
- ./nacos/cluster-logs/nacos1:/home/nacos/logs- ./nacos/init.d:/home/nacos/init.d
ports:
- 8850:8848 - 7850:7848 - 9870:9848 - 9852:9849depends_on:-mysql
networks:
caseor_bridge:
ipv4_address:
172.0.10.5nacos2:
container_name: nacos2
hostname: nacos2
image: nacos
/nacos-server
environment:
- MODE=cluster- PREFER_HOST_MODE=hostname- NACOS_SERVERS=nacos1:8848 nacos2:8848 nacos3:8848 - SPRING_DATASOURCE_PLATFORM=mysql- MYSQL_SERVICE_HOST=172.0.10.3 - MYSQL_SERVICE_PORT=3306 - MYSQL_SERVICE_USER=root- MYSQL_SERVICE_PASSWORD=123456 - MYSQL_SERVICE_DB_NAME=nacos- MYSQL_SERVICE_DB_PARAM=characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true - JVM_XMS=128m- JVM_XMX=128m- JVM_XMN=128m
volumes:
- ./nacos/cluster-logs/nacos2:/home/nacos/logs- ./nacos/init.d:/home/nacos/init.d
ports:
- 8849:8848 - 7849:7848 - 9869:9848 - 9851:9849depends_on:-mysql
networks:
caseor_bridge:
ipv4_address:
172.0.10.6nacos3:
container_name: nacos3
hostname: nacos3
image: nacos
/nacos-server
environment:
- MODE=cluster- PREFER_HOST_MODE=hostname- NACOS_SERVERS=nacos1:8848 nacos2:8848 nacos3:8848 - SPRING_DATASOURCE_PLATFORM=mysql- MYSQL_SERVICE_HOST=172.0.10.3 - MYSQL_SERVICE_PORT=3306 - MYSQL_SERVICE_USER=root- MYSQL_SERVICE_PASSWORD=123456 - MYSQL_SERVICE_DB_NAME=nacos- MYSQL_SERVICE_DB_PARAM=characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true - JVM_XMS=128m- JVM_XMX=128m- JVM_XMN=128m
volumes:
- ./nacos/cluster-logs/nacos3:/home/nacos/logs- ./nacos/init.d:/home/nacos/init.d
ports:
- 8848:8848 - 7848:7848 - 9848:9848 - 9849:9849depends_on:-mysql
networks:
caseor_bridge:
ipv4_address:
172.0.10.7

具体配置可以参考官网

查看nacos 控制台
http://127.0.0.1:8848/nacos/

http://127.0.0.1:8849/nacos/

http://127.0.0.1:8850/nacos/

可以看到集群已经搭建完成了

在.net core中使用nacos

https://github.com/nacos-group/nacos-sdk-csharp

nacos-sdk-csharp.AspNetCore
nacos
-sdk-csharp.Extensions.Configuration

在appsettings.json中新增Nacos配置

  "Nacos": {"ServerAddresses": [ "http://192.168.110.39:8848"],//命名空间GUID,public默认没有
    "Namespace": "779857e5-b517-437c-9026-f04c98d4bac1",//配置中心
    "Listeners": [
{
"Group": "DEFAULT_GROUP","DataId": "appsettings.json","Optional": false}
],
//服务发现 "ServiceName": "saas-system","GroupName": "DEFAULT_GROUP"}

在program下新增

//读取nacos配置文件
builder.Host.UseNacosConfig("Nacos");//注册服务到nacos
builder.Services.AddNacosAspNet(builder.Configuration, "Nacos");

在program下继续读取appsettings的配置信息,首选需要在nacos上的appsettings配置信息

启动该服务

dotnet run --urls=http://*:8083
dotnet run --urls=http://*:8084
dotnet run --urls=http://*:8085

在其他服务中调用当前服务接口

 [HttpGet("nacos.test")]public async Task<IActionResult>TestNacos()
{
var instance= await _namingService.SelectOneHealthyInstance("saas-system", "DEFAULT_GROUP");var host = $"{instance.Ip}:{instance.Port}";var baseUrl = instance.Metadata.TryGetValue("secure", out_)? $"https://{host}": $"http://{host}";var url = $"{baseUrl}/system/tenant.package.query.list";using HttpClient client = new();var result = awaitclient.GetAsync(url);return Ok(awaitresult.Content.ReadAsStringAsync());

}

引言

在实际业务开发中,随着业务的变化,数据的复杂性和多样性不断增加。传统的关系型数据库模型在这种情况下会显得受限,因为它们需要预先定义严格的数据模式,并且通常只能存储具有相同结构的数据。而面对非结构化或半结构化数据的存储和处理需求,选择使用非关系型数据库或者创建子表存储这些变化的结构可能会变得复杂。在这种情况下,我们可以利用
MySQL

JSON
字段类型来解决这个问题。
JSON
字段提供了灵活的数据存储方式,能够轻松应对数据结构的变化和动态性,从而更好地满足业务需求。

MySQL5.7.8
版本引入了JSON数据类型,允许在数据库表中存储和操作符合
JSON
格式的数据。这种原生支持
JSON

JavaScript
对象表示法)的数据类型遵循
RFC 7159
标准,提供了有效存储复杂、半结构化数据的方式。
MySQL8.0
版本全面支持
JSON
数据类型,标志着
MySQL
在处理非结构化数据方面取得了显著进展。除了能够高效存储
JSON
文档外,
MySQL8.0

JSON
数据类型还引入了强大的内置函数,极大地提升了操作和查询
JSON
数据的效率和便捷性。本文我们以
MYSQL 8.0
环境研究一下它的
JSON
类型。

JSON数据类型的特性

  • 二进制存储
    MySQL 8.0采用了一种优化的二进制格式存储JSON数据,相较于传统的字符串存储方式,这种新格式能够更迅速地读取和解析JSON文档。该格式允许服务器通过键或数组索引直接查找子对象或嵌套值,无需事先读取或解析文档中的所有值。这不仅降低了存储空间占用,还提升了查询性能。JSON列存储的JSON文档所需的空间大致与LONGBLOB或LONGTEXT相同。但要注意,存储在JSON列中的JSON文档大小受
    max_allowed_packet
    系统变量的限制。

  • 自动验证
    当插入或更新包含JSON字段的记录时,MySQL会自动验证所插入的内容是否符合JSON格式规范,确保数据的一致性和完整性。

  • 索引支持
    JSON列不直接索引,但可以在JSON字段中特定路径上创建索引,例如通过
    JSON_EXTRACT()
    函数提取出用于索引的值。此外,MySQL优化器还会寻找与JSON表达式匹配的虚拟列上的兼容索引。

  • 部分内容更新
    从MySQL 8.0开始,优化器可以执行JSON列的部分、原地更新,而不是完全删除旧文档并将新文档完全写入列。这种优化可以通过使用
    JSON_SET()

    JSON_REPLACE()

    JSON_REMOVE()
    等函数进行更新。

  • 丰富的JSON函数
    MySQL提供了一组用于操作JSON值的SQL函数,包括创建、操作和搜索。此外,还有一组用于操作GeoJSON值的空间函数。

JSON操作函数

MySQL提供了一组用于操作JSON值的SQL函数,包括创建、操作和搜索。

1、创建JSON值

  • JSON_ARRAY
    用于创建JSON数组。语法格式:
JSON_ARRAY([val[, val] ...])
mysql> SELECT JSON_ARRAY(1, "abc", NULL, TRUE, NOW());
+------------------------------------------------------+
| JSON_ARRAY(1, "abc", NULL, TRUE, NOW())              |
+------------------------------------------------------+
| [1, "abc", null, true, "2024-02-05 03:29:56.000000"] |
+------------------------------------------------------+
1 row in set (0.00 sec)
  • JSON_OBJECT
    用于创建JSON对象。语法格式:
JSON_OBJECT([key, val[, key, val] ...])
 SELECT JSON_OBJECT('name', 'CoderAcademy', 'age', 30) AS person;
+-----------------------------+
| person                      |
+-----------------------------+
| {"age": 30, "name": "CoderAcademy"} |
+-----------------------------+
1 row in set (0.00 sec)

注意,如果传入的不是合法的JSON格式,则会报错。

  • JSON_QUOTE
    用于去除JSON字符串中的引号,将一个JSON格式化的字符串转换为常规的数据库内可直接使用的字符串。当从JSON文档中提取出一个原本被双引号包围的字符串时,此函数会移除这些引号,从而便于后续对提取出的数据进行进一步的SQL操作或者与其他非JSON字段进行比较。
mysql> SELECT JSON_QUOTE('CoderAcademy'), JSON_QUOTE('"CoderAcademy"');
+----------------------------+------------------------------+
| JSON_QUOTE('CoderAcademy') | JSON_QUOTE('"CoderAcademy"') |
+----------------------------+------------------------------+
| "CoderAcademy"             | "\"CoderAcademy\""           |
+----------------------------+------------------------------+
1 row in set (0.00 sec)

mysql> SELECT JSON_QUOTE('[1, 2, 3]') AS json1 ,JSON_QUOTE('["a", "b","c"]')  AS json2, JSON_QUOTE('{"name":"CoderAcademy", "age": 30}')  AS json3;
+-------------+------------------------+--------------------------------------------+
| json1       | json2                  | json3                                      |
+-------------+------------------------+--------------------------------------------+
| "[1, 2, 3]" | "[\"a\", \"b\",\"c\"]" | "{\"name\":\"CoderAcademy\", \"age\": 30}" |
+-------------+------------------------+--------------------------------------------+
1 row in set (0.00 sec)
  • CAST(value AS JSON)
    在MySQL8.0中,CAST(value AS JSON)函数会尝试将给定的值转化为一个有效的JSON格式字符串。语法结构如下:
CAST(value AS JSON)
mysql> SELECT CAST('["apple", "banana", "cherry"]' AS JSON) AS json1, CAST('{"name":"CoderAcademy", "age": 30}' AS JSON ) AS json2;
+-------------------------------+-------------------------------------+
| json1                         | json2                               |
+-------------------------------+-------------------------------------+
| ["apple", "banana", "cherry"] | {"age": 30, "name": "CoderAcademy"} |
+-------------------------------+-------------------------------------+
1 row in set (0.00 sec)

需要注意的是,如果要转换的值不符合JSON格式规范,则会抛出错误。例如:
NULL
、不合法的JSON字符串或其他非转换类型的值。

mysql> SELECT cast('NULL' AS json);
ERROR 3141 (22032): Invalid JSON text in argument 1 to function cast_as_json: "Invalid value." at position 0.

mysql> SELECT CAST('{"name":"CoderAcademy", "age"}' AS JSON );
ERROR 3141 (22032): Invalid JSON text in argument 1 to function cast_as_json: "Missing a colon after a name of object member." at position 29.
  • JSON_TYPE
    用于查询JSON值类型的内置函数。该函数返回一个表示给定JSON值基本类型的字符串。
    语法结构:
JSON_TYPE(value)
  1. "NULL":如果路径指向的值是JSON
    null
  2. "OBJECT":如果路径指向的是一个JSON对象(键值对集合)。
  3. "ARRAY":如果路径指向的是一个JSON数组。
  4. "STRING":如果路径指向的是一个JSON字符串。
  5. "NUMBER":如果路径指向的是一个JSON数字(包括整数和浮点数)。
  6. "TRUE" 或 "FALSE":如果路径指向的是布尔值
    true

    false
mysql> SELECT JSON_TYPE(NULL) AS json_null, JSON_TYPE('["a", "b","c"]') AS json_array, JSON_TYPE('{"name":"CoderAcademy", "age": 30}') AS json_object;
+-----------+------------+-------------+
| json_null | json_array | json_object |
+-----------+------------+-------------+
| NULL      | ARRAY      | OBJECT      |
+-----------+------------+-------------+
1 row in set (0.00 sec)

2、合并JSON

  • JSON_MERGE_PRESERVE
    用于合并两个或多个JSON文档的函数。并保留所有键值对。
    语法结构:
JSON_MERGE_PRESERVE(json_doc, json_doc[, json_doc] ...)
mysql> SELECT JSON_MERGE_PRESERVE('["a", 1]', '{"key": "value"}') AS json_value;
+----------------------------+
| json_value                 |
+----------------------------+
| ["a", 1, {"key": "value"}] |
+----------------------------+
1 row in set (0.00 sec)

在处理重复键时,如果是合并对象(JOSN_OBJECT),将value自动包装为数组,并通过组合值的方式合并数组。

mysql> SELECT JSON_MERGE_PRESERVE('{"name":"CoderAcademy", "age": 30}', '{"name":"CoderAcademy", "age": 35}') AS json_value;
+-------------------------------------------------------------+
| json_value                                                  |
+-------------------------------------------------------------+
| {"age": [30, 35], "name": ["CoderAcademy", "CoderAcademy"]} |
+-------------------------------------------------------------+
1 row in set (0.00 sec)

如果是合并数组(JSON_ARRAY),将它们的值组合成一个数组,作为结果中该键的值。

mysql> SELECT JSON_MERGE_PRESERVE('{"hobbies":["Java", "Mysql"]}', '{"hobbies":["Python", "Mysql"]}') AS json_value;
+---------------------------------------------------+
| json_value                                        |
+---------------------------------------------------+
| {"hobbies": ["Java", "Mysql", "Python", "Mysql"]} |
+---------------------------------------------------+
1 row in set (0.00 sec)

MySQL 8.0.3以后支持

  • JSON_MERGE_PATCH
    用于合并两个或多个JSON文档的函数。仅保留最后一个值。
    语法格式如下:
JSON_MERGE_PATCH(json_doc, json_doc[, json_doc] ...)
mysql> SELECT JSON_MERGE_PATCH('["a", 1]', '{"key": "value"}') AS json_value;
+------------------+
| json_value       |
+------------------+
| {"key": "value"} |
+------------------+
1 row in set (0.00 sec)

mysql> SELECT JSON_MERGE_PATCH('{"name":"CoderAcademy", "age": 30}', '{"name":"CoderAcademy", "age": 35}') AS json_value;
+-------------------------------------+
| json_value                          |
+-------------------------------------+
| {"age": 35, "name": "CoderAcademy"} |
+-------------------------------------+
1 row in set (0.00 sec)

MySQL 8.0.3以后支持

3、JSON搜索

  • JSON_CONTAINS
    MySQL8.0中引入的用于检查 JSON 数组或对象是否包含特定值或键值对的函数。语法格式如下:
JSON_CONTAINS(json_doc, candidate[, path])

其中
json_doc
是要检查的JSON文档,通常是一个列名或者JSON字符串表达式。
candidate
是要查找的值。而
path
(可选)指定在 JSON 文档中的路径,用于更精确地定位要检查的元素,如果没有指定路径,则在整个JSON文档中搜索给定的候选值。
JSON_CONTAINS
函数返回
1
表示包含,返回
0
表示不包含。

mysql> SELECT JSON_CONTAINS('{"a": 1, "b": 2, "c": {"d": 4}}', '1', '$.a') AS contains_value;
+----------------+
| contains_value |
+----------------+
|              1 |
+----------------+
1 row in set (0.00 sec)

mysql> SELECT JSON_CONTAINS('[ "apple", "banana", "cherry" ]', '"apple"') AS contains_apple;
+----------------+
| contains_apple |
+----------------+
|              1 |
+----------------+
1 row in set (0.00 sec)

mysql> SELECT JSON_CONTAINS('{"fruits": ["apple", "banana", "cherry"]}', '"apple"', '$.fruits') AS contains_apple_in_path;
+------------------------+
| contains_apple_in_path |
+------------------------+
|                      1 |
+------------------------+
1 row in set (0.00 sec)

mysql> SELECT * FROM test_json WHERE JSON_CONTAINS(json_data, '"apple"', '$.fruits');
+----+-------------------------------------------+---------------------+
| id | json_data                                 | create_time         |
+----+-------------------------------------------+---------------------+
|  3 | {"fruits": ["apple", "banana", "cherry"]} | 2024-02-05 07:28:40 |
+----+-------------------------------------------+---------------------+
1 row in set (0.00 sec)

特别需要注意的是:
在使用
JSON_CONTAINS
时,注意数据类型的匹配,确保值的类型与JSON中的类型一致。

JSON_CONTAINS参数传递匹配值是
''
并不代表他是一个字符串,例如上述第一个例子:我们搜索
1
,使用
'1'
,搜索
apple
时,使用
'"apple"'

同时
JSON_CONTAINS()
不支持对嵌套JSON文档进行递归搜索,它只针对JSON路径指定的位置进行比较。

  • JSON_CONTAINS_PATH
    用于检查JSON文档中是否存在指定路径的函数。与
    JSON_CONTAINS()
    函数不同,
    JSON_CONTAINS_PATH()
    不检查路径对应的值是否匹配某个特定值,而是仅判断JSON文档内是否存在指定的路径结构。语法结构:
JSON_CONTAINS_PATH(json_doc, one_or_all, path[, path] ...)

json_doc
是待检查的目标JSON文档。
one_or_all
是一个字符串参数,可以是
'one'

'all'

'one'
表示只要存在任意一个提供的路径就返回1(真),
'all'
表示所有提供的路径都必须存在才返回1(真)。
path
:一个或多个JSON路径表达式,用于在JSON文档内部定位要检查的路径。

mysql> SET @json_data = '{"name":"John","age":30,"hobbies":["reading","coding"],"address":{"city":"New York","country":"USA"}}';
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT JSON_CONTAINS_PATH(@json_data, 'one', '$.name') AS has_name, JSON_CONTAINS_PATH(@json_data, 'one', '$.address.country') AS has_country, JSON_CONTAINS_PATH(@json_data, 'all', '$.hobbies[0]', '$.hobbies[1]') AS has_both_hobbies;
+----------+-------------+------------------+
| has_name | has_country | has_both_hobbies |
+----------+-------------+------------------+
|        1 |           1 |                1 |
+----------+-------------+------------------+
1 row in set (0.00 sec)

mysql> SELECT * FROM test_json WHERE JSON_CONTAINS_PATH(json_data, 'one', '$.address.country');
+----+------------------------------------------------------------------------------------------------------------------+---------------------+
| id | json_data                                                                                                        | create_time         |
+----+------------------------------------------------------------------------------------------------------------------+---------------------+
|  2 | {"age": 30, "name": "John", "address": {"city": "New York", "country": "USA"}, "hobbies": ["reading", "coding"]} | 2024-02-05 07:25:47 |
+----+------------------------------------------------------------------------------------------------------------------+---------------------+
1 row in set (0.00 sec)
  • JSON_EXTRACT
    从JSON字段中提取指定路径的值。语法格式:
JSON_EXTRACT(json_doc, path[, path] ...)

json_doc
是包含JSON数据的列名或者直接的JSON字符串。
path
是一个或多个以逗号分隔的JSON路径表达式,用于指定要提取的值的位置。路径可以使用点
.
和方括号
[]
来表示对象属性和数组索引。

mysql> SET @user_info = '{"name":"John Doe","age":30,"hobbies":["reading","coding"],"address":{"street":"123 Main St","city":"New York"}}';
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT
    ->   JSON_EXTRACT(@user_info, '$.name') AS name,
    ->   JSON_EXTRACT(@user_info, '$.age') AS age,
    ->   JSON_EXTRACT(@user_info, '$.hobbies[0]') AS first_hobby,
    ->   JSON_EXTRACT(@user_info, '$.address.city') AS city;
+------------+------+-------------+------------+
| name       | age  | first_hobby | city       |
+------------+------+-------------+------------+
| "John Doe" | 30   | "reading"   | "New York" |
+------------+------+-------------+------------+
1 row in set (0.00 sec)

mysql> SELECT JSON_EXTRACT(json_data, '$.name')            AS name,
    ->        JSON_EXTRACT(json_data, '$.age')             AS age,
    ->        JSON_EXTRACT(json_data, '$.hobbies[0]')      AS first_hobby,
    ->        JSON_EXTRACT(json_data, '$.address.city') AS city
    -> FROM test_json
    -> WHERE JSON_CONTAINS_PATH(json_data, 'one', '$.name');
+------------+------+-------------+------------+
| name       | age  | first_hobby | city       |
+------------+------+-------------+------------+
| "John Doe" | 30   | "reading"   | "New York" |
| "John"     | 30   | "reading"   | "New York" |
+------------+------+-------------+------------+
2 rows in set (0.00 sec)

我们介绍一下path中的一些规则:


  1. .
    运算符
    用于访问嵌套的对象属性。例如:
    $.name
    表示访问顶级对象的 "name" 属性。
  2. 方括号
    []
    运算符
    用于访问数组元素。对于数组索引,使用数字表示位置,从0开始。例如:
    $.hobbies[0]
    表示访问顶级对象 "hobbies" 数组的第一个元素。
  3. 多路径查询
    在一个函数调用中可以指定多个路径,每个路径之间用逗号分隔。例如:
    JSON_EXTRACT(json_column, '$.path1', '$.path2')
  • JSON_KEYS
    用于从JSON文档中提取所有键(key)的一个函数,它返回一个包含JSON对象所有键名的数组。这对于需要获取或操作JSON对象内部属性名称时非常有用。语法结构:
JSON_KEYS(json_doc[, path])

json_doc
是包含JSON数据的列名或者直接的JSON字符串。
path
(可选参数)如果提供了路径,则只返回该路径指向的对象内的键。例如,若要提取嵌套在 JSON 文档内某个对象的所有键,可以指定到该对象的路径。

mysql> SELECT JSON_KEYS(json_data) AS top_level_keys FROM test_json;
+---------------------------------------+
| top_level_keys                        |
+---------------------------------------+
| ["age", "name", "address", "hobbies"] |
| ["age", "name", "address", "hobbies"] |
| ["fruits"]                            |
+---------------------------------------+
3 rows in set (0.00 sec)

mysql> SELECT JSON_KEYS(json_data, '$.address') AS address_keys FROM test_json WHERE JSON_CONTAINS_PATH(json_data, 'one', '$.address');
+---------------------+
| address_keys        |
+---------------------+
| ["city", "street"]  |
| ["city", "country"] |
+---------------------+
2 rows in set (0.00 sec)
  • JSON_OVERLAPS
    该函数会检查两个JSON对象,并返回布尔值。如果至少有一个键存在于两个对象中且对应值相等,则返回真(1),否则返回假(0)。这个函数并不关注两个JSON对象的所有内容,仅针对有交集的键值对进行比较。语法结构如下:
JSON_OVERLAPS(json_doc1, json_doc2)
mysql> SET @doc1 = '{"name": "John", "age": 30}';
Query OK, 0 rows affected (0.00 sec)

mysql> SET @doc2 = '{"name": "John", "address": "New York"}';
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT JSON_OVERLAPS(@doc1, @doc2) AS do_overlap;
+------------+
| do_overlap |
+------------+
|          1 |
+------------+
1 row in set (0.00 sec)

需要注意的是,如果两个JSON对象没有共享的键,或者共享的键对应的值不相等,则此函数返回0。此外,它只适用于JSON对象,对于JSON数组则不会进行这样的比较。

MySQL 8.0.17以后才有这个函数

  • JSON_SEARCH
    用于在JSON文档中搜索指定的字符串值,并返回找到该值的第一个匹配路径。这个函数对于从JSON数据结构中检索特定值非常有用。语法结构如下:
JSON_SEARCH(json_doc, one_or_all, search_str[, escape_char[, path] ...])

json_doc
包含要搜索的JSON数据的列名或JSON字符串。
one_or_all
表示搜索模式,可以是
'one'

'all'
,如果是
'one'
(默认)则只要找到一个匹配就返回第一个匹配的路径,如果没有找到则返回NULL,如果是
'all'
则返回所有匹配路径的JSON数组,如果没有找到则返回NULL。
search_str
是要在JSON文档中查找的字符串值。
escape_char
(可选)代表如果搜索字符串中有特殊字符需要转义,可以提供一个转义字符。
path
(可选)可以指定一个或多个JSON路径,限制搜索范围到这些路径所指向的对象或数组元素。

mysql> SELECT
    ->   JSON_SEARCH(json_data, 'one', 'John') AS name_path,
    ->   JSON_SEARCH(json_data, 'all', 'New York') AS main_street_path
    -> FROM test_json;
+-----------+------------------+
| name_path | main_street_path |
+-----------+------------------+
| NULL      | "$.address.city" |
| "$.name"  | "$.address.city" |
| NULL      | NULL             |
+-----------+------------------+
3 rows in set (0.00 sec)

注意,
JSON_SEARCH()
主要适用于搜索字符串类型的值,在MySQL 8.0及以前版本中,它不支持直接搜索数值型或其他非字符串类型的内容。此外,该函数可能无法处理嵌套的JSON对象或数组内的复杂搜索场景,因为它只能返回单个键值对路径,而不是深度遍历整个JSON结构以寻找匹配项。

  • JSON_VALUE
    用于从JSON文档中提取标量值的一个函数。它允许你从JSON数据结构中检索出符合SQL数据类型的特定键的值,并将其转换为一个可直接在SQL语句中使用的标准SQL数据类型(如字符串、数字或布尔值)。语法格式如下:
JSON_VALUE(json_doc, path)

JSON_VALUE()
返回的是位于给定路径下的JSON文档中的标量值(即字符串、数字或布尔值),而不是JSON格式的值。这与
JSON_EXTRACT()
函数不同,后者返回的是JSON格式的值,即使提取的是标量值也是如此。

mysql> SELECT
    ->   JSON_VALUE(json_data, '$.name') AS name,
    ->   JSON_VALUE(json_data, '$.age') AS age,
    ->   JSON_VALUE(json_data, '$.is_student') AS is_student
    -> FROM test_json;
+----------+------+------------+
| name     | age  | is_student |
+----------+------+------------+
| John Doe | 30   | NULL       |
| John     | 30   | NULL       |
| NULL     | NULL | NULL       |
| John Doe | 30   | true       |
+----------+------+------------+
4 rows in set (0.01 sec)

函数简化了JSON数据在SQL查询中的处理,特别是当你需要将JSON字段的值作为普通SQL数据类型进行比较、聚合或其他操作时。

MySQL 8.0.21版本开始提供JSON_VALUE

4、JSON数据修改

  • JSON_ARRAY_APPEND
    用于向JSON数组末尾追加元素的函数。这个函数允许你在现有的JSON数组中添加新的元素,无论是标量值还是嵌套的JSON对象或数组。语法格式:
JSON_ARRAY_APPEND(json_doc, path, val[, path, val] ...)

json_doc
指包含JSON数据的列名或者直接的JSON字符串。
path
代表一个JSON路径表达式,指向要追加元素到其后的JSON数组。而
value
就是要追加到指定路径下JSON数组中的值,可以是标量值(如字符串、数字、布尔值),也可以是另一个JSON对象或数组。

mysql> SELECT json_data FROM test_json WHERE id = 3;
+-------------------------------------------+
| json_data                                 |
+-------------------------------------------+
| {"fruits": ["apple", "banana", "cherry"]} |
+-------------------------------------------+
1 row in set (0.00 sec)

mysql> UPDATE test_json
    -> SET json_data = JSON_ARRAY_APPEND(json_data, '$.fruits', 'cherry')
    -> WHERE id = 3;
Query OK, 1 row affected (0.02 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> SELECT json_data FROM test_json WHERE id = 3;
+-----------------------------------------------------+
| json_data                                           |
+-----------------------------------------------------+
| {"fruits": ["apple", "banana", "cherry", "cherry"]} |
+-----------------------------------------------------+
1 row in set (0.00 sec)

这里有一点需要注意,如果
path
所指的对象不是一个数组,那么在进行追加操作之前,MySQL会将该对象转换为一个只有一个元素的新数组。

mysql> SELECT json_data FROM test_json WHERE id = 5;
+--------------------------------------------------------------+
| json_data                                                    |
+--------------------------------------------------------------+
| {"fruits": ["apple", "banana", "cherry"], "hobbies": "Java"} |
+--------------------------------------------------------------+
1 row in set (0.01 sec)

mysql> UPDATE test_json
    -> SET json_data = JSON_ARRAY_APPEND(json_data, '$.hobbies', 'Python')
    -> WHERE id = 5;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> SELECT json_data FROM test_json WHERE id = 5;
+--------------------------------------------------------------------------+
| json_data                                                                |
+--------------------------------------------------------------------------+
| {"fruits": ["apple", "banana", "cherry"], "hobbies": ["Java", "Python"]} |
+--------------------------------------------------------------------------+
1 row in set (0.00 sec)

此函数适用于需要动态修改和扩展数据库内存储的JSON数组的情况,特别是在处理具有可变长度列表的数据时特别有用。

  • JSON_ARRAY_INSERT
    用于向JSON数组的特定位置插入元素的函数。这个函数允许你在现有的JSON数组的指定索引处插入一个新元素,这个元素可以是单个元素值、JSON数组、JSON对象。语法格式:
JSON_ARRAY_INSERT(json_doc, path, val[, path, val] ...)

json_doc
代表包含JSON数据的列名或者直接的JSON字符串。而
path
是一个JSON路径表达式,指向要插入元素的JSON数组,并且可以指定要插入的位置(通过数组索引)。而
value
就是要插入到指定路径下JSON数组中的值,可以是普通值(如字符串、数字、布尔值),也可以是另一个JSON对象或数组。

mysql> SELECT json_data FROM test_json WHERE id = 6;
+----------------------------------------------------+
| json_data                                          |
+----------------------------------------------------+
| {"fruits": ["apple", "banana"], "hobbies": "Java"} |
+----------------------------------------------------+
1 row in set (0.00 sec)

mysql> UPDATE test_json
    -> SET json_data = JSON_ARRAY_INSERT(json_data, '$.fruits[0]', 'cherry')
    -> WHERE id = 6;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> SELECT json_data FROM test_json WHERE id = 6;
+--------------------------------------------------------------+
| json_data                                                    |
+--------------------------------------------------------------+
| {"fruits": ["cherry", "apple", "banana"], "hobbies": "Java"} |
+--------------------------------------------------------------+
1 row in set (0.00 sec)

-- 插入一个数组
mysql> UPDATE test_json
    -> SET json_data = JSON_ARRAY_INSERT(json_data, '$.fruits[0]', CAST('["cherry", "orange"]' AS JSON))
    -> WHERE id = 7;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> SELECT json_data FROM test_json WHERE id = 7;
+--------------------------------------------------------------------------+
| json_data                                                                |
+--------------------------------------------------------------------------+
| {"fruits": [["cherry", "orange"], "apple", "banana"], "hobbies": "Java"} |
+--------------------------------------------------------------------------+
1 row in set (0.00 sec)
  • JSON_INSERT
    用于向JSON文档插入新键值对或替换已存在键的值的一个函数。语法格式如下:
JSON_INSERT(json_doc, path, val[, path, val] ...)

json_doc
代表包含JSON数据的列名或者直接的JSON字符串。而
path
是一个JSON路径表达式,指向要插入元素的JSON数组,并且可以指定要插入的位置(通过数组索引)。而
value
就是要插入到指定路径下JSON数组中的值,可以是普通值(如字符串、数字、布尔值),也可以是另一个JSON对象或数组。

如果在JSON文档中,路径已存在,则不会覆盖现有的文档值。

mysql> SELECT json_data FROM test_json WHERE id = 8;
+----------------------------------------------------+
| json_data                                          |
+----------------------------------------------------+
| {"fruits": ["apple", "banana"], "hobbies": "Java"} |
+----------------------------------------------------+
1 row in set (0.00 sec)

mysql> UPDATE test_json
    -> SET json_data = JSON_INSERT(json_data, '$.hobbies', 'Python')
    -> WHERE id = 8;
Query OK, 0 rows affected (0.00 sec)
Rows matched: 1  Changed: 0  Warnings: 0

mysql> SELECT json_data FROM test_json WHERE id = 8;
+----------------------------------------------------+
| json_data                                          |
+----------------------------------------------------+
| {"fruits": ["apple", "banana"], "hobbies": "Java"} |
+----------------------------------------------------+
1 row in set (0.00 sec)

如果指定的路径不存在于原始JSON文档中,则会在该路径处创建新的键值对。

mysql> SELECT json_data FROM test_json WHERE id = 9;
+---------------------------------+
| json_data                       |
+---------------------------------+
| {"fruits": ["apple", "banana"]} |
+---------------------------------+
1 row in set (0.00 sec)

mysql> UPDATE test_json
    -> SET json_data = JSON_INSERT(json_data, '$.hobbies', CAST('["Java", "Python"]' AS JSON),
    ->     '$.name', 'CoderAcademy',
    ->     '$.address', cast('{"city": "New York", "street": "123 Main St"}' AS JSON))
    -> WHERE id = 9;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> SELECT json_data FROM test_json WHERE id = 9;
+--------------------------------------------------------------------------------------------------------------------------------------------------+
| json_data                                                                                                                                        |
+--------------------------------------------------------------------------------------------------------------------------------------------------+
| {"name": "CoderAcademy", "fruits": ["apple", "banana"], "address": {"city": "New York", "street": "123 Main St"}, "hobbies": ["Java", "Python"]} |
+--------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)
  • JSON_REMOVE
    用于从 JSON 文档中删除数据。语法结构如下:
JSON_REMOVE(json_doc, path[, path] ...)

json_doc
代表包含JSON数据的列名或者直接的JSON字符串。
path
代表删除数据的路径。

如果路径存在,则会删除路径对应的元素,否则对数据没有影响,即不会删除,也不会报错。

mysql> SELECT json_data FROM test_json WHERE id = 10;
+----------------------------------------------------+
| json_data                                          |
+----------------------------------------------------+
| {"fruits": ["apple", "banana"], "hobbies": "Java"} |
+----------------------------------------------------+
1 row in set (0.00 sec)

mysql> UPDATE test_json
    -> SET json_data = JSON_REMOVE(json_data, '$.hobbies')
    -> WHERE id = 10;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> SELECT json_data FROM test_json WHERE id = 10;
+---------------------------------+
| json_data                       |
+---------------------------------+
| {"fruits": ["apple", "banana"]} |
+---------------------------------+
1 row in set (0.00 sec)

mysql> UPDATE test_json
    -> SET json_data = JSON_REMOVE(json_data, '$.fruits[1]')
    -> WHERE id = 10;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> SELECT json_data FROM test_json WHERE id = 10;
+-----------------------+
| json_data             |
+-----------------------+
| {"fruits": ["apple"]} |
+-----------------------+
1 row in set (0.00 sec)

mysql> UPDATE test_json
    -> SET json_data = JSON_REMOVE(json_data, '$.fruits[1]')
    -> WHERE id = 10;
Query OK, 0 rows affected (0.00 sec)
Rows matched: 1  Changed: 0  Warnings: 0

mysql> SELECT json_data FROM test_json WHERE id = 10;
+-----------------------+
| json_data             |
+-----------------------+
| {"fruits": ["apple"]} |
+-----------------------+
1 row in set (0.00 sec)
  • JSON_REPLACE
    函数用于替换 JSON 文档中的现有值。语法结构如下:
JSON_REPLACE(json_doc, path, val[, path, val] ...)

json_doc
代表包含JSON数据的列名或者直接的JSON字符串。
path
代表替换数据的路径。
val
代表要替换的值。

如果替换路径在文档中存在,则就用新值覆盖文档中原值,否则不会替换,也不会报错。

mysql> SELECT json_data FROM test_json WHERE id = 11;
+----------------------------------------------------+
| json_data                                          |
+----------------------------------------------------+
| {"fruits": ["apple", "banana"], "hobbies": "Java"} |
+----------------------------------------------------+
1 row in set (0.01 sec)

mysql> UPDATE test_json
    -> SET json_data = JSON_REPLACE(json_data, '$.name', 'CoderAcademy')
    -> WHERE id = 11;
Query OK, 0 rows affected (0.00 sec)
Rows matched: 1  Changed: 0  Warnings: 0

mysql> SELECT json_data FROM test_json WHERE id = 11;
+----------------------------------------------------+
| json_data                                          |
+----------------------------------------------------+
| {"fruits": ["apple", "banana"], "hobbies": "Java"} |
+----------------------------------------------------+
1 row in set (0.00 sec)


mysql> UPDATE test_json
    -> SET json_data = JSON_REPLACE(json_data, '$.fruits[1]', 'orange', '$.hobbies',  CAST('["Java", "Python"]' AS JSON))
    -> WHERE id = 11;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> SELECT json_data FROM test_json WHERE id = 11;
+----------------------------------------------------------------+
| json_data                                                      |
+----------------------------------------------------------------+
| {"fruits": ["apple", "orange"], "hobbies": ["Java", "Python"]} |
+----------------------------------------------------------------+
1 row in set (0.00 sec)
  • JSON_SET
    用于在 JSON 文档中插入或更新数据。语法格式如下:
JSON_SET(json_doc, path, val[, path, val] ...)

json_doc
代表包含JSON数据的列名或者直接的JSON字符串。
path
代表替换数据的路径。
val
代表要插入或更新的新值。

如果路径在文档中已存在,则会覆盖原文档中值,如果不存在,则插入新值。

mysql> SELECT json_data FROM test_json WHERE id = 12;
+----------------------------------------------------+
| json_data                                          |
+----------------------------------------------------+
| {"fruits": ["apple", "banana"], "hobbies": "Java"} |
+----------------------------------------------------+
1 row in set (0.00 sec)

mysql> UPDATE test_json
    -> SET json_data = JSON_SET(json_data, '$.fruits[1]', 'orange',
    ->     '$.hobbies', CAST('["Java", "Python"]' AS JSON),
    ->     '$.name', 'CoderAcademy',
    ->     '$.address', cast('{"city": "New York", "street": "123 Main St"}' AS JSON))
    -> WHERE id = 12;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> SELECT json_data FROM test_json WHERE id = 12;
+--------------------------------------------------------------------------------------------------------------------------------------------------+
| json_data                                                                                                                                        |
+--------------------------------------------------------------------------------------------------------------------------------------------------+
| {"name": "CoderAcademy", "fruits": ["apple", "orange"], "address": {"city": "New York", "street": "123 Main St"}, "hobbies": ["Java", "Python"]} |
+--------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

JSON类型列的索引

在 MySQL 8.0 中,JSON 类型的索引有一些重要的改进和新特性。JSON 类型的索引使得在处理包含 JSON 数据的列时,查询的性能得到了显著提升。

JSON路径索引

MySQL 8.0 引入了 JSON 路径索引,允许在 JSON 对象的特定路径上创建索引,以便更高效地执行 JSON 路径查询。路径索引允许在 JSON 对象中的特定路径上进行范围查询、排序和过滤。我们以查询地址信息中
country
等于
"US"为例
,他有这几中写法:

-- 使用JSON_CONTAINS
SELECT * FROM user_info WHERE JSON_CONTAINS(address, '"US"', '$.country');

-- 使用JSON_VALUE
SELECT * FROM user_info WHERE JSON_VALUE(address, '$.country') = "US";

-- 使用JSON_EXTRACT
SELECT * FROM user_info WHERE JSON_EXTRACT(address, '$.country') = 'US';

-- 使用 ->> 运算符
SELECT * FROM user_info WHERE address->>"$.country" = "US";
-- 或者
SELECT * FROM user_info WHERE CAST(address->>"$.country" AS CHAR(30)) = "US";

在JSON类型字段上创建索引时,要遵守的规则是要确保索引表达式与查询时的条件表达式匹配,这样MySQL才能正确地使用索引进行优化查询。

所以针对不同的sql查询,我们提供不同的索引。例如:

  • 使用JSON_EXTRACT
    我们可以采取新增一个虚拟列的方式去使用索引,比如我们新增一个
    country
    的虚拟列,然后在虚拟列上增加索引
-- 添加生成的虚拟列  
ALTER TABLE user_info  
ADD COLUMN country VARCHAR(255) GENERATED ALWAYS AS (JSON_UNQUOTE(JSON_EXTRACT(address, '$.country'))) STORED;  
  
-- 在生成的列上创建索引  
CREATE INDEX idx_json_country ON user_info(country);

我们执行一下计划:

mysql> EXPLAIN SELECT * FROM user_info WHERE country = 'US';
+----+-------------+-----------+------------+------+------------------+------------------+---------+-------+------+----------+-------+
| id | select_type | table     | partitions | type | possible_keys    | key              | key_len | ref   | rows | filtered | Extra |
+----+-------------+-----------+------------+------+------------------+------------------+---------+-------+------+----------+-------+
|  1 | SIMPLE      | user_info | NULL       | ref  | idx_json_country | idx_json_country | 1023    | const |    2 |   100.00 | NULL  |
+----+-------------+-----------+------------+------+------------------+------------------+---------+-------+------+----------+-------+
1 row in set, 1 warning (0.01 sec)

可以看出使用了索引
idx_json_country

  • 使用 ->> 运算符
    在 JSON 类型的索引中,使用
    ->>
    运算符,会转换为
    JSON_UNQUOTE(JSON_EXTRACT(...))
    ,而
    JSON_UNQUOTE()
    返回的值具有LONGTEXT 数据类型。MySQL不能对没有键部分上的前缀长度指定的 LONGTEXT 列建立索引,而在功能性键部分中又不允许指定前缀长度。

我们可以这样创建索引:

CREATE INDEX idx_json_country_cast ON user_info((CAST(address->>"$.country" AS CHAR(30)) COLLATE utf8mb4_bin));

然后看一下执行计划:

mysql> EXPLAIN SELECT * FROM user_info WHERE address->>"$.country" = "US";
+----+-------------+-----------+------------+------+------------------+------------------+---------+-------+------+----------+-------+
| id | select_type | table     | partitions | type | possible_keys    | key              | key_len | ref   | rows | filtered | Extra |
+----+-------------+-----------+------------+------+------------------+------------------+---------+-------+------+----------+-------+
|  1 | SIMPLE      | user_info | NULL       | ref  | idx_json_country | idx_json_country_cast | 123    | const |    2 |   100.00 | NULL  |
+----+-------------+-----------+------------+------+------------------+------------------+---------+-------+------+----------+-------+
1 row in set, 1 warning (0.01 sec)

多值索引

多值索引是 MySQL 8.0.17 版本引入的新功能,它允许在 InnoDB 存储引擎中创建索引来支持对存储数组值的列进行高效查询。传统的索引是一对一的,而多值索引允许在一个数据记录上拥有多个索引记录。多值索引主要用于索引 JSON 数组。

要创建多值索引,可以在 CREATE TABLE、ALTER TABLE 或 CREATE INDEX 语句中使用 CAST(... AS ... ARRAY) 函数来定义。这将把 JSON 数组中的同类型标量值转换为 SQL 数据类型数组。然后,MySQL 会在这个 SQL 数据类型数组上创建一个虚拟列,并在虚拟列上创建一个功能性索引。最终,这个功能性索引构成了多值索引。

例如,我们在
address
中增加一个
zipcode
列用于存储地址邮编,每个地址包含若干个邮编。我们对这个
zipcode
就可以使用多值索引。

mysql> ALTER TABLE user_info ADD INDEX idx_json_zipcode((CAST(address->'$.zipcode' AS SIGNED ARRAY)));
Query OK, 0 rows affected (0.03 sec)
Records: 0  Duplicates: 0  Warnings: 0

我们分别执行以下sql进行验证:

mysql> SELECT * FROM user_info WHERE 94507 MEMBER OF(address->'$.zipcode');
+----+-----------+--------------------------------------------------------------------------------------------------+---------------------+
| id | user_name | address                                                                                          | create_time         |
+----+-----------+--------------------------------------------------------------------------------------------------+---------------------+
|  2 | lisi      | {"city": "shanghai", "street": "123 Main St", "country": "CN", "zipcode": [94568, 94507, 94582]} | 2024-02-05 11:08:22 |
|  3 | wangwu    | {"city": "guangzhou", "street": "123 Main St", "country": "CN", "zipcode": [94477, 94507]}       | 2024-02-05 11:08:22 |
|  4 | qianliu   | {"city": "New York", "street": "123 Main St", "country": "US", "zipcode": [94507, 94582]}        | 2024-02-05 11:08:22 |
+----+-----------+--------------------------------------------------------------------------------------------------+---------------------+
3 rows in set (0.01 sec)

mysql> EXPLAIN
    -> SELECT * FROM user_info WHERE 94507 MEMBER OF(address->'$.zipcode');
+----+-------------+-----------+------------+------+------------------+------------------+---------+-------+------+----------+-------------+
| id | select_type | table     | partitions | type | possible_keys    | key              | key_len | ref   | rows | filtered | Extra       |
+----+-------------+-----------+------------+------+------------------+------------------+---------+-------+------+----------+-------------+
|  1 | SIMPLE      | user_info | NULL       | ref  | idx_json_zipcode | idx_json_zipcode | 9       | const |    3 |   100.00 | Using where |
+----+-------------+-----------+------------+------+------------------+------------------+---------+-------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)

可以看出使用了索引。

mysql> SELECT * FROM user_info WHERE JSON_CONTAINS(address->'$.zipcode', CAST('[94507,94582]' AS JSON));
+----+-----------+--------------------------------------------------------------------------------------------------+---------------------+
| id | user_name | address                                                                                          | create_time         |
+----+-----------+--------------------------------------------------------------------------------------------------+---------------------+
|  2 | lisi      | {"city": "shanghai", "street": "123 Main St", "country": "CN", "zipcode": [94568, 94507, 94582]} | 2024-02-05 11:08:22 |
|  4 | qianliu   | {"city": "New York", "street": "123 Main St", "country": "US", "zipcode": [94507, 94582]}        | 2024-02-05 11:08:22 |
+----+-----------+--------------------------------------------------------------------------------------------------+---------------------+
2 rows in set (0.01 sec)

mysql> EXPLAIN
    -> SELECT * FROM user_info WHERE JSON_CONTAINS(address->'$.zipcode', CAST('[94507,94582]' AS JSON));
+----+-------------+-----------+------------+-------+------------------+------------------+---------+------+------+----------+-------------+
| id | select_type | table     | partitions | type  | possible_keys    | key              | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-----------+------------+-------+------------------+------------------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | user_info | NULL       | range | idx_json_zipcode | idx_json_zipcode | 9       | NULL |    6 |   100.00 | Using where |
+----+-------------+-----------+------------+-------+------------------+------------------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)

多值索引还可以作为复合索引的一部分进行定义。在复合索引中,只能有一个多值键部分,并且可以与其他单值部分一起使用。多值键部分可以按任意顺序使用。

mysql> ALTER TABLE user_info ADD INDEX idx_name_zipcode(user_name, (CAST(address->'$.zipcode' AS SIGNED ARRAY)));
Query OK, 0 rows affected (0.04 sec)
Records: 0  Duplicates: 0  Warnings: 0

总结

MySQL提供了丰富的内置函数支持JSON数据类型的存储和操作,如检查、提取、插入、更新及搜索JSON文档中的内容。此外,为了提高含有JSON字段的查询效率,用户可以在满足条件的情况下为JSON特定路径创建索引,如使用虚拟生成列或者MySQL 8.0以上的原生JSON路径索引功能。多值索引特别适用于JSON数组元素的检索优化,使得数据库能针对数组内的每个独立值建立索引记录,提升复杂查询场景下的性能表现。

我们可以了解到 JSON 数据类型的灵活性、易用性以及适用性,以及如何在实际项目中充分利用它来应对动态数据模型、半结构化数据存储和查询等方面的挑战。

文中示例表结构与数据

CREATE TABLE `test_json`  
(  
    `id`          BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增主键',  
    `json_data`     JSON COMMENT 'json值',  
    `create_time` datetime            NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',  
    PRIMARY KEY ( `id` )  
) ENGINE = INNODB  
  AUTO_INCREMENT = 1  
  DEFAULT CHARSET = utf8mb4 COMMENT = 'json测试表';


CREATE TABLE `user_info`  
(  
    `id`          BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增主键',  
    `user_name` varchar(64) NOT NULL DEFAULT '' COMMENT '名称',  
    `address`     JSON COMMENT '地址信息',  
    `create_time` datetime            NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',  
    PRIMARY KEY ( `id` )  
) ENGINE = INNODB  
  AUTO_INCREMENT = 1  
  DEFAULT CHARSET = utf8mb4 COMMENT = '用户信息';

参考文献

MySQL :: MySQL 8.0 Reference Manual :: 15.1.15 CREATE INDEX Statement

MySQL :: MySQL 8.0 Reference Manual :: 14.17 JSON Functions

本文已收录于我的个人博客:
码农Academy的博客,专注分享Java技术干货,包括Java基础、Spring Boot、Spring Cloud、Mysql、Redis、Elasticsearch、中间件、架构设计、面试题、程序员攻略等

一、简介

在之前的线程系列文章中,我们介绍了线程创建的几种方式以及常用的方法介绍。

今天我们接着聊聊多线程线程安全的问题,以及解决办法。

实际上,在多线程环境中,难免会出现多个线程对一个对象的实例变量进行同时访问和操作,如果编程处理不当,会产生
脏读
现象。

二、线程安全问题介绍

我们先来看一个简单的线程安全问题的例子!

public class DataEntity {

    private int count = 0;

    public void addCount(){
        count++;
    }

    public int getCount(){
        return count;
    }
}
public class MyThread extends Thread {

    private DataEntity entity;

    public MyThread(DataEntity entity) {
        this.entity = entity;
    }

    @Override
    public void run() {
        for (int j = 0; j < 1000000; j++) {
            entity.addCount();
        }
    }
}
public class MyThreadTest {

    public static void main(String[] args) {
        // 初始化数据实体
        DataEntity entity = new DataEntity();
        //使用多线程编程对数据进行计算
        for (int i = 0; i < 10; i++) {
            MyThread thread = new MyThread(entity);
            thread.start();
        }

        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("result: " + entity.getCount());
    }
}

多次运行结果如下:

第一次运行:result: 9788554
第二次运行:result: 9861461
第三次运行:result: 6412249
...

上面的代码中,总共开启了 10 个线程,每个线程都累加了 1000000 次,如果结果正确的话,自然而然总数就应该是 10 * 1000000 = 10000000。

但是多次运行结果都不是这个数,而且每次运行结果都不一样,为什么会出现这个结果呢

简单的说,
这是主内存和线程的工作内存数据不一致,以及多线程执行时无序,共同造成的结果

我们先简单的了解一下 Java 的内存模型,后期我们在介绍里面的原理!

如上图所示,线程 A 和线程 B 之间,如果要完成数据通信的话,需要经历以下几个步骤:

  • 1.线程 A 从主内存中将共享变量读入线程 A 的工作内存后并进行操作,之后将数据重新写回到主内存中;
  • 2.线程 B 从主存中读取最新的共享变量,然后存入自己的工作内存中,再进行操作,数据操作完之后再重新写入到主内存中;

如果线程 A 更新后数据并没有及时写回到主存,而此时线程 B 从主内存中读到的数据,可能就是过期的数据,于是就会出现“
脏读
”现象。

因此在多线程环境下,如果不进行一定干预处理,可能就会出现像上文介绍的那样,采用多线程编程时,程序的实际运行结果与预期会不一致,就会产生非常严重的问题。

针对多线程编程中,程序运行不安全的问题,Java 提供了
synchronized
关键字来解决这个问题,当多个线程同时访问共享资源时,会保证线程依次排队操作共享变量,从而保证程序的实际运行结果与预期一致。

我们对上面示例中的
DataEntity.addCount()
方法进行改造,再看看效果如下。

public class DataEntity {

    private int count = 0;

    /**
     * 在方法上加上 synchronized 关键字
     */
    public synchronized void addCount(){
        count++;
    }

    public int getCount(){
        return count;
    }
}

多次运行结果如下:

第一次运行:result: 10000000
第二次运行:result: 10000000
第三次运行:result: 10000000
...

运行结果与预期一致!

三、synchronized 使用详解

synchronized
作为 Java 中的关键字,在多线程编程中,有着非常重要的地位,也是新手了解并发编程的基础,从功能角度看,它有以下几个比较重要的特性:

  • 原子性
    :即一个或多个操作要么全部执行成功,要么全部执行失败。
    synchronized
    关键字可以保证只有一个线程拿到锁,访问共享资源
  • 可见性
    :即一个线程对共享变量进行修改后,其他线程可以立刻看到。执行
    synchronized
    时,线程获取锁之后,一定从主内存中读取数据,释放锁之前,一定会将数据写回主内存,从而保证内存数据可见性
  • 有序性
    :即保证程序的执行顺序会按照代码的先后顺序执行。
    synchronized
    关键字,可以保证每个线程依次排队操作共享变量

synchronized
也被称为同步锁,它可以把任意一个非 NULL 的对象当成锁,只有拿到锁的线程能进入方法体,并且只有一个线程能进入,其他的线程必须等待锁释放了才能进入,它属于独占式的悲观锁,同时也属于可重入锁。

关于锁的知识,我们后面在介绍,大家先了解一下就行。

从实际的使用角度来看,
synchronized
修饰的对象有以下几种:

  • 修饰一个方法
    :被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象
  • 修饰一个静态的方法
    :其作用的范围是整个静态方法,作用的对象是这个类的所有对象
  • 修饰一个代码块
    :被修饰的代码块称为同步语句块,其作用的范围是大括号
    {}
    括起来的代码,作用的对象是调用这个代码块的对象,使用上比较灵活

下面我们一起来看看它们的具体用法。

3.1、修饰一个方法


synchronized
修饰一个方法时,多个线程访问同一个对象,哪个线程持有该方法所属对象的锁,就拥有执行权限,否则就只能等待。

如果多线程访问的不是同一个对象,不会起到保证线程同步的作用

示例如下:

public class DataEntity {

    private int count;

    /**
     * 在方法上加上 synchronized 关键字
     */
    public synchronized void addCount(){
        for (int i = 0; i < 3; i++) {
            try {
                System.out.println(Thread.currentThread().getName() + ":" + (count++));
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public int getCount() {
        return count;
    }
}
public class MyThreadA extends Thread {

    private DataEntity entity;

    public MyThreadA(DataEntity entity) {
        this.entity = entity;
    }

    @Override
    public void run() {
        entity.addCount();
    }
}
public class MyThreadB extends Thread {

    private DataEntity entity;

    public MyThreadB(DataEntity entity) {
        this.entity = entity;
    }

    @Override
    public void run() {
        entity.addCount();
    }
}
public class MyThreadTest {

    public static void main(String[] args) {
        // 初始化数据实体
        DataEntity entity = new DataEntity();

        MyThreadA threadA = new MyThreadA(entity);
        threadA.start();

        MyThreadB threadB = new MyThreadB(entity);
        threadB.start();


        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("result: " + entity.getCount());
    }
}

运行结果如下:

Thread-0:0
Thread-0:1
Thread-0:2
Thread-1:3
Thread-1:4
Thread-1:5
result: 6

当两个线程共同操作一个对象时,此时每个线程都会依次排队执行。

假如两个线程操作的不是一个对象,此时没有任何效果,示例如下:

public class MyThreadTest {

    public static void main(String[] args) {
        DataEntity entity1 = new DataEntity();
        MyThreadA threadA = new MyThreadA(entity1);
        threadA.start();

        DataEntity entity2 = new DataEntity();
        MyThreadA threadB = new MyThreadA(entity2);
        threadB.start();


        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("result: " + entity1.getCount());
        System.out.println("result: " + entity2.getCount());
    }
}

运行结果如下:

Thread-0:0
Thread-1:0
Thread-0:1
Thread-1:1
Thread-0:2
Thread-1:2
result: 3
result: 3

从结果上可以看出,当
synchronized
修饰一个方法,当多个线程访问同一个对象的方法,每个线程会依次排队;如果访问的不是一个对象,线程不会进行排队,像正常执行一样。

3.2、修饰一个静态的方法

synchronized
修改一个静态的方法时,代表的是对当前
.java
文件对应的 Class 类加锁,不区分对象实例。

示例如下:

public class DataEntity {

    private static int count;

    /**
     * 在静态方法上加上 synchronized 关键字
     */
    public synchronized static void addCount(){
        for (int i = 0; i < 3; i++) {
            try {
                System.out.println(Thread.currentThread().getName() + ":" + (count++));
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static int getCount() {
        return count;
    }
}
public class MyThreadA extends Thread {

    @Override
    public void run() {
        DataEntity.addCount();
    }
}
public class MyThreadB extends Thread {

    @Override
    public void run() {
        DataEntity.addCount();
    }
}
public class MyThreadTest {

    public static void main(String[] args) {

        MyThreadA threadA = new MyThreadA();
        threadA.start();

        MyThreadB threadB = new MyThreadB();
        threadB.start();


        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("result: " + DataEntity.getCount());
    }
}

运行结果如下:

Thread-0:0
Thread-0:1
Thread-0:2
Thread-1:3
Thread-1:4
Thread-1:5
result: 6

静态同步方法和非静态同步方法持有的是不同的锁,前者是类锁,后者是对象锁,类锁可以理解为这个类的所有对象。

3.3、修饰一个代码块

synchronized
用于修饰一个代码块时,只会控制代码块内的执行顺序,其他试图访问该对象的线程将被阻塞,编程比较灵活,在实际开发中用的应用比较广泛。

示例如下

public class DataEntity {

    private int count;

    /**
     * 在方法上加上 synchronized 关键字
     */
    public void addCount(){
        synchronized (this){
            for (int i = 0; i < 3; i++) {
                try {
                    System.out.println(Thread.currentThread().getName() + ":" + (count++));
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public int getCount() {
        return count;
    }
}
public class MyThreadTest {

    public static void main(String[] args) {
        // 初始化数据实体
        DataEntity entity = new DataEntity();

        MyThreadA threadA = new MyThreadA(entity);
        threadA.start();

        MyThreadB threadB = new MyThreadB(entity);
        threadB.start();


        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("result: " + entity.getCount());
    }
}

运行结果如下:

Thread-0:0
Thread-0:1
Thread-0:2
Thread-1:3
Thread-1:4
Thread-1:5
result: 6

其中
synchronized (this)
中的
this
,表示的是当前类实例的对象,效果等同于
public synchronized void addCount()

除此之外,
synchronized()
还可以修饰任意实例对象,作用的范围就是具体的实例对象。

比如,修饰个自定义的类实例对象,作用的范围是拥有
lock
对象,其实也等价于
synchronized (this)

public class DataEntity {

    private Object lock = new Object();

    /**
     * synchronized 可以修饰任意实例对象
     */
    public void addCount(){
        synchronized (lock){
            // todo...
        }
    }
}

当然也可以用于修饰类,表示类锁,效果等同于
public synchronized static void addCount()

public class DataEntity {
    
    /**
     * synchronized 可以修饰类,表示类锁
     */
    public void addCount(){
        synchronized (DataEntity.class){
            // todo...
        }
    }
}

synchronized
修饰代码块,比较经典的应用案例,就是单例设计模式中的双重校验锁实现。

public class Singleton {  

    private volatile static Singleton singleton;  
    
    private Singleton (){}  
    
    public static Singleton getSingleton() {  
        if (singleton == null) {  
            synchronized (Singleton.class) {  
                if (singleton == null) {  
                    singleton = new Singleton();  
                }  
            }  
        }  
        return singleton;  
    }  
}

采用代码块的实现方式,编程会更加灵活,可以显著的提升并发查询的效率。

四、synchronized 锁重入介绍

synchronized
关键字拥有锁重入的功能,所谓锁重入的意思就是:当一个线程得到一个对象锁后,再次请求此对象锁时可以再次得到该对象的锁,而无需等待。

我们看个例子就能明白。

public class DataEntity {

    private int count = 0;

    
    public synchronized void addCount1(){
        System.out.println(Thread.currentThread().getName() + ":" + (count++));
        addCount2();
    }

    public synchronized void addCount2(){
        System.out.println(Thread.currentThread().getName() + ":" + (count++));
        addCount3();
    }

    public synchronized void addCount3(){
        System.out.println(Thread.currentThread().getName() + ":" + (count++));

    }

    public int getCount() {
        return count;
    }
}
public class MyThreadA extends Thread {

    private DataEntity entity;

    public MyThreadA(DataEntity entity) {
        this.entity = entity;
    }

    @Override
    public void run() {
        entity.addCount1();
    }
}
public class MyThreadB extends Thread {

    private DataEntity entity;

    public MyThreadB(DataEntity entity) {
        this.entity = entity;
    }

    @Override
    public void run() {
        entity.addCount1();
    }
}
public class MyThreadTest {

    public static void main(String[] args) {
        // 初始化数据实体
        DataEntity entity = new DataEntity();

        MyThreadA threadA = new MyThreadA(entity);
        threadA.start();

        MyThreadB threadB = new MyThreadB(entity);
        threadB.start();


        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("result: " + entity.getCount());
    }
}

运行结果如下:

Thread-0:0
Thread-0:1
Thread-0:2
Thread-1:3
Thread-1:4
Thread-1:5
result: 6

从结果上看线程没有交替执行,线程
Thread-0
获取到锁之后,再次调用其它带有
synchronized
关键字的方法时,可以快速进入,而
Thread-1
线程需等待对象锁完全释放之后再获取,这就是锁重入。

五、小结

从上文中我们可以得知,在多线程环境下,恰当的使用
synchronized
关键字可以保证线程同步,使程序的运行结果与预期一致。

  • 1.当
    synchronized
    修饰一个方法时,作用的范围是整个方法,作用的对象是调用这个方法的对象;
  • 2..当
    synchronized
    修饰一个静态方法时,作用的范围是整个静态方法,作用的对象是这个类的所有对象;
  • 3.当
    synchronized
    修饰一个代码块时,作用的范围是代码块,作用的对象是修饰的内容,如果是类,则这个类的所有对象都会受到控制;如果是任意对象实例子,则控制的是具体的对象实例,谁拥有这个对象锁,就能进入方法体

synchronized
是一种同步锁,属于独占式,使用它进行线程同步,JVM 性能开销很大,大量的使用未必会带来好处。

关于更深入的原理知识,我们会在 JVM 系列中进行详解。文章内容难免有所遗漏,欢迎网友留言指出。

六、参考

1、
五月的仓颉 - synchronized锁机制

2、
你听 - 让你彻底理解Synchronized

引入依赖

<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-core</artifactId>
    <version>1.8.7</version>
</dependency>

基本用法

try (Entry entry = SphU.entry("HelloWorld")) {
    // 被保护的逻辑
    System.out.println("hello world");
} catch (BlockException ex) {
    // 处理被流控的逻辑
    System.out.println("blocked!");
}

接下来,阅读源码,我们从SphU.entry()开始

每个SphU#entry()将返回一个Entry。这个类维护了当前调用的一些信息:

  • createTime :这个entry的创建时间,用于响应时间统计
  • current Node :在当前上下文中的资源的统计
  • origin Node :原始节点的统计
  • ResourceWrapper :资源名称

CtSph#entryWithPriority()方法就是整个流控的基本流程:

1、首先,获取当前线程上下文,如果为空,则创建一个

2、然后,查找处理器链

3、最后,依次执行处理器

这是一个典型的责任链

接下来,挨个来看,首先看一下上下文。上下文是一个线程局部变量
ThreadLocal<Context>

如果当前线程还没有上下文,则创建一个

有了Context之后,接下来查找处理器

这些功能插槽(slot chain)有不同的职责:

  • NodeSelectorSlot :负责收集资源的路径,并将这些资源的调用路径,以树状结构存储起来,用于根据调用路径来限流降级;
  • ClusterBuilderSlot :用于存储资源的统计信息以及调用者信息,例如该资源的 RT, QPS, thread count 等等,这些信息将用作为多维度限流,降级的依据;
  • StatisticSlot :用于记录、统计不同纬度的 runtime 指标监控信息;
  • FlowSlot :用于根据预设的限流规则以及前面 slot 统计的状态,来进行流量控制;
  • AuthoritySlot :根据配置的黑白名单和调用来源信息,来做黑白名单控制;
  • DegradeSlot :通过统计信息以及预设的规则,来做熔断降级;
  • SystemSlot :通过系统的状态,例如 load1 等,来控制总的入口流量;

到这里为止,资源有了,上下文有了,处理器链有了,于是,接下来就可以对资源应用所有的处理器了

关于功能插槽的学习就先到这里,下面补充一个知识点:Node

Node 用于保存资源的实时统计信息

StatisticNode 保存三种实时统计指标:

  1. 秒级指标
  2. 分钟级指标
  3. 线程数

DefaultNode 用于保存特定上下文中特定资源名称的统计信息

EntranceNode 代表调用树的入口

总之一句话,Node是用于保存统计信息的。那么,这些指标数据是如何计数的呢?

Sentinel 使用
滑动窗口
实时记录和统计资源指标。ArrayMetric背后的滑动窗口基础结构是LeapArray。

下面重点看一下StatisticNode

StatisticNode是用于实时统计的处理器插槽。在进入这个槽位时,需要分别计算以下信息:

  • ClusterNode :该资源ID的集群节点统计信息总和
  • Origin node :来自不同调用者/起源的集群节点的统计信息
  • DefaultNode :特定上下文中特定资源名称的统计信息
  • 最后,是所有入口的总和统计

private int calculateTimeIdx(/*@Valid*/ long timeMillis) {
    long timeId = timeMillis / windowLengthInMs;
    // Calculate current index so we can map the timestamp to the leap array.
    return (int)(timeId % array.length());
}

protected long calculateWindowStart(/*@Valid*/ long timeMillis) {
    return timeMillis - timeMillis % windowLengthInMs;
}

/**
 * Get bucket item at provided timestamp.
 *
 * @param timeMillis a valid timestamp in milliseconds
 * @return current bucket item at provided timestamp if the time is valid; null if time is invalid
 */
public WindowWrap<T> currentWindow(long timeMillis) {
    if (timeMillis < 0) {
        return null;
    }

    int idx = calculateTimeIdx(timeMillis);
    // Calculate current bucket start time.
    long windowStart = calculateWindowStart(timeMillis);

    /*
     * Get bucket item at given time from the array.
     *
     * (1) Bucket is absent, then just create a new bucket and CAS update to circular array.
     * (2) Bucket is up-to-date, then just return the bucket.
     * (3) Bucket is deprecated, then reset current bucket.
     */
    while (true) {
        WindowWrap<T> old = array.get(idx);
        if (old == null) {
            /*
             *     B0       B1      B2    NULL      B4
             * ||_______|_______|_______|_______|_______||___
             * 200     400     600     800     1000    1200  timestamp
             *                             ^
             *                          time=888
             *            bucket is empty, so create new and update
             *
             * If the old bucket is absent, then we create a new bucket at {@code windowStart},
             * then try to update circular array via a CAS operation. Only one thread can
             * succeed to update, while other threads yield its time slice.
             */
            WindowWrap<T> window = new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
            if (array.compareAndSet(idx, null, window)) {
                // Successfully updated, return the created bucket.
                return window;
            } else {
                // Contention failed, the thread will yield its time slice to wait for bucket available.
                Thread.yield();
            }
        } else if (windowStart == old.windowStart()) {
            /*
             *     B0       B1      B2     B3      B4
             * ||_______|_______|_______|_______|_______||___
             * 200     400     600     800     1000    1200  timestamp
             *                             ^
             *                          time=888
             *            startTime of Bucket 3: 800, so it's up-to-date
             *
             * If current {@code windowStart} is equal to the start timestamp of old bucket,
             * that means the time is within the bucket, so directly return the bucket.
             */
            return old;
        } else if (windowStart > old.windowStart()) {
            /*
             *   (old)
             *             B0       B1      B2    NULL      B4
             * |_______||_______|_______|_______|_______|_______||___
             * ...    1200     1400    1600    1800    2000    2200  timestamp
             *                              ^
             *                           time=1676
             *          startTime of Bucket 2: 400, deprecated, should be reset
             *
             * If the start timestamp of old bucket is behind provided time, that means
             * the bucket is deprecated. We have to reset the bucket to current {@code windowStart}.
             * Note that the reset and clean-up operations are hard to be atomic,
             * so we need a update lock to guarantee the correctness of bucket update.
             *
             * The update lock is conditional (tiny scope) and will take effect only when
             * bucket is deprecated, so in most cases it won't lead to performance loss.
             */
            if (updateLock.tryLock()) {
                try {
                    // Successfully get the update lock, now we reset the bucket.
                    return resetWindowTo(old, windowStart);
                } finally {
                    updateLock.unlock();
                }
            } else {
                // Contention failed, the thread will yield its time slice to wait for bucket available.
                Thread.yield();
            }
        } else if (windowStart < old.windowStart()) {
            // Should not go through here, as the provided time is already behind.
            return new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
        }
    }
}

现在,有2个窗口,每个窗口500ms,2个窗口总共1000ms

假设,当前时间戳是1200ms,那么 (1200 / 500) % 2 = 0, 1200 - 1200 % 500 = 1000

这个时候,如果0这个位置没有窗口,则创建一个新的窗口,新窗口的窗口开始时间是1000ms

如果0这个位置有窗口,则继续判断旧窗口的窗口开始时间是否为1000ms,如果是,则表示窗口没有过期,直接返回该窗口。如果旧窗口的开始时间小于1000ms,则表示旧窗口过期了,于是重置旧窗口的统计数据,重新设置窗口开始时间(PS:相当于将窗口向后移动)

窗口(桶)数据保存在MetricBucket中

总结一下:

1、每个线程过来之后,创建上下文,然后依次经过各个功能插槽

2、每个资源都有自己的处理器链,也就是说多次访问同一个资源时,用的同一套处理器链(插槽)

3、Node相当于是一个载体,用于保存资源的实时统计信息

4、第一次进入插槽后,创建一个新Node,后面再补充Node的信息;第二次进入的时候,由于上下文的名称都是一样的,所以不会再创建Node,而是用之前的Node,也就是还是在之前的基础上记录统计信息。可以这样理解,每个DefaultNode就对应一个特定的资源。

5、StatisticNode中保存三种类型的指标数据:每秒的指标数据,每分钟的指标数据,线程数。

6、指标数据统计采用滑动窗口,利用当前时间戳和窗口长度计算数据应该落在哪个窗口数组区间,通过窗口开始时间判断窗口是否过期。实际数据保存在MetricBucket中

最后,千言万语汇聚成这张原理图

NodeSelectorSlot构造调用链路,ClusterBuilderSlot构造统计节点,StatisticSlot利用滑动窗口进行指标统计,然后是流量控制

参考文档

https://sentinelguard.io/zh-cn/docs/quick-start.html

https://sentinelguard.io/zh-cn/docs/basic-implementation.html

https://sentinelguard.io/zh-cn/docs/dashboard.html

https://blog.csdn.net/xiaolyuh123/article/details/107937353

https://www.cnblogs.com/magexi/p/13124870.html

https://www.cnblogs.com/mrxiaobai-wen/p/14212637.html

https://www.cnblogs.com/taromilk/p/11750962.html

https://www.cnblogs.com/taromilk/p/11751000.html

https://www.cnblogs.com/wekenyblog/p/17519276.html

https://javadoop.com/post/sentinel

https://www.cnblogs.com/cuzzz/p/17413429.html

前言

前不久.NET团队发布了.NET 9 的首个预览版,并且分享.NET团队对 .NET 9 的初步愿景,该愿景将于今年年底在 .NET Conf 2024 上发布。其中最重要的关注领域是:
云原生和智能应用开发

云原生开发人员平台

过去几年,.NET团队一直在构建强大的云原生基础,如运行时性能和应用监控。.NET 9 还将重点转向为流行的生产基础架构和服务提供铺平道路,例如在 Kubernetes 中运行,以及使用托管数据库和 Redis 等缓存服务。将在 .NET 协议栈的多个层面提供这些改进。所有这些功能都与 .NET Aspire 结合在一起,大大降低了构建云应用程序的成本和复杂性,缩短了开发与生产之间的距离。

云原生开发人员的工具

  • Visual Studio计划进行改进,以支持和增强我们的云平台、Native AOT、.NET Aspire 和 Azure 部署。
  • Visual Studio 和 Visual Studio Code 将为 .NET Aspire 提供新的开发和部署体验。这将包括配置组件、调试(包括热重载)AppHost 和子进程,以及与开发人员仪表板完全集成。开发人员将能够通过 Visual Studio、Visual Studio Code 和 Azure Developer CLI (azd) 将项目部署到 Azure 容器应用。

.NET和人工智能

展望.NET 9,致力于让.NET开发人员更轻松地将人工智能集成到他们现有的和新的应用程序中。开发人员将发现与 OpenAI 和开放源码软件模型(托管和本地)协同工作的优秀库和文档,将继续在 Semantic Kernel、OpenAI 和 Azure SDK 上开展合作,以确保 .NET 开发人员在构建智能应用程序时获得一流的体验。

下载体验.NET 9 Preview 1

参考文章

DotNetGuide技术社区交流群

  • DotNetGuide技术社区是一个面向.NET开发者的开源技术社区,旨在为开发者们提供全面的C#/.NET/.NET Core相关学习资料、技术分享和咨询、项目推荐、招聘资讯和解决问题的平台。
  • 在这个社区中,开发者们可以分享自己的技术文章、项目经验、遇到的疑难技术问题以及解决方案,并且还有机会结识志同道合的开发者。
  • 我们致力于构建一个积极向上、和谐友善的.NET技术交流平台,为广大.NET开发者带来更多的价值和成长机会。





欢迎加入DotNetGuide技术社区微信交流群