AbstractRoutingDataSource实现多数据源切换以及事务中无法切换问题

news2024/12/23 23:12:08

一、AbstractRoutingDataSource实现多数据源切换

        为了实现数据源的动态切换,我们采用了AbstractRoutingDataSource结合AOP+反射来自定义注解。通过这种机制,我们可以在运行时根据自定义注解来选择不同的数据源,从而实现灵活高效的数据访问策略。

        具体来说,我们首先创建了一个继承自AbstractRoutingDataSource的动态数据源类DynamicDataSource,该类能够管理多个数据源并根据线程上下文中的特定键值来选择使用哪一个数据源。接着,我们定义了一个自定义注解,用于标记需要切换数据源的方法。然后,利用AOP技术,我们在方法执行之前和之后添加了环绕通知,在这个通知中,我们通过反射获取到当前方法的自定义注解信息,并据此设置线程上下文中的数据源键值,从而引导数据源切换逻辑。

       这样,当一个被标注了自定义注解的方法被调用时,系统会自动根据注解指定的数据源名称切换到对应的数据源,完成对数据库的操作。这种方式不仅提高了代码的复用性和可维护性,而且通过解耦业务逻辑与数据源选择,增强了系统的可扩展性和灵活性。

1、自定义注解DataSource
import java.lang.annotation.*;

@Target({ElementType.METHOD})//注解方法
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
    String name() default "";
}
2、切面类DataSourceAspect
import com.zcloud.sunshine.common.constant.DatabaseType;
import com.zcloud.sunshine.config.DynamicDataSource;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

@Aspect
@Component
@Slf4j
public class DataSourceAspect {

    @Pointcut("@annotation(com.zcloud.sunshine.annotation.DataSource)")
    public void dataSourcePointCut() {

    }

    @Around("dataSourcePointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        
       //获取被拦截方法的签名,该签名包含了方法的名称、参数类型等信息
        MethodSignature signature = (MethodSignature) point.getSignature();
        //从签名中获取了实际的方法对象
        Method method = signature.getMethod();
        //从方法上获取名为DataSource的注解。如果这个方法上有DataSource注解,那么dataSource将不为null,否则为null
        DataSource dataSource = method.getAnnotation(DataSource.class);
        if (dataSource == null) {
            log.info("默认数据源 dataSource1");
            DynamicDataSource.setDataSource(DatabaseType.dataSource1.toString());
        } else {
            log.info("切换数据源: " + dataSource.name());
            DynamicDataSource.setDataSource(dataSource.name());
        }

        try {
            return point.proceed();
        } finally {
        //最后一定要清除,这里使用的ThreadLocal来存储的数据源key,所以为了防止内存泄露一定要清除
        //而且该清除操作也是为了防止该切换操作对后续的切换操作造成影响
            DynamicDataSource.clearDataSource();
        }
    }
}

上面两步主要就是通过aop+反射完成自定义切换数据源注解的功能,实现在该注解修饰的方法上,设置当前方法的数据源。如果未设置数据源,则使用默认的数据源。

3、自定义动态数据源DynamicDataSource类
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

import javax.sql.DataSource;
import java.util.Map;

public class DynamicDataSource extends AbstractRoutingDataSource {
//使用ThreadLocal来存储当前线程的数据源名称,保证多线程情况下,各自的数据源互不影响
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();

    public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
        //将注册的数据源以及设置的默认数据源设置到父类对应的成员变量中
        super.setDefaultTargetDataSource(defaultTargetDataSource);
        super.setTargetDataSources(targetDataSources);
        super.afterPropertiesSet();
    }

    //返回当前数据源
    @Override
    protected Object determineCurrentLookupKey() {
        return getDataSource();
    }

    public static void setDataSource(String dataSource) {
        contextHolder.set(dataSource);
    }

    public static String getDataSource() {
        return contextHolder.get();
    }

    public static void clearDataSource() {
        contextHolder.remove();
    }
}
4、配置多数据源DynamicDataSourceConfig 
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import com.zcloud.sunshine.common.constant.DatabaseType;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

