这一期我们分享一位开源开发者参与flux适配昇腾NPU的实践经验,欢迎广大开发者对
华为技术栈适配
进行讨论。

开源适配实践

flux是一个AI图像生成模型,有120亿参数量,具有大量的用户基础,可以根据命令行输入的文字去
生成对应的图片
。本次适配使用的flux模型权重文件是schnell版本。

下面我简单分享一下flux模型适配昇腾NPU的实践心得,如有改进之处,欢迎指正,也希望对华为生态感兴趣的小伙伴可以加入进来一起探讨。

如何验证开源项目

本次适配我需要在拥有
昇腾NPU的主机上将flux模型运行起来,验证其功能是否可以正常使用。昇腾NPU主机需要先安装CANN软件,CANN软件的安装可参考
昇腾社区
的开发资源。后续的所有操作都在此主机上进行。

在GitHub平台上下载项目源码(
点击链接下载
),通过分析README文件中的内容可以知道flux模型开发环境所需要的python版本是python3.10,其相关依赖都封装在pyproject.toml文件中。同时flux模型主要采用python语言进行开发,所以我使用anaconda管理模型环境。anaconda作为环境管理工具,可以为不同的模型创建独立的环境,这样可以避免依赖版本冲突,保持环境的整洁性。该主机需要的anaconda为Linux系统ARM64版,查找相关文档进行安装配置后,使用anaconda去创建属于flux的虚拟环境。

第一步:进入创建的conda环境,安装相关依赖

在代码仓库中,是由pyproject.toml文件去管理的依赖,使用【pip install -e ".[all]"】命令下载依赖。我们下载好flux模型所需要的相关依赖之后,需要安装对应版本的torch_npu,使pytorch框架可以运行在昇腾NPU上,这样flux模型所需要的环境就搭建完成了。torch_npu是华为为昇腾NPU设计的pytorch后端库,使得pytorch框架能够在昇腾NPU上运行,是
连接pytorch框架与昇腾NPU的桥梁。

第二步:准备模型权重文件

我先运行了一次模型,发现权重文件在运行时会自动从Hugging Face上下载,由于模型权重文件较大,
且每次运行都会重新下载
,比较占用内存。所以我们提前下载模型权重文件到主机上。

分析代码文件,涉及到的相关模型权重有以下3个:

t5:文本转换为机器理解的语言

clip:文本转换为图像

flux:图像生成

第三步:编写运行代码

flux模型权重文件下载完成后,就可以修改相关代码,实现模型权重文件从本地加载。

在之前的代码逻辑里面,判断加载哪一个模型是基于模型的名称去进行判断的,但是我们现在传入的参数是路径,这个方法不适用,需要修改代码,直接说明加载的模型是T5或者CLIP。

然后运行模型,报错提示设备为CUDA,但我的设备是NPU。根据这个报错信息,我进行了对应的修改,将device="CUDA",改为device="NPU"

再次运行之后报错提示bfloat16不支持在这个设备上,我根据这个报错信息,找到bfloat16的位置,修改为float32

进行这些修改之后,运行模型,查看NPU已经进入了运行状态。

测试结果:

经过测试,模型可以通过输入的内容,去生成对应的图片,所以模型的功能在此主机上可以正常使用了。

期待各位小伙伴的加入,一起体验适配带来的乐趣。在体验过程中,如有问题可
点击链接
进入开源开发者专属问答区,加入开源开发者专项计划。加入我们,您可以在项目中提 Issues与其他开发者进行互动,也可以添加项目相关的微信群进行技术讨论与交流。

沃土云创开源开发者专项计划是华为给开源开发者提供专属激励资源,鼓励开发者积极参与开源 for Huawei适配,践行“让优秀开发者支持更优秀开发者”的理念。

点击关注,第一时间了解华为云新鲜技术~

基于AI底座的数智油气田参考架构
Architecture for Intelligent & Digital Oilfileds Based-on AI
王权 2024.12.29

2024年12月29日,在石油圈-能源说线上讲座中,王权首次提出“基于AI底座的数智油气田参考架构”。该架构可视为其于2003年提出的“数字油田参考架构”的升级版。

大系统观哲学思想

