17、Canal监听MySQL-Binlog实现数据监听

news/2025/10/31 10:42:38/文章来源:https://www.cnblogs.com/Iven-L/p/19178290

17、Canal监听MySQL-Binlog实现数据监听

一、Canal简介:

Canal 是阿里巴巴开源的一款基于数据库增量日志解析的中间件,主要用于实现数据库变更数据的实时同步。

c8800bf8-5e5c-433c-a111-2a76009a6e59

Canal源码

 

二、工作原理:

1、MySQL主备复制原理:

 44171535-bdc3-4e49-8207-a7682fbcaf2c

(1)、MySQL master 将数据变更写入二进制日志( binary log, 其中记录叫做二进制日志事件binary log events,可以通过 show binlog events 进行查看)

(2)、MySQL slave 将 master 的 binary log events 拷贝到它的中继日志(relay log)

(3)、MySQL slave 重放 relay log 中事件,将数据变更反映它自己的数据

2、canal工作原理:

(1)、canal 模拟 MySQL slave 的交互协议,伪装自己为 MySQL slave ,向 MySQL master 发送dump 协议

(2)、MySQL master 收到 dump 请求,开始推送 binary log 给 slave (即 canal )

(3)、canal 解析 binary log 对象(原始为 byte 流)

 

三、MySQL 配置(开启 Binlog):

1、开启 Binlog(ROW 模式):

# MySQL 配置文件
# Linux:my.cnf配置文件(/etc/mysql/)
# Window:my.ini配置文件(C:\ProgramData\MySQL\MySQL Server 5.7\)
# 开启 Binlog
log_bin = mysql-bin# 选择 ROW 模式(记录行级变更)
binlog-format = ROW# 配置数据库唯一 ID(与 Canal 服务端的 slaveId 不同)
server-id = 1

1

2

2、重启 MySQL 并验证:

# 打开命令提示符(cmd/services.msc):
# 按 Win + R 键,输入 cmd,然后按 Enter 键打开命令提示符窗口。
# 停止MySQL服务:
net stop MySQL57# 启动MySQL服务:
net start MySQL57# 验证
SHOW VARIABLES LIKE 'log_bin';
SHOW VARIABLES LIKE 'binlog_format';

3

4

3、创建 Canal 专用账号(权限最小化):

-- 1. 创建支持远程连接的用户(% 表示任意 IP)
-- CREATE USER 'canal'@'%' IDENTIFIED BY 'canal';
-- 授予权限
-- GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%';-- 2. 创建支持本地连接的用户(localhost)
CREATE USER 'canal'@'localhost' IDENTIFIED BY 'canal';-- 授予相同权限
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'localhost';-- 刷新权限,使配置生效
FLUSH PRIVILEGES;

 

四、Canal 服务端配置:

1、下载并解压 Canal 服务端:

github-canal包

3e125b24-cfe6-426d-b7df-6f0341c1a31a

2、配置 Canal 实例:

(1)、instance.properties配置:

# MySQL 主库地址(Canal 连接的 MySQL 地址)
canal.instance.master.address=127.0.0.1:3306# MySQL 账号密码
canal.instance.dbUsername=canal
canal.instance.dbPassword=canal

5

(2)、windows启动 Canal 服务端:

1)、双击启动bin/startup.bat:

 6

2)、存在黑屏闪退,修改bin/startup.bat,重启:

7

3)、日志:

 8

9

10

11

 

五、SpringBoot整合Canal实现MySQL数据监听:

1、POM配置:

        <dependency><groupId>com.alibaba.otter</groupId><artifactId>canal.client</artifactId><version>1.1.8</version></dependency><dependency><groupId>com.alibaba.otter</groupId><artifactId>canal.protocol</artifactId><version>1.1.8</version></dependency>

2、YML配置:

canal:# 自动启动同步标志位auto-sync: trueinstances:# 第一个实例instance1:host: 127.0.0.1port: 11111# canal server 中配置的实例名(canal.destinations = example)name: example# 批量拉取条数batch-size: 100# 无数据时休眠时间(ms)sleep-time: 1000

3、Entity类声明:

CanalProperties.class

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;/*** Canal配置属性类(映射YAML配置)*/
@Data
@Component
@ConfigurationProperties(prefix = "canal")
public class CanalProperties {// 是否自动启动同步private boolean autoSync = true;// 多实例配置private Map<String, InstanceConfig> instances = new HashMap<>();@Datapublic static class InstanceConfig {private String host;private Integer port;private String name;private Integer batchSize = 100;private Integer sleepTime = 1000;}}

DataEventTypeEnum.enum

import org.springframework.util.StringUtils;
import java.util.Arrays;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;public enum DataEventTypeEnum {INSERT("INSERT"),UPDATE("UPDATE"),DELETE("DELETE");private final String name;DataEventTypeEnum(String name) {this.name = name;}public String NAME() {return name;}private static final Map<String, DataEventTypeEnum> NAME_MAP =Arrays.stream(DataEventTypeEnum.values()).collect(Collectors.toMap(DataEventTypeEnum::NAME, Function.identity()));public static DataEventTypeEnum getEnum(String name) {if (!StringUtils.hasText(name)) {return null;}return NAME_MAP.get(name);}
}

JsonMessageType.class

import lombok.Data;@Data
public class JsonMessageType {/*** 库名*/private String schemaName;/*** 表名*/private String tableName;/*** 事件类型* (INSERT/UPDATE/DELETE)*/private String eventType;/*** 数据JSON字符串*/private String data;}

4、CanalRunnerAutoConfig启动Canal配置:

import com.iven.canal.entity.CanalProperties;
import com.iven.canal.handle.CanalWorkRegistry;
import com.iven.canal.utils.JsonMessageParser;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;/*** Canal自动配置*/
@Slf4j
@Configuration
@RequiredArgsConstructor
public class CanalRunnerAutoConfig {private final CanalProperties canalProperties;private final JsonMessageParser jsonMessageParser;private final CanalWorkRegistry workRegistry;@Beanpublic ApplicationRunner canalApplicationRunner() {return args -> {if (!canalProperties.isAutoSync()) {log.info("Canal自动同步已关闭");return;}// 如果没有任何Work,则不启动Canalif (!workRegistry.hasWork()) {log.info("无表同步处理器,不启动Canal");return;}// 启动所有配置的Canal实例canalProperties.getInstances().forEach((instanceKey, config) -> {CanalRunner runner = new CanalRunner(config.getHost(),config.getPort(),config.getName(),config.getBatchSize(),config.getSleepTime(),jsonMessageParser,workRegistry);runner.start();});};}
}

5、CanalRunner拉取数据:

import com.alibaba.fastjson2.JSON;
import com.alibaba.otter.canal.client.CanalConnector;
import com.alibaba.otter.canal.client.CanalConnectors;
import com.alibaba.otter.canal.protocol.CanalEntry;
import com.alibaba.otter.canal.protocol.Message;
import com.iven.canal.entity.JsonMessageType;
import com.iven.canal.handle.CanalWork;
import com.iven.canal.handle.CanalWorkRegistry;
import com.iven.canal.utils.JsonMessageParser;
import lombok.extern.slf4j.Slf4j;
import java.net.InetSocketAddress;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;/*** Canal运行器* 手动管理生命周期** 1、启动Canal实例* 2、处理解析后的数据*/
@Slf4j
public class CanalRunner {private Thread thread;private final String canalIp;private final Integer canalPort;private final String canalInstance;private final Integer batchSize;private final Integer sleepTime;private final JsonMessageParser jsonMessageParser;private final CanalWorkRegistry workRegistry;public CanalRunner(String canalIp, Integer canalPort, String canalInstance, Integer batchSize,Integer sleepTime, JsonMessageParser jsonMessageParser, CanalWorkRegistry workRegistry) {this.canalIp = canalIp;this.canalPort = canalPort;this.canalInstance = canalInstance;this.batchSize = batchSize;this.sleepTime = sleepTime;this.jsonMessageParser = jsonMessageParser;this.workRegistry = workRegistry;}/*** 启动Canal实例*/public void start() {if (thread == null || !thread.isAlive()) {thread = new Thread(this::run, "canal-runner-" + canalInstance);thread.start();log.info("Canal实例[{}]启动成功", canalInstance);}}/*** 停止Canal实例*/public void stop() {if (thread != null && !thread.isInterrupted()) {thread.interrupt();}}private void run() {log.info("Canal实例[{}]启动中...", canalInstance);CanalConnector connector = CanalConnectors.newSingleConnector(new InetSocketAddress(canalIp, canalPort), canalInstance, "", "");try {connector.connect();// 订阅所有表(后续通过Work过滤)
            connector.subscribe();connector.rollback();while (!thread.isInterrupted()) {Message message = connector.getWithoutAck(batchSize);long batchId = message.getId();List<CanalEntry.Entry> entries = message.getEntries();if (batchId == -1 || entries.isEmpty()) {Thread.sleep(sleepTime);} else {// 解析数据并处理Map<String, List<JsonMessageType>> parsedData = jsonMessageParser.parse(entries);processParsedData(parsedData);// 确认处理成功
                    connector.ack(batchId);}}} catch (InterruptedException e) {log.info("Canal实例[{}]被中断", canalInstance);} catch (Exception e) {log.error("Canal实例[{}]运行异常", canalInstance, e);// 处理失败回滚
            connector.rollback();} finally {connector.disconnect();log.info("Canal实例[{}]已停止", canalInstance);}}/*** 调用Work处理解析后的数据** @param parsedData*/private void processParsedData(Map<String, List<JsonMessageType>> parsedData) {parsedData.forEach((tableKey, dataList) -> {// 获取该表的所有WorkList<CanalWork> works = workRegistry.getWorksByTable(tableKey);if (!works.isEmpty() && !dataList.isEmpty()) {// 转换数据格式(Json字符串 -> Map)List<Map<String, Object>> dataMaps = dataList.stream().map(item -> JSON.<Map<String, Object>>parseObject(item.getData(), Map.class)).collect(Collectors.toList());String schemaName = dataList.get(0).getSchemaName();// 调用每个Work的处理方法works.forEach(work -> work.handle(dataMaps, dataList.get(0).getEventType(), schemaName));}});}}

6、JsonMessageParser解析数据:

MessageParser

import com.alibaba.otter.canal.protocol.CanalEntry;
import java.util.List;/*** 消息解析器接口**/
public interface MessageParser<T> {T parse(List<CanalEntry.Entry> canalEntryList);}
 JsonMessageParser
import com.alibaba.fastjson2.JSON;
import com.alibaba.otter.canal.protocol.CanalEntry;
import com.iven.canal.entity.DataEventTypeEnum;
import com.iven.canal.entity.JsonMessageType;
import com.iven.canal.handle.CanalWorkRegistry;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.*;/*** Json消息解析器** 1、遍历原始数据列表接收* 2、解析行级变更数据* 3、封装为 JsonParseType*/
@Slf4j
@Component
@RequiredArgsConstructor
public class JsonMessageParser implements MessageParser<Map<String, List<JsonMessageType>>> {private final CanalWorkRegistry workRegistry;@Overridepublic Map<String, List<JsonMessageType>> parse(List<CanalEntry.Entry> canalEntryList) {Map<String, List<JsonMessageType>> dataMap = new HashMap<>();for (CanalEntry.Entry entry : canalEntryList) {if (!CanalEntry.EntryType.ROWDATA.equals(entry.getEntryType())) {continue;}// 1. 获取库名、表名、带库名的表标识String schemaName = entry.getHeader().getSchemaName();String tableName = entry.getHeader().getTableName();String fullTableName = schemaName + "." + tableName;// 2. 检查是否有对应的处理器(支持两种格式)boolean hasFullTableWork = !CollectionUtils.isEmpty(workRegistry.getWorksByTable(fullTableName));boolean hasSimpleTableWork = !CollectionUtils.isEmpty(workRegistry.getWorksByTable(tableName));if (!hasFullTableWork && !hasSimpleTableWork) {log.debug("表[{}]和[{}]均无同步处理器,跳过", fullTableName, tableName);continue;}try {CanalEntry.RowChange rowChange = CanalEntry.RowChange.parseFrom(entry.getStoreValue());rowChange.getRowDatasList().forEach(rowData -> {JsonMessageType jsonMessageType = parseRowData(entry.getHeader(), rowChange.getEventType(), rowData);if (jsonMessageType != null) {// 3. 按存在的处理器类型,分别添加到数据映射中if (hasFullTableWork) {dataMap.computeIfAbsent(fullTableName, k -> new ArrayList<>()).add(jsonMessageType);}if (hasSimpleTableWork) {dataMap.computeIfAbsent(tableName, k -> new ArrayList<>()).add(jsonMessageType);}}});} catch (Exception e) {log.error("解析数据失败", e);}}return dataMap;}private JsonMessageType parseRowData(CanalEntry.Header header, CanalEntry.EventType eventType,CanalEntry.RowData rowData) {// 获取库名String schemaName = header.getSchemaName();// 获取表名String tableName = header.getTableName();if (eventType == CanalEntry.EventType.DELETE) {return dataWrapper(schemaName, tableName, DataEventTypeEnum.DELETE.NAME(), rowData.getBeforeColumnsList());} else if (eventType == CanalEntry.EventType.INSERT) {return dataWrapper(schemaName, tableName, DataEventTypeEnum.INSERT.NAME(), rowData.getAfterColumnsList());} else if (eventType == CanalEntry.EventType.UPDATE) {return dataWrapper(schemaName, tableName, DataEventTypeEnum.UPDATE.NAME(), rowData.getAfterColumnsList());}return null;}private JsonMessageType dataWrapper(String schemaName, String tableName, String eventType,List<CanalEntry.Column> columns) {Map<String, String> data = new HashMap<>();columns.forEach(column -> data.put(column.getName(), column.getValue()));JsonMessageType result = new JsonMessageType();result.setSchemaName(schemaName);result.setTableName(tableName);result.setEventType(eventType);result.setData(JSON.toJSONString(data));return result;}}

7、CanalWorkRegistry匹配处理器:

CanalWork

import java.util.List;
import java.util.Map;/*** Canal-Work处理器**/
public interface CanalWork {/*** 返回需要处理的表名(如:tb_user)*/String getTableName();/*** 处理表数据的方法* @param dataList 表数据列表(每条数据是字段名-值的Map)* @param eventType 事件类型(INSERT/UPDATE/DELETE)* @param schemaName 库名(用于区分不同库的表)*/void handle(List<Map<String, Object>> dataList, String eventType, String schemaName);}

CanalWorkRegistry

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;/*** 处理器注册器,* 扫描并缓存所有CanalWork实现类,按表名分组管理,提供查询表对应处理器的方法*/
@Slf4j
@Component
public class CanalWorkRegistry implements ApplicationContextAware {/*** 表名 -> Work列表(支持一个表多个Work)*/private final Map<String, List<CanalWork>> tableWorkMap = new HashMap<>();@Overridepublic void setApplicationContext(ApplicationContext applicationContext) throws BeansException {// 扫描所有CanalWork实现类Map<String, CanalWork> workMap = applicationContext.getBeansOfType(CanalWork.class);// 按表名分组
        tableWorkMap.putAll(workMap.values().stream().collect(Collectors.groupingBy(CanalWork::getTableName)));log.info("已注册的表同步处理器: {}", tableWorkMap.keySet());}/*** 获取指定表的Work列表** @param tableName* @return*/public List<CanalWork> getWorksByTable(String tableName) {return tableWorkMap.getOrDefault(tableName, Collections.emptyList());}/*** 判断是否有表需要处理** @return*/public boolean hasWork() {return !tableWorkMap.isEmpty();}}

8、CanalWork实现类处理数据:

import com.iven.canal.entity.DataEventTypeEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;/*** tb_user表数据处理** Canal服务 → 变更数据 → CanalRunner 拉取 → JsonMessageParser 解析 →* 筛选出 tb_user 数据 → CanalWorkRegistry 获取 TbUserCanalWorkHandle →* 调用 handle 方法 → 按事件类型(INSERT/UPDATE/DELETE)执行对应逻辑*/
@Slf4j
@Component
public class TbUserCanalWorkHandle implements CanalWork {@Overridepublic String getTableName() {return "demo.tb_user";}@Overridepublic void handle(List<Map<String, Object>> dataList, String eventType, String schemaName) {log.info("开始处理[{}库]的tb_user表数据,事件类型:{},数据量:{}", schemaName, eventType, dataList.size());DataEventTypeEnum dataEventTypeEnum = DataEventTypeEnum.getEnum(eventType);// 根据事件类型分别处理switch (dataEventTypeEnum) {case INSERT:handleInsert(dataList, schemaName);break;case UPDATE:handleUpdate(dataList, schemaName);break;case DELETE:handleDelete(dataList, schemaName);break;default:log.warn("未处理的事件类型:{}", eventType);}}/*** 处理新增数据*/private void handleInsert(List<Map<String, Object>> dataList, String schemaName) {log.info("处理[{}库]的tb_user新增数据,共{}条", schemaName, dataList.size());dataList.forEach(data -> {Object userId = data.get("id");Object username = data.get("name");// 新增逻辑:如同步到ES、缓存初始化等log.info("新增用户 - ID: {}, 用户名: {}", userId, username);});}/*** 处理更新数据*/private void handleUpdate(List<Map<String, Object>> dataList, String schemaName) {log.info("处理[{}库]的tb_user更新数据,共{}条", schemaName, dataList.size());dataList.forEach(data -> {Object userId = data.get("id");Object newPhone = data.get("phone"); // 假设更新了手机号// 更新逻辑:如更新ES文档、刷新缓存等log.info("更新用户 - ID: {}, 新手机号: {}", userId, newPhone);});}/*** 处理删除数据*/private void handleDelete(List<Map<String, Object>> dataList, String schemaName) {log.info("处理[{}库]的tb_user删除数据,共{}条", schemaName, dataList.size());dataList.forEach(data -> {Object userId = data.get("id");// 删除逻辑:如从ES删除、清除缓存等log.info("删除用户 - ID: {}", userId);});}
}

b8706148-77aa-40f9-9f39-65310de78089

 

调度流程:

整个流程通过注册器管理处理器、解析器转换数据格式、运行器控制 Canal 客户端生命周期,最终将数据库变更事件分发到对应表的处理器,实现了变更数据的监听与业务处理解耦。用户只需实现CanalWork接口,即可自定义任意表的变更处理逻辑。

(1)、初始化阶段

1)、Spring 容器启动时,CanalWorkRegistry 扫描所有 CanalWork 实现类(如 TbUserCanalWorkHandle),按表名分组缓存到 tableWorkMap 中。

2)、CanalRunnerAutoConfig 检查配置(CanalProperties),若开启自动同步且存在 CanalWork,则为每个 Canal 实例创建 CanalRunner 并启动。

(2)、运行阶段

1)、CanalRunner 建立与 Canal 服务的连接,订阅数据库变更事件。

2)、循环拉取变更数据(Message),通过 JsonMessageParser 解析为表名 - 数据列表的映射(Map<String, List<JsonMessageType>>)。

3)、调用 processParsedData 方法,根据表名从 CanalWorkRegistry 获取对应的 CanalWork 列表,执行 handle 方法处理数据。

(3)、销毁阶段

程序停止时,CanalRunner 中断线程,断开与 Canal 服务的连接。

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/951532.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2025 工业加热器厂家选型指南:最新推荐实力制造商排行榜,覆盖多场景加热设备解决方案

引言 在工业生产中,加热器作为关键设备,直接关系到工艺稳定性、生产效率与产品质量,广泛应用于机械制造、化工、新能源、汽车等多个领域。然而当前市场上加热器品牌繁杂,部分产品存在加热不均、能耗高、售后响应慢…

2025年1.5毫米道路路基hdpe土工膜推荐TOP生产厂家

2025年1.5毫米道路路基HDPE土工膜推荐TOP生产厂家引言在道路建设工程中,路基防渗是确保道路长期稳定性和耐久性的关键环节。HDPE土工膜作为一种高性能防渗材料,因其优异的抗渗透性、耐化学腐蚀性和机械强度,已成为道…

moe+diffusion language model(DLM) - jack

Blog 代码:GitHub - JinjieNi/OpenMoE2: The official repo for "OpenMoE 2: Sparse Diffusion Language Models". OpenMoE 2 是第一个 moe+diffusion language model (DLM) 的架构研究,并且会from scrat…

sg_后台线程运行函数示例

import PySimpleGUI as sg import math import time from threading import Eventdef calculate_sqrt_sum(window, stop_event):"""后台计算函数:计算1亿以内自然数的平方根之和"""tot…

portainer docker-compose.yml

portainer docker-compose.ymlservices: app: image: portainer/portainer-ce:2.35.0-alpine container_name: portainer restart: unless-stopped ports: - 19443:9443 # Public HTTPS Port vol…

一行代码快速开发 AntdUI 风格的 WinForm 通用后台框架

前言 在快速迭代的软件开发环境中,如何高效地开发一个功能完整、界面美观的 WinForm 管理系统,是许多开发者面临的现实问题。今天推荐一款基于 Ant Design 设计语言的 WinForm UI 框架,它通过深度封装和现代化设计,…

2025年优质的网带炉厂家选购指南与推荐

2025年优质的网带炉厂家选购指南与推荐网带炉选购概述在2025年的工业制造领域,网带炉作为热处理设备的重要组成部分,其性能和质量直接关系到生产效率和产品质量。随着工业4.0技术的深入发展,智能化、节能环保、高精…

2025年耐用的别墅电梯行业内口碑厂家排行榜

2025年耐用的别墅电梯行业内口碑厂家排行榜随着城市化进程的加快和人们对生活品质要求的提高,别墅电梯已成为高端住宅的标配。2025年,别墅电梯行业迎来了新一轮的技术革新和市场洗牌,消费者在选择电梯产品时更加注重…

ui设计公司审美积累 | 扁平化app界面设计

ui设计公司审美积累 | 扁平化app界面设计扁平化设计核心特征:界面整体简洁明了,去除了多余的装饰元素,如阴影、纹理等。色彩运用上,以白色、浅灰等纯色为基底,搭配少量高饱和度的辅助色,如蓝色、橙色等,形成鲜明…

飞牛os初体验

飞牛os初体验Posted on 2025-10-31 10:35 kacoro 阅读(0) 评论(0) 收藏 举报 nas的需求一直都有,而飞牛的简单易用,其实是pve可能更适合,但是最好还是有集显或者双显卡的机器使用。以后再考虑装吧。 直接再hy…

2025 年钢球厂家最新推荐榜:技术实力与市场口碑深度解析,筛选优质服务商420 不锈钢球 / 304 不锈钢球 / 316L 不锈钢球制造商推荐

引言 随着轴承制造、汽车配件、精密仪器等行业的快速发展,钢球市场需求持续增长,但市场上产品质量参差不齐,企业采购时难以精准辨别优质服务商。为解决这一问题,本次推荐榜由机械工业金属制品协会联合行业专家团队…

2025年正规的学校宿舍铁床厂家推荐及采购指南

2025年正规的学校宿舍铁床厂家推荐及采购指南引言随着教育事业的蓬勃发展和企事业单位对员工住宿条件的日益重视,学校宿舍铁床作为基础配套设施,其质量与安全性越来越受到关注。2025年,市场上涌现出一批专业从事宿舍…

2025年短视频运营公司推荐:行业五大短视频公司深度解析

短视频运营在当今数字化时代的重要性不言而喻,企业要想在竞争激烈的市场中脱颖而出,选择一家专业的短视频运营公司至关重要。以下为您推荐五家的短视频运营公司。 TOP1推荐:南方网通佛山分公司 评价指数:★★★★★…

2025年知名的五星酒店家具厂家最新用户好评榜

2025年知名的五星酒店家具厂家最新用户好评榜在高端酒店行业,家具的品质与设计直接影响着宾客体验和酒店整体形象。随着2025年酒店业的全面复苏与升级,五星级酒店对家具供应商的要求更加严苛,不仅需要卓越的产品质量…

2025 年广告喷绘公司最新推荐榜:优质企业实力解析与选择指南墙体广告喷绘广告牌 / 墙面广告喷绘 / 手绘广告喷绘推荐

引言 随着商业视觉传播需求的持续增长,广告喷绘行业规模逐年扩大,据广告协会 2024 年度行业报告显示,国内广告喷绘市场规模已突破 800 亿元,年增长率达 12.3%。然而,市场中仍存在资质混杂、工艺参差不齐的问题,超…

2025年正规的玻璃淋浴房配件品牌厂家排行榜

2025年正规的玻璃淋浴房配件品牌厂家排行榜 随着家居装修品质要求的不断提升,玻璃淋浴房作为现代卫浴空间的重要组成部分,其配件的质量直接影响使用体验和安全性。优质的淋浴房配件不仅需要具备出色的耐用性和顺滑度…

Python 格式化字符串 _ 优雅群发春节短信

Python 格式化字符串 _ 优雅群发春节短信gpa_dict = {}#字典增加key:valuegpa_dict.setdefault("A", 10.2)gpa_dict.setdefault("B", 30.315)gpa_dict.setdefault("C", 20.45)gpa_dict…

2025年升压充电芯片供货厂家权威推荐榜单:升降压充电管理IC/超级电容充电/开关型充电管理IC源头厂家精选

随着便携式电子设备与新能源产业的快速发展,升压充电芯片作为高效能电源管理的核心组件,其市场需求持续增长。行业数据显示,2024年中国电源管理芯片市场规模已突破1200亿元,其中升压充电芯片占比达28%,年均增长率…

2025年财税咨询会计公司:专业选择与企业成长指南

文章摘要 本文探讨2025年财税咨询会计公司的发展趋势,重点分析如何选择靠谱的服务提供商。基于行业数据和客户案例,强调专业团队、三对一服务和全面业务范围的重要性,并以临沂华恒企业管理有限公司为例,展示其资质…

2025年财税咨询会计公司:趋势、选择与临沂华恒的专业服务

文章摘要 本文探讨2025年财税咨询会计行业的发展趋势,包括数字化转型和合规要求提升,并提供选择靠谱公司的实用指南。重点推荐临沂华恒企业管理有限公司,其拥有财政局授权资质、专业会计团队和三对一服务,一站式解…