/**
 * @author zy
 * @desc 配置多数据源
 */
@Configuration
@Component
public class DynamicDataSourceConfig {

    @Bean(value = "dataSource1")
    @ConfigurationProperties(prefix = "spring.datasource.druid.dataSource1")
    public DataSource dataSource1() {  //此处的返回类型DataSource不是我们自定义的注解DataSource,而是java.sql包下的
        return DruidDataSourceBuilder.create().build();
    }

    @Bean(value = "dataSource2")
    @ConfigurationProperties(prefix = "spring.datasource.druid.dataSource2")
    public DataSource dataSource2() {
        return DruidDataSourceBuilder.create().build();
    }

    @Bean
    @Primary
    public DynamicDataSource dataSource(@Qualifier(value = "dataSource1") DataSource dataSource1,
                                        @Qualifier(value = "dataSource2") DataSource dataSource2
                                        ) {
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(DatabaseType.dataSource1.toString(), dataSource1);
        targetDataSources.put(DatabaseType.dataSource2.toString(), dataSource2);
        //注册所有的数据源,并且将数据源1设置为默认数据源
        return new DynamicDataSource(dataSource1, targetDataSources);
    }
}

5、多数据源枚举类
public enum DatabaseType {

    dataSource1,//数据库1
    dataSource2//数据库2
}
6、配置多数据源数据库连接信息
#数据源1
spring.datasource.druid.dataSource1.url=jdbc:mysql://127.0.0.1:3306/zcloud_user?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useTimezone=true&serverTimezone=GMT%2B8
spring.datasource.druid.dataSource1.username=root
spring.datasource.druid.dataSource1.password=zy521
spring.datasource.druid.dataSource1.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.druid.dataSource1.type=com.alibaba.druid.pool.DruidDataSource

#数据源2
spring.datasource.druid.dataSource2.url=jdbc:mysql://127.0.0.1:3306/zcloud_order?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useTimezone=true&serverTimezone=GMT%2B8
spring.datasource.druid.dataSource2.username=root
spring.datasource.druid.dataSource2.password=zy521
spring.datasource.druid.dataSource2.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.druid.dataSource2.type=com.alibaba.druid.pool.DruidDataSource

3-6步骤中的3-4这两个步骤就是实现动态切换数据源的核心代码。我们在配置类DynamicDataSourceConfig中配置我们需要切换的数据源信息,并且设置默认的数据源。配置类中设置动态数据源的信息实际就是调用我们定义的动态数据源类DynamicDataSource的有参构造方法DynamicDataSource。

从该构造方法中,我们可以得知,我们设置的动态数据源信息也就是设置给了我们继承的父类AbstractRoutingDataSource中的成员变量,并且调用了父类的成员方法afterPropertiesSet()。afterPropertiesSet方法大家是不是感觉很熟悉?是的,看过springbean的生命周期的应该对该方法都不会陌生。afterPropertiesSet方法就是在一个类实现InitializingBean接口必须要重写的方法。 该方法是在bean属性设置完成后执行。在我们这个场景,其实就是在我们设置完AbstractRoutingDataSource中的成员变量后去执行。在这个方法里面我们可以看到,其实他就是对我们之前设置的成员变量,进行了一个处理,再赋值给对应的成员变量。

    public void afterPropertiesSet() {
        if (this.targetDataSources == null) {
            throw new IllegalArgumentException("Property 'targetDataSources' is required");
        } else {
            this.resolvedDataSources = CollectionUtils.newHashMap(this.targetDataSources.size());
            this.targetDataSources.forEach((key, value) -> {
                Object lookupKey = this.resolveSpecifiedLookupKey(key);
                DataSource dataSource = this.resolveSpecifiedDataSource(value);
                this.resolvedDataSources.put(lookupKey, dataSource);
            });
            if (this.defaultTargetDataSource != null) {
                this.resolvedDefaultDataSource = this.resolveSpecifiedDataSource(this.defaultTargetDataSource);
            }

        }
    }

 这些都是在项目启动的时候执行的,目的说白了就是将所有的动态数据源信息存储到一个map集合resolvedDataSources中,并设置默认的数据源resolvedDefaultDataSource。