新的基于AI底座的数智油气田参考架构更加突出了大系统思维,建立了主客体互动的主谓宾格清晰的大系统逻辑哲学范式:

  • 主格/主体
    :油气田企业,以及人,作为系统主体,承载着系统的使命——企业再造——数字化转型与智能化发展;
  • 宾格/客体
    :油气田企业实体、AI智能体,自下而上的数据资源、信息资源、知识资源、智力资源,以及数字孪生体和虚拟员工;
  • 谓格/工具
    :主体操作客体所使用的算力服务、工具服务、模型服务、代理服务,以及全面支持主体行为的全息智能生态。

架构层次

基于AI底座的数智油气田参考架构共分为7层,分别是基础层、访问层、模型层、代理层、全息层、协作层和使命层。

  • 基础层

这是架构的底层,包括了各种数据资源和计算资源。数据资源涵盖了油气田运营中,由各种设备和仪器以及人工采集的、各种形式的原始数据,包括各类井站场的各种动静态与实时时序数据,如地震数据、钻井数据、录井数据、测井数据、压裂数据、测试数据、生产数据、QHSE(质量、健康、安全、环境)数据、研究数据和运营数据等。这些数据通过现场物联网、生产网和办公网传输到相应位置。计算资源则包括算力中心(云端的通用计算、专业软件高性能计算、人工智能专用计算,以及存储和网络传输能力)、算法中心、数据中心、软件中心、运维中心以及边缘计算等。

  • 访问层

这一层提供了对基础层资源的访问接口,包括专业软件(如MIS、ERP、MES、OA等),以及各种App、中间件和其他工具服务。这些服务调用数据湖、数据仓库、数据港口中的信息资源,完成指定任务。根据产权所属,数据可分为私有和公有等种类;根据存储方式,数据可分为结构化和非结构化,以及关系型数据库、向量数据库、全文检索数据库等。不同的数据既是系统必须面对的客观存在,也是完成各种服务任务的保障。

  • 模型层

在这一层,通过模型服务为上层调用提供响应。需要构建由业务模型、专业模型和基础/行业模型组成的多层次模型体系。首先要根据油气田自身情况采用国内外的或开源或闭源的基础大模型,建立本领域或行业的大模型;然后,基于基础/行业模型,建立满足不同专业需求的专业模型,还要建立油藏、井筒、管网、工程等各方面的面向具体技术节点的模型;最后,还要建立直接面向业务的模型,为科研、生产、管理、决策等提供服务。这些大小模型通过调用自然语言处理、视觉、知识图谱、推理、扩散、创新、对齐、机器学习、空间智能、大数据工具以及其他各种模型和算法,利用知识资源实现任务目标。

  • 代理层

这一层由智能体等为人提供代理服务。处在不同层级、具有不同功能、角色的众多智能体将链接成为组群,组成智能体协作网,实现了智能体、助手、具身智能等相互之间的智能化联动和合作。人通过连接一个或少数智能体,就可以获得期望的结果。如果所连接的智能体无法满足人提出的需求,那它将通过自动网关或路由找到能够解决问题的其他智能体,并请求完成相关任务,在全部工作完成后,与人连接的智能体将把任务执行结果反馈到人。

  • 全息层

在这一层,建立一个基于油气田全息数字孪生体的信息通畅、数据及时准确、用户界面自然友好的全息智能生态。通过全息智能生态,人可以和人工智能体——虚拟员工,形成主从关系的协作联合体,以人的思维驱动的意志为主,以人工智能体的系统意志为辅,掌握和运用好油气田生产科研、经营管理和战略决策的全息,实现对油气田全生命周期的高效智能管理。

  • 协作层

在这一层,实现人机共治。人与AI共同管理油气田,人工智能全面支撑和共同运营油气田各级的指挥中心、决策中心、监控中心、研究中心。必须保障人作为油气田运营的最重要参与者和最终决策者。

  • 使命层

这是架构的顶层,代表了企业的战略和使命,是整个架构的指导思想和最终目标。数字化转型和智能化发展,就是企业系统的一次再造过程,是企业系统结构的重大调整和改变。领导、执行和参与这次企业再造的每一个人都需要具备大系统观。

应用层更换Linux机器开机启动LOGO

平台开机Logo默认是编译进内核的,更换起来很不方便,通过改写内核源码,可以实现应用层直接更换内核Logo。

1.uboot相关修改

网上教程一般会这么改

"loadlogo=mmc read 0x15000000 0x5800 0x2800;\0" \
"run loadlogo; " \