而真正的设置数据源是使用我们自定义的动态数据源类DynamicDataSource的setDataSource()方法实现的。我们设置的数据源能够生效的原因就是,我们在继承AbstractRoutingDataSource类的时候,实现了该抽象方法determineCurrentLookupKey。该方法其实就是我们能够动态设置数据源的核心方法,说白了也就是AbstractRoutingDataSource给我们留的一个口子。

我们实现该方法的逻辑并不难,就是返回我们设置的数据源。 然后,我们看AbstractRoutingDataSource使用该方法的返回值做了什么。追溯到AbstractRoutingDataSource类,找到这个方法determineTargetDataSource中调用了我们实现的determineCurrentLookupKey方法(其实这里也就是使用了模板方法设计模式)。

protected DataSource determineTargetDataSource() {
        Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
        //调用我们设置的数据源名称
        Object lookupKey = this.determineCurrentLookupKey();
        //从我们在项目启动的时候注册到map集合resolvedDataSources中的数据源信息中get
        //该名称的数据源
        DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);
        //没有的话就使用默认数据源
        if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
            dataSource = this.resolvedDefaultDataSource;
        }
        //为空抛出异常
        if (dataSource == null) {
            throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
        } else { 
            return dataSource;
        }
    }

再追溯determineTargetDataSource()方法,可以看到我们再使用getConnection()方法获取数据库连接的时候,就是调用了该方法determineTargetDataSource来获取数据源,然后根据数据源来获取数据库连接的。

至此,我们就应该明白为什么我们能够通过 AbstractRoutingDataSource来实现动态切换数据源了。

二、事务问题

1、问题描述

       在使用spring编程式事务的时候,切换数据源dataSource2会出现无法切换的情况。导致在事务中进行数据库操作的时候使用的默认数据源dataSource1,这样就会导致sql报错。因为默认数据源中并没有sql语句中进行操作的表。

事务代码:

 //开启事务
TransactionStatus transaction = transactionManager.getTransaction(transactionDefinition);
            try {
                if (!CollectionUtils.isEmpty(list)) {
                    //插入运行记录
                    traceRouteResultMapper.addDialRunRecord2(dialRunRecord);
                    //插入执行结果
                    traceRouteResultMapper.insertBatch(list);
                    //插入告警
                    if(!CollectionUtils.isEmpty(alarmListAdd)){
                        dialTestAlarmMapper.insertBatch(alarmListAdd);
                    }
                    //更新告警最新触发时间
                    if(!CollectionUtils.isEmpty(alarmUpdateTime)){
                        dialTestAlarmMapper.updateLastAlarmTime(alarmUpdateTime);
                    }
                }
                //提交事务
                transactionManager.commit(transaction);
            } catch (Exception e) {
                //回滚
                transactionManager.rollback(transaction);
                log.error("TraceRouteServiceImpl.getIndexInfo traceroute失败!", e);
            }

 map层代码:

@Mapper
public interface DialTestAlarmMapper {

    @DataSource(name = "dataSource2")
    List<DialTestAlarmPartInfoDto> queryAllTraceRouteAlarm();

    @DataSource(name = "dataSource2")
    void insertBatch(@Param("dtoList") List<DialTestAlarmDto> alarmListAdd);

    @DataSource(name = "dataSource2")
    void updateLastAlarmTime(@Param("dtoList")  List<String> alarmUpdateTimeIds);
}
2、问题原因

     为什么在事务内无法切换数据源呢?想要弄清这个原因,我们就要看看开启事务的时候,做了什么。追溯开始事务的源码,可以发现一个关键方法doBegin()。

protected void doBegin(Object transaction, TransactionDefinition definition) {
        DataSourceTransactionManager.DataSourceTransactionObject txObject = (DataSourceTransactionManager.DataSourceTransactionObject)transaction;
        Connection con = null;

        try {
            if (!txObject.hasConnectionHolder() || txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
                Connection newCon = this.obtainDataSource().getConnection();
                if (this.logger.isDebugEnabled()) {
                    this.logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction");
                }

                txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
            }
            //获取数据库连接
            txObject.getConnectionHolder().setSynchronizedWithTransaction(true);
            con = txObject.getConnectionHolder().getConnection();
            Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition);
            txObject.setPreviousIsolationLevel(previousIsolationLevel);
            txObject.setReadOnly(definition.isReadOnly());
            if (con.getAutoCommit()) {
                txObject.setMustRestoreAutoCommit(true);
                if (this.logger.isDebugEnabled()) {
                    this.logger.debug("Switching JDBC Connection [" + con + "] to manual commit");
                }

                con.setAutoCommit(false);
            }

            this.prepareTransactionalConnection(con, definition);
            txObject.getConnectionHolder().setTransactionActive(true);
            int timeout = this.determineTimeout(definition);
            if (timeout != -1) {
                txObject.getConnectionHolder().setTimeoutInSeconds(timeout);
            }
             // 把当前的数据源的Connection与线程进行绑定
            if (txObject.isNewConnectionHolder()) {
                TransactionSynchronizationManager.bindResource(this.obtainDataSource(), txObject.getConnectionHolder());
            }

        } catch (Throwable var7) {
            if (txObject.isNewConnectionHolder()) {
                DataSourceUtils.releaseConnection(con, this.obtainDataSource());
                txObject.setConnectionHolder((ConnectionHolder)null, false);
            }

            throw new CannotCreateTransactionException("Could not open JDBC Connection for transaction", var7);
        }
    }

从该方法中,我们可以看到,在我们开启事务的时候,就完成了数据库连接connection和线程的绑定。此时,由于还未进行数据源的切换,所绑定的连接自然来自于默认的数据源。这意味着,在随后的事务处理过程中,只要事务尚未提交,所有针对数据库的操作都将通过这个已经建立好的连接来进行。因此,如果尝试在事务进行中切换数据源,则不会生效。

3、解决办法

      既然我们已经知道了问题的根本原因,解决办法也就变得相对直接明了。我们可以在开启事务之前,先手动的将数据源切换到dataSource2。这样,在随后打开事务时,将会使用来dataSource2的连接与当前线程进行绑定。通过这种预先设置的方式,我们确保了事务中使用的连接来源于我们指定的数据源,从而解决了在事务中切换数据源的问题。

            //手动设置数据源为dataSource2
            DynamicDataSource.setDataSource(DatabaseType.dataSource2.toString());
            TransactionStatus transaction = transactionManager.getTransaction(transactionDefinition);
            try {
                if (!CollectionUtils.isEmpty(list)) {
                    //插入运行记录
                    traceRouteResultMapper.addDialRunRecord2(dialRunRecord);
                    //插入执行结果
                    traceRouteResultMapper.insertBatch(list);
                    //插入告警
                    if(!CollectionUtils.isEmpty(alarmListAdd)){
                        dialTestAlarmMapper.insertBatch(alarmListAdd);
                    }
                    //更新告警最新触发时间
                    if(!CollectionUtils.isEmpty(alarmUpdateTime)){
                        dialTestAlarmMapper.updateLastAlarmTime(alarmUpdateTime);
                    }
                }
                //提交事务
                transactionManager.commit(transaction);
            } catch (Exception e) {
                //回滚
                transactionManager.rollback(transaction);
                log.error("TraceRouteServiceImpl.getIndexInfo traceroute失败!", e);
            }finally {
                //最后清除设置的数据源
                DynamicDataSource.clearDataSource();
            }

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

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

相关文章

Linux文件/目录高级管理一(头歌实训)