核心代码是
mmc read 0x15000000 0x5800 0x2800;
这个命令的作用是从MMC或SD卡的第22528(0x5800)个扇区开始,读取10240(0x2800)个扇区的数据,并将这些数据存储到内存地址0x15000000处。这种操作通常用于将存储设备上的数据加载到内存中,以便进行进一步的处理或执行。
目的是将图片文件放到0x15000000处,用于后面logo.c文件中使用

但经过实践发现,这么改会存在
"run loadlogo; "
未执行的情况,从而导致LOGO更换不上。
故选择将最后一段改成:
"else run netboot; fi; mmc read 0x15000000 0x5800 0x2800;"
解决。(可能还需要手动进uboot命令行,将bootcmd重置成默认)

2.kernel内核文件修改

修改drivers/video/logo/Kconfig文件
在最后新加一个LOGO_LINUX_CUSTOM_CLUT224的配置选项:

/* 在如下配置 */
config  LOGO_LINUX_CUSTOM_CLUT224
    bool "Custom 224-color linux logo"
    default y
endif # LOGO

修改drivers/video/logo/logo.c文件
到这一步可以有两种修改办法,这取决于后续LOGO图片是怎么处理的
首先在头部增加如下代码

/* logo's are marked __initdata. Use __init_refok to tell
 * modpost that it is intended that this function uses data
 * marked __initdata.
 */

#ifdef CONFIG_LOGO_LINUX_CUSTOM_CLUT224  //新增
#define IMAGE_HEAD_SIZE    	54
#define IMAGE_MAX_PIXEL    	2048000    	
#define IMAGE_MAX_SIZE (IMAGE_HEAD_SIZE+IMAGE_MAX_PIXEL+1024)
#define COLOR_MAX_NUM    	224        	/* linux logo just support 224 colors     		*/
#define IMAGE_MTD_NUM    	4        	/* the logo saved in MTD4 						*/
#define IMAGE_OFFSET    	0x0			/* the logo's offset int the mtd area     		*/
volatile static unsigned char* remapped_area;
static unsigned char logo_flash_clut224_data[IMAGE_MAX_PIXEL] __initdata = {0};
static unsigned char logo_flash_clut224_clut[COLOR_MAX_NUM * 3] __initdata = {0};
static struct linux_logo logo_flash_clut224 __initdata =
{
	.type      = LINUX_LOGO_CLUT224,
	.width     = 0,
	.height    = 0,
	.clutsize  = 0,
	.clut      = logo_flash_clut224_clut,
	.data      = logo_flash_clut224_data,
};
#endif

const struct linux_logo * __init_refok fb_find_logo(int depth)
{
	const struct linux_logo *logo = NULL;

	if (nologo)...

后续分成:
LINUX上处理图片

WINDOWS上处理图片
如果图片在
Linux
上处理,在
if (depth >= 8)
做如下修改:

if (depth >= 8)
	{
#ifdef CONFIG_LOGO_LINUX_CUSTOM_CLUT224
		unsigned char head[60] = {0};
		unsigned char *image = NULL;
		unsigned char *clut = NULL;
		unsigned char *data = NULL;
		
		int clutsize = 0;
		int size      = 0;
		int offset    = 0;
		int width     = 0;
		int height    = 0;
		int count     = 0;
		int compress  = 0;
		int sizeimage = 0;
		int clrused   = 0;

		int i  = 0;
		int j  = 0;
		int fi = 0;
		int li = 0;
		
		unsigned int real_width = 0;
		unsigned int logo_index = 0;

		remapped_area = __phys_to_virt(0x15000000);
		memcpy(head, remapped_area, 58);
		printk("image head:%c%c\n", head[0], head[1]);

		if(head[0] == 'K' && head[1] == 'I'&& head[2] == 'K')
		{
			printk("right ppm image head");
			memcpy( &logo_flash_clut224.width, remapped_area+3, 4);
			memcpy( &logo_flash_clut224.height, remapped_area + 4+3, 4);
			memcpy( &logo_flash_clut224.clutsize, remapped_area + 8+3, 4);
			memcpy( logo_flash_clut224.data, remapped_area + 15, logo_flash_clut224.width * logo_flash_clut224.height);
			memcpy( logo_flash_clut224.clut, remapped_area + 15 + (logo_flash_clut224.width * logo_flash_clut224.height), logo_flash_clut224.clutsize * 3);
			logo=&logo_flash_clut224;
		}
		else
		{
#ifdef CONFIG_LOGO_LINUX_CLUT224
			printk("LINUX_clut224\n");
			logo = &logo_linux_clut224;
#endif
		}
#endif

如果选择在
Windows
上处理LOGO图片,代码改成

#ifdef CONFIG_LOGO_LINUX_CUSTOM_CLUT224
		unsigned char head[60] = {0};
		unsigned char *image = NULL;
		unsigned char *clut = NULL;
		unsigned char *data = NULL;
		
		int clutsize = 0;
		int size      = 0;
		int offset    = 0;
		int width     = 0;
		int height    = 0;
		int count     = 0;
		int compress  = 0;
		int sizeimage = 0;
		int clrused   = 0;

		int i  = 0;
		int j  = 0;
		int fi = 0;
		int li = 0;
		
		unsigned int real_width = 0;
		unsigned int logo_index = 0;

		remapped_area = __phys_to_virt(0x15000000);
		memcpy(head, remapped_area, 58);
		printk(" %s-%d:right image head:%c%c\n", __FILE__, __LINE__, head[0], head[1]);

		if(1)
		{
			printk("right ppm image head");
			memcpy( &logo_flash_clut224.width, remapped_area+4, 4);
			memcpy( &logo_flash_clut224.height, remapped_area + 8, 4);
			memcpy( &logo_flash_clut224.clutsize, remapped_area + 12, 4);
			memcpy( logo_flash_clut224.data, remapped_area + 16, logo_flash_clut224.width * logo_flash_clut224.height);
			memcpy( logo_flash_clut224.clut, remapped_area + 16 + (logo_flash_clut224.width * logo_flash_clut224.height), logo_flash_clut224.clutsize * 3);
			logo=&logo_flash_clut224;
		}
		else
		{
#ifdef CONFIG_LOGO_LINUX_CLUT224
			printk("LINUX_clut224\n");
			logo = &logo_linux_clut224;
#endif
		}
#endif

没错,就是因为后续图片格式会有所不一样,读取图片信息的方式也不同。

之后在然后重新编译内核。配置内核时,需要去掉logo路径下的其它选项,只保留Custom 224-color linux logo这一个选项。

3.LOGO图片修改

因为Logo图片中所使用的颜色数不能超过224种,如果超过将无法正常显示,因此我们需要制作符合要求的图像。

Linux
环境下制作:
先下载附件
res

Windows环境
下制作:方法是我自己寻找制作的,因为有这种需求,安装包放在云盘中
链接:
https://pan.baidu.com/s/1Yn7BtTh4GtVqoS-cc2nU8Q?pwd=1234
提取码:1234
按照文件中readme.txt一步步执行即可

两者都是为了把图片制作成mylogo.bin提供给logo.c解读,使LOGO修改生效
你用哪种方式,上述logo.c就改成哪种就行。

4.去除linux的开机光标

光标会导致LOGO图片显示时,左上角有黑块,方法如下:

在内核的当前目录进入到drivers/video/console/fbcon.c文件
将static void fb_flashcursor(void *private) 和 static void fbcon_cursor(struct vc_data *vc, int mode) 里的内容注释掉,使之变成空函数

我的建议是在这两个函数开头直接return;就行

参考连接:
如何修改 ARM Linux 系统的启动画面
去除linux的开机光标
应用层更换内核启动logo

一、介绍
今天是这个系列《C++之 Opencv 入门到提高》的第七篇文章。这篇文章也不难,介绍如何调整图像的亮度。图像本质上也是数据,既然是数据,我们就可以针对图像的每个像素进行增加或者减少的的操作,这样的结果就是图片变暗或者变亮了。这个调整的过程不是随便操作、无限制的,因为有些结果并不是我们想要的,我们为了让图片每个像素变化的更合理,就可以使用【saturate_cast】函数进行控制,好好的体会一下吧。这都是基础,为以后的学习做好铺垫。虽然操作很简单,但是背后有很多东西需要我们深究,才能做到知其然知其所以然。OpenCV 具体的简介内容,我就不多说了,网上很多,大家可以自行脑补。
OpenCV 的官网地址:
https://opencv.org/
,组件下载地址:
https://opencv.org/releases/

OpenCV 官网学习网站:
https://docs.opencv.ac.cn/4.10.0/index.html

我需要进行说明,以防大家不清楚,具体情况我已经罗列出来。
操作系统:Windows Professional 10(64位)
开发组件:OpenCV – 4.10.0
开发工具:Microsoft Visual Studio Community 2022 (64 位) - Current版本 17.8.3
开发语言:C++(VC16)

二、知识学习
接口很简单,不用多说,仔细研究一下原理更有用。

1 #include <opencv2/opencv.hpp>
2 #include <iostream>
3 #include <math.h>
4 
5 using namespacestd;6 using namespacecv;7 
8 
9 intmain()10 {11     //图像变换可以看做如下:12     //1、像素变换,也就是点操作,修改像素的值,获取像素的值,调整图像的亮度和对比度等类似操作。13     //2、领域操作,也是区域操作。可以做图像的卷积,图像特征的提取,图像梯度的计算,也可以对图像进行深度处理,比如:角点检测,模糊处理,平滑处理。14     //
15     //调整图像亮度和对比度属于像素变换,也就是点操作。16     //g(i,j)=af(i,j)+b,其中 a >0 ,b 是增益变量。f(i,j)表示输入图像的每个像素点,g(i,j)表示输出图像的每个像素点。17     //a > 0,对于常规图像来说必须大于零,因为 opencv 是基于 RGB 格式来处理图像的,它的值是【0-255】,所以必须大于零。如果是 tiff 格式的图像,这个值就有可能是负值。18     //a 这个参数的作用就是用于提高对比度的,它是乘法,是倍数增加的,所以,新的图像像素值就会比原始图像像素值有更大的变化,效果就是对比度提高了。19     //b 就是提高图像的亮度,越亮,值越接近 255,也就是越接近白色。说白了如果我们想提高图像的亮度,就要提高 b(增益变量)的值。20 
21 
22     //2、重要API23     //Mat image=Mat::zeros(src.size(),src.type())创建一张和原图一项大小和类型的空白图像,像素初始值是0.24     //saturate_cast<uchar>(value)确保值在【0-255】之间。25     //Mat.at<Vec3b>(x,y)[index]=value 给指定通道每个像素赋值。26 
27 
28     //3、代码演示
29 
30 Mat src, dst;31     src = imread("D:\\360MoveData\\Users\\Administrator\\Desktop\\TestImage\\3.jpg", IMREAD_COLOR);32     if(src.empty())33 {34         cout << "图像加载失败!!!" <<endl;35         return    -1;36 }37 
38     namedWindow("原始图像", WINDOW_AUTOSIZE);39     imshow("原始图像", src);40 
41     int width =src.cols;42     int height =src.rows;43     int channels =src.channels();44 
45     dst =Mat::zeros(src.size(),src.type());46     double alpha = 2;47     double beta = 40;48     for (int row = 0; row < height; row++)49 {50         for (int col = 0; col < width; col++)51 {52             //RGB 三通道的
53             if (channels == 3)54 {55                 float b = src.at<Vec3b>(row, col)[0];56                 float g = src.at<Vec3b>(row, col)[1];57                 float r = src.at<Vec3b>(row, col)[2];58 
59                 dst.at<Vec3b>(row, col)[0] = saturate_cast<uchar>(alpha * b +beta);60                 dst.at<Vec3b>(row, col)[1] = saturate_cast<uchar>(alpha * g +beta);61                 dst.at<Vec3b>(row, col)[2] = saturate_cast<uchar>(alpha * r +beta);62 }63             else//Gray 单通道的
64 {65                 float v = src.at<uchar>(row, col);66                 dst.at<uchar>(row, col) = saturate_cast<uchar>(alpha * v +beta);67 }68 }69 }70 
71     namedWindow("对比度图像",WINDOW_AUTOSIZE);72     imshow("对比度图像",dst);73 
74     waitKey(0);75     return 0;76 }

图像亮度调整效果的对比:

调整后的效果:

效果很明显。


三、总结
这是 C++ 使用 OpenCV 的第七篇文章,其实也没那么难,操作很简单,函数也不难理解,最重要是活学活用。初见成效,继续努力。皇天不负有心人,不忘初心,继续努力,做自己喜欢做的,开心就好。

前言

分库分表是解决单库单表性能瓶颈的有效手段,但也会引入新的复杂性和技术挑战。

这篇文章跟大家一起聊聊,分库分表后带来的7个问题,以及相关的解决方案,希望对你会有所帮助。

(我最近开源了一个基于 SpringBoot+Vue+uniapp 的商城项目,欢迎访问和star。)[
https://gitee.com/dvsusan/susan_mall
]

1. 全局唯一 ID 问题

问题描述

在分库分表后,每张表的自增 ID 只在本表范围内唯一,但无法保证全局唯一。

例如:

  • 订单表_1
    的主键从 1 开始,
    订单表_2
    的主键也从 1 开始。
  • 在需要全局唯一 ID 的场景(如订单号、用户 ID)中会发生冲突。

解决方案

1.1 使用分布式 ID 生成器

推荐工具:
  • Snowflake
    :Twitter 开源的分布式 ID 算法。
  • 百度 UidGenerator
    :基于 Snowflake 的改进版。
  • Leaf
    :美团开源,号段模式和 Snowflake 双支持。
代码示例:Snowflake 算法
public class SnowflakeIdGenerator {
    private final long epoch = 1622476800000L; // 自定义时间戳
    private final long workerIdBits = 5L; // 机器ID
    private final long datacenterIdBits = 5L; // 数据中心ID
    private final long sequenceBits = 12L; // 序列号

    private final long maxWorkerId = ~(-1L << workerIdBits);
    private final long maxDatacenterId = ~(-1L << datacenterIdBits);
    private final long sequenceMask = ~(-1L << sequenceBits);

    private long workerId;
    private long datacenterId;
    private long sequence = 0L;
    private long lastTimestamp = -1L;

    public SnowflakeIdGenerator(long workerId, long datacenterId) {
        if (workerId > maxWorkerId || workerId < 0) throw new IllegalArgumentException("Worker ID out of range");
        if (datacenterId > maxDatacenterId || datacenterId < 0) throw new IllegalArgumentException("Datacenter ID out of range");
        this.workerId = workerId;
        this.datacenterId = datacenterId;
    }

    public synchronized long nextId() {
        long timestamp = System.currentTimeMillis();
        if (timestamp < lastTimestamp) throw new RuntimeException("Clock moved backwards");

        if (timestamp == lastTimestamp) {
            sequence = (sequence + 1) & sequenceMask;
            if (sequence == 0) timestamp = waitNextMillis(lastTimestamp);
        } else sequence = 0L;

        lastTimestamp = timestamp;
        return ((timestamp - epoch) << (workerIdBits + datacenterIdBits + sequenceBits))
                | (datacenterId << (workerIdBits + sequenceBits))
                | (workerId << sequenceBits)
                | sequence;
    }

    private long waitNextMillis(long lastTimestamp) {
        long timestamp = System.currentTimeMillis();
        while (timestamp <= lastTimestamp) timestamp = System.currentTimeMillis();
        return timestamp;
    }
}

1.2 数据库号段分配

  • 原理
    :维护一个独立的
    global_id
    表,分库按步长分配 ID:
    • 库 1:ID 步长为 2,从 1 开始(1, 3, 5...)。
    • 库 2:ID 步长为 2,从 2 开始(2, 4, 6...)。
示例
CREATE TABLE global_id (
    id INT PRIMARY KEY AUTO_INCREMENT,
    stub CHAR(1) NOT NULL UNIQUE
);
-- 步长设置:
SET @@auto_increment_increment = 2;
SET @@auto_increment_offset = 1;

2. 跨库跨表查询复杂性

问题描述

分库分表后,聚合查询(如总数统计、分页查询)需要跨多个分片表执行,增加了查询复杂度。

例如:

  • 查询所有订单总数,需要跨 10 个订单表聚合。
  • 按创建时间分页查询所有订单。

解决方案

2.1 使用中间件(推荐)

  • ShardingSphere

    MyCAT
    :支持 SQL 分片执行和结果合并。
  • 优点:业务代码无需修改,中间件完成分库分表逻辑。

2.2 手动分片查询

  • 按分片逐一查询数据,在业务层合并结果。
示例代码:聚合查询
public int countAllOrders() {
    int total = 0;
    for (String db : List.of("db1", "db2", "db3")) {
        String sql = "SELECT COUNT(*) FROM " + db + ".orders";
        total += jdbcTemplate.queryForObject(sql, Integer.class);
    }
    return total;
}
示例代码:跨分片分页查询
public List<Order> paginateOrders(int page, int size) {
    List<Order> allOrders = new ArrayList<>();
    for (String table : List.of("orders_1", "orders_2")) {
        String sql = "SELECT * FROM " + table + " LIMIT 100";
        allOrders.addAll(jdbcTemplate.query(sql, new OrderRowMapper()));
    }
    allOrders.sort(Comparator.comparing(Order::getCreatedAt));
    return allOrders.stream()
            .skip((page - 1) * size)
            .limit(size)
            .collect(Collectors.toList());
}

手动分片查询的方案,如果数据比较多,性能会比较差。

3. 分布式事务问题

问题描述

分布式事务(如订单表在库 A,库存表在库 B)无法使用单库事务,导致可能会出现数据的一致性问题。

解决方案

3.1 分布式事务框架

  • Seata
    :支持跨库的分布式事务。
  • 示例代码
@GlobalTransactional
public void createOrder(Order order) {
    orderService.saveOrder(order); // 写入库A
    stockService.reduceStock(order.getProductId()); // 更新库B
}

3.2 柔性事务

  • 使用消息中间件实现最终一致性。
  • 典型实现:
    RocketMQ 消息事务

4. 分片键设计问题

问题描述

分片键选择不当可能导致数据倾斜(热点问题)或查询路由效率低。

解决方案

4.1 分片键设计原则

  1. 数据分布均匀
    :避免热点问题。
  2. 常用查询字段
    :尽量选高频查询字段。

4.2 路由表

  • 维护全局路由表,映射分片键到分表。
示例代码:路由表查询
public String getTargetTable(int userId) {
    String sql = "SELECT table_name FROM routing_table WHERE user_id = ?";
    return jdbcTemplate.queryForObject(sql, new Object[]{userId}, String.class);
}

5. 数据迁移问题

问题描述

扩容(如从 4 个分片扩展到 8 个分片)时,旧数据需要迁移到新分片,迁移复杂且可能影响线上服务。

解决方案

5.1 双写策略

  • 数据迁移期间,旧表和新表同时写入。
  • 待迁移完成后,切换到新表。

5.2 增量同步

  • 使用
    Canal
    监听 MySQL Binlog,将数据迁移到新分片。
示例:Canal 配置
canal.destinations:
  example:
    mysql:
      hostname: localhost
      port: 3306
      username: root
      password: password
    kafka:
      servers: localhost:9092
      topic: example_topic

6. 分页查询问题

问题描述

分页查询需要从多个分片表合并数据,再统一分页,逻辑复杂度增加。

解决方案

  1. 各分片分页后合并
    :先按分片分页查询,业务层合并排序后分页。
  2. 中间件支持分页
    :如 ShardingSphere。
示例代码:跨分片分页
public List<Order> queryPagedOrders(int page, int size) {
    List<Order> results = new ArrayList<>();
    for (String table : List.of("orders_1", "orders_2")) {
        results.addAll(jdbcTemplate.query("SELECT * FROM " + table + " LIMIT 100", new OrderRowMapper()));
    }
    results.sort(Comparator.comparing(Order::getCreatedAt));
    return results.stream().skip((page - 1) * size).limit(size).collect(Collectors.toList());
}

但如果分的表太多,可能会有内存占用过多的问题,需要做好控制。

7. 运维复杂性

问题描述

分库分表后,运维难度增加:

  • 数据库实例多,监控和备份复杂。
  • 故障排查需要跨多个库。

解决方案

  1. 自动化运维平台
    :如阿里云 DMS。
  2. 监控工具
    :使用 Prometheus + Grafana 实现分片监控。

总结

分库分表本质上是“性能换复杂度”,它虽然能有效提升系统的性能和扩展性,但问题也随之而来。

分库分表后带来的问题总结如下:

问题 解决方案
全局唯一 ID 雪花算法、号段分配、Leaf
跨库跨表查询 中间件支持(如 ShardingSphere)或手动合并
分布式事务 分布式事务框架(Seata)、消息最终一致性
分片键设计问题 路由表或高效分片键
数据迁移问题 双写策略或增量同步(如 Canal)
分页查询问题 分片查询后合并排序
运维复杂性 自动化工具(DMS)、监控工具(Prometheus + Grafana)

应根据业务场景选择适合的分库分表策略,并通过工具和技术方案,解决由此带来的一些问题,最终实现系统的高性能与高可靠性。

最后说一句(求关注,别白嫖我)
如果这篇文章对您有所帮助,或者有所启发的话,帮忙关注一下我的同名公众号:苏三说技术,您的支持是我坚持写作最大的动力。

求一键三连:点赞、转发、在看。

关注公众号:【苏三说技术】,在公众号中回复:进大厂,可以免费获取我最近整理的10万字的面试宝典,好多小伙伴靠这个宝典拿到了多家大厂的offer。