目录 任务描述 相关知识 Linux修改文件权限命令 Linux修改所有者权限 Linux修改同组用户权限 Linux修改其他用户权限 编程要求 任务描述 相关知识 Linux修改目录权限命令 Linux修改所有者权限 Linux修改同组用户权限 Linux修改其他用户权限 编程要求 任务描述 相…

Linux(Centos)服务器探索ffmpeg笔记 (命令行、Nvidia硬件加速、GPU、CPU、CUDA、h264_nvenc、过滤器、加水印)

目录 前言内容简介为什么会有这篇文章 1、服务器上怎么使用ffmpeg1.1 使用编译好的&#xff08;需要root权限&#xff09;1.2 自己怎么编译&#xff08;需要root权限&#xff09; 2 、非Root用户要怎么安装和使用3、ffmpeg命令的一些使用引导和参数介绍3.1 编译参数3.2 查询支持…

labview中TDMS读写波形图

TDMS与二进制读写速度区别不大&#xff0c;但是它具备关系型数据库的一些优点&#xff0c;经常用于存取波形数据。

数据库工程师的工作职责(合集)

数据库工程师的工作职责1 职责&#xff1a; 1. 日常数据库的基本安装&#xff0c;维护&#xff0c;升级&#xff0c;监控的; 2. 配合研发部门进行数据库设计支持&#xff0c;协助开发、设计和进行SQL语言优化; 3. 配合相关部门数据库相关的任务&#xff0c;比如数据导入导出&am…

单片机LCD1602显示电子时钟设计,含汇编程序、仿真、论文

目录 1、摘要 2 系统方案 2.1 系统整体方案的论证 3 硬件设计与实现 3.1单片机最小系统 3.2振荡电路的工作原理 3.2时钟电路的工作原理 3.3单片机最小系统电路图 3.4 时钟芯片 3.5 液晶显示电路 4 实物调试及测试 4.1 实物图 4.2仿真结果图如下所示 5、单片机源…

JAVA实现easyExcel动态生成excel

添加pom依赖 <dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId><version>2.2.6</version> </dependency><!--工具类--> <dependency><groupId>cn.hutool</groupId><…

请编写一个函数void fun(char*ss),其功能是:将字符串ss中所有下标为奇数位置上的字母转换为大写(若该位置上不是字母,则不转换)。

本文收录于专栏:算法之翼 https://blog.csdn.net/weixin_52908342/category_10943144.html 订阅后本专栏全部文章可见。 本文含有题目的题干、解题思路、解题思路、解题代码、代码解析。本文分别包含C语言、C++、Java、Python四种语言的解法完整代码和详细的解析。 题干 请编…

三星电脑文件夹误删了怎么办?恢复方案在此

在使用三星电脑的过程中&#xff0c;我们可能会不小心删除了某个重要的文件夹&#xff0c;其中可能包含了工作文件、家庭照片、视频或其他珍贵的数据。面对这种突发情况&#xff0c;不必过于焦虑。本文将为您提供几种有效的恢复方案&#xff0c;希望能帮助您找回误删的文件夹及…

正点原子[第二期]Linux之ARM(MX6U)裸机篇学习笔记-6

前言&#xff1a; 本文是根据哔哩哔哩网站上“正点原子[第二期]Linux之ARM&#xff08;MX6U&#xff09;裸机篇”视频的学习笔记&#xff0c;在这里会记录下正点原子 I.MX6ULL 开发板的配套视频教程所作的实验和学习笔记内容。本文大量引用了正点原子教学视频和链接中的内容。…

Java面试八股之Java中为什么没有全局变量

Java中为什么没有全局变量 Java中没有传统意义上的全局变量&#xff0c;这是因为Java语言设计遵循面向对象的原则&#xff0c;强调封装性和模块化&#xff0c;以及避免全局状态带来的副作用。 封装性&#xff1a; 全局变量违反了面向对象编程中的封装原则&#xff0c;即隐藏对…

【ARM 裸机】模仿 STM32 驱动开发

1、修改驱动 对于 STM32 来说&#xff0c;使用了一个结构体将一个外设的所有寄存器都放在一起&#xff0c;在上一节的基础上进行修改&#xff1b; 1.1、添加清除 bss 段代码&#xff0c; 1.2、添加寄存器结构体 新建一个文件&#xff0c;命名imx6u.h&#xff0c;注意地址的连…

JS手写set与map

目录 setaddhas与equalsdelete迭代器完整实现 map set set是一个没有重复元素的集合&#xff0c;事实上我们无法完全的使用js来模拟出set的全部功能&#xff0c;因为浏览器原生的set底层是使用c实现&#xff0c;能直接访问内存&#xff0c;所以我们只能实现set的一部分功能 这…

Django与mysqlclient链接不成功

先检查自己的python是什么版本&#xff0c;是64位还是32位&#xff0c;这个自己去网上查。 我的是32位的&#xff0c;因为直接pip下载不了&#xff0c;网上也没有32位的whl&#xff0c;所以卸载重装一个64位的3.9.6的python 网上直接搜mysqlclient&#xff0c;找到对应py39也…

WPF2 样式布局

样式布局 WPF中的各类控件元素, 都可以自由的设置其样式。 诸如: 字体(FontFamily) 字体大小(FontSize) 背景颜色(Background) 字体颜色(Foreground) 边距(Margin) 水平位置(HorizontalAlignment) 垂直位置(VerticalAlignment) 等等。 而样式则是组织和重用以上的重要工具。…

ray.tune调参学习笔记1:超参数优化器tuner设置

最近研究中学习使用python的ray.tune进行神经网络调参。在这里记录学习过程中的收获&#xff0c;希望能够帮助到有同样需求的人。学习过程主要参考ray官网文档&#xff0c;但由于笔者使用的ray为2.2.0版本&#xff0c;而官方文档为更高级版本&#xff0c;笔者代码和官方文档代码…

Python实现本地视频/音频播放器

Python实现本地视频/音频播放器 在Python中&#xff0c;有几个库可以用于视频播放&#xff0c;但是没有一个库是完美的&#xff0c;因为它们可能依赖于外部软件或有一些限制。 先看介绍用Python实现本地视频播放器&#xff0c;再介绍用Python实现本地音乐播放器。 Python实现…

【C 数据结构】图

文章目录 【 1. 基本原理 】1.1 无向图1.2 有向图1.3 基本知识 【 2. 图的存储结构 】2.1 完全图2.2 稀疏图和稠密图2.3 连通图2.3.1 (普通)连通图连通图 - 无向图非连通图 的 连通分量 2.3.2 强连通图强连通图 - 有向图非强连通有向图 的 强连通分量 2.3.3 生成树 - 连通图2.3…

重仓比特币

作者&#xff1a;Arthur Hayes Co-Founder of 100x. 编译&#xff1a;liam ccvalue (下文中表达的任何观点均为作者的个人观点&#xff0c;不应作为投资决策的依据&#xff0c;也不应被视为参与投资交易的建议或意见&#xff09;。 我们中断牛市常规节目&#xff0c;为您播报这…

【研发管理】产品经理知识体系-产品创新中的市场调研

导读&#xff1a;在产品创新过程中&#xff0c;市场调研的重要性不言而喻。它不仅是产品创新的起点&#xff0c;也是确保产品成功推向市场的关键步骤。对于产品经理系统学习和掌握产品创新中的市场调研相关知识体系十分重要。 目录 概述&#xff1a;市场调研重要性 1、相关概…

华为数字化转型与数据管理实践介绍(附PPT下载)

华为作为全球领先的信息与通信技术&#xff08;ICT&#xff09;解决方案提供商&#xff0c;在数字化转型和数据管理领域拥有丰富的实践经验和技术积累。其数字化转型解决方案旨在帮助企业通过采用最新的ICT技术&#xff0c;实现业务流程、组织结构和文化的全面数字化&#xff0…