广告
返回顶部
首页 > 资讯 > 后端开发 > 其他教程 >C++怎么实现数据库连接池
  • 469
分享到

C++怎么实现数据库连接池

2023-07-05 15:07:17 469人浏览 独家记忆
摘要

本文小编为大家详细介绍“c++怎么实现数据库连接池”,内容详细,步骤清晰,细节处理妥当,希望这篇“C++怎么实现数据库连接池”文章能帮助大家解决疑惑,下面跟着小编的思路慢慢深入,一起来学习新知识吧。项目背景为了提高Mysql数据库的访问瓶颈

本文小编为大家详细介绍“c++怎么实现数据库连接池”,内容详细,步骤清晰,细节处理妥当,希望这篇“C++怎么实现数据库连接池”文章能帮助大家解决疑惑,下面跟着小编的思路慢慢深入,一起来学习新知识吧。

项目背景

为了提高Mysql数据库的访问瓶颈,常用的方法有如下两个:

  • 服务器端增加缓存服务器缓存常用的数据(例如redis

  • 增加连接池,来提高MYSQL Server的访问效率,在高并发的情况下,每一个用户大量的tcp三次握手。Mysql Server的连接认证,mysql Server关闭连接回收资源和TCP四次挥手所耗费的性能时间也是明显的,增加连接池就是为了减少这一部分的性能损耗。

注:常见的Mysqloracle、SQLServer等数据库都是基于C/S架构设计的。

市面上主流的Mysql数据库连接池,对于短时间内的大量增删改查操作的性能提升很明显,但大多数都是Java实现的,该项目的开展就是为了提高Mysql Server的访问效率,实现基于C++代码的数据库连接池模块。

针对于系统启动时就创建一定数量的连接,用户一旦执行CURD操作,直接拿出一条连接即可,不需要TCP的连接过程和资源回收过程,使用完该连接后归还给连接池的连接队列,供之后使用。

功能点介绍

连接池一般包含了数据库连接所用的ip地址、port端口号、username用户名、passWord密码以及其他一些性能参数:比如初始连接量、最大连接量、最大空闲时间、连接超时时间等

本项目重点实现上述通用功能

1、初始连接量(initSize)

初始连接量表示连接池事先会和MySQL Server创建的initSize数量的Connection连接。在完成初始连接量之后,当应用发起MySQL访问时,不用创建新的MySQLServer连接,而是从连接池中直接获取一个连接,当使用完成后,再把连接归还到连接池中。

2、最大连接量(maxSize)

并发访问MySQL Server的请求增加,初始连接量不够用了,此时会增加连接量,但是增加的连接量的上限就是maxSIze。因为每一个连接都会占用一个Socket资源,一般连接池和服务器都是部署在一台主机上,如果连接池的连接数量过多,那么服务器就不能响应太多的客户端请求了。

3、最大空闲时间(maxIdleTime)

当高并发过去,因为高并发而新创建的连接在很长时间(maxIdleTime)内没有得到使用,那么这些新创建的连接处于空闲,并且占用着一定的资源,这个时候就需要将其释放掉,最终只用保存iniSize个连接就行。

4、连接超时时间(connectionTimeOut)

当MySQL的并发访问请求量过大,连接池中的连接数量已经达到了maxSize,并且此时连接池中没有可以使用的连接,那么此时应用阻塞connectionTimeOut的时间,如果此时间内有使用完的连接归还到连接池,那么他就可以使用,如果超过这个时间还是没有连接,那么它获取数据库连接池失败,无法访问数据库。

功能点实现的相关原理综述

  • 连接池只需要一个实例,所以ConnectionPool以单例`模式设计;

  • 从ConnectionPool中可以获取和Mysql的连接Connection;

  • 空闲连接Connection全部维护在一个线程安全的Connection队列中,使用线程互斥保证队列的线程安;

  • 如果Connection队列为空,还需要再获取连接,此时需要动态创建连接,上限数量是maxSize;

  • 队列中空闲连接时间超过maxIdleTime的就会被释放掉,只保留初始的initSize个连接就可以了,这个功能点肯定要放在独立的线程中去做;

  • 如果Connection队列为空,而此时连接的数量已达上限maxSize,那么等待ConnectionTimeout时间还获取不到空闲的连接,那么获取连接失败,此处从Connection队列获取空闲连接,可以使用带超时时间的mutex互斥锁来实现连接超时时间;

  • 用户获取的连接用shared_ptr智能指针来管理,用lambda表达式定制连接释放的功能(不真正释放连接,而是把连接归还到连接池中);

  • 连接的生产和连接的消费采用生产者-消费者线程模型来设计,使用了线程间的同步通信机制条件变量和互斥锁。

图示如下:

C++怎么实现数据库连接池

关键技术点

1、MySql数据库编程

目的:在C++下输入Sql语句对数据库进行操作的代码封装

说明:这里的MYSQL的数据库编程直接采用oracle公司提供的C++客户端开发包 , 读者可以自己查阅资料或搜索官方文档自行学习相关api的使用方法。

Connection.h:

class Connection{public:// 初始化数据库连接Connection();// 释放数据库连接资源~Connection();// 连接数据库bool connect(string ip,unsigned short port,string user,string password,string dbname);// 更新操作 insert、delete、updatebool update(string sql);// 查询操作 selectMYSQL_RES* query(string sql);// 刷新一下连接的起始的空闲时间点void refreshAliveTime() { _alivetime = clock(); }// 返回存活的时间clock_t getAliveeTime()const { return clock() - _alivetime; }private:MYSQL* _conn; // 表示和MySQL Server的一条连接clock_t _alivetime; // 记录进入空闲状态后的起始存活时间};

Connection.cpp:

Connection::Connection(){// 初始化数据库连接_conn = mysql_init(nullptr);}Connection::~Connection(){// 释放数据库连接资源if (_conn != nullptr)mysql_close(_conn);}bool Connection::connect(string ip, unsigned short port,string username, string password, string dbname){// 连接数据库MYSQL* p = mysql_real_connect(_conn, ip.c_str(), username.c_str(),password.c_str(), dbname.c_str(), port, nullptr, 0);return p != nullptr;}bool Connection::update(string sql){// 更新操作 insert、delete、updateif (mysql_query(_conn, sql.c_str())){LOG("更新失败:" + sql);return false;}return true;}MYSQL_RES* Connection::query(string sql){// 查询操作 selectif (mysql_query(_conn, sql.c_str())){LOG("查询失败:" + sql);return nullptr;}return mysql_use_result(_conn);}

这里需要说明的是:在windows上使用数据库需要进行相关配置

大致配置内容如下:

  • 右键项目- C/C++ - 常规 -附加包含目录 - 增加mysql.h的头文件路径;

  • 右键项目 - 链接器 - 常规 - 附加库目录 - 填写libmysql.lib的路径;

  • 右键项目 - 链接器 - 输入 - 附加依赖项 - 填写libmysql.lib的路径;

  • 把libmysql.dll的动态链接库(linux下后缀名是.so库)放在工程目录下。

2、数据库连接池单例代码

连接池仅需要一个实例,同时服务器肯定是多线程的,必须保证线程安全,所以采用懒汉式线程安全的单例:

CommonConnectionPool.h: 部分代码

class ConnectionPool{public:// 获取连接池对象实例static ConnectionPool* getConnectionPool();// 给外部提供接口,从连接池中获取一个可用的空闲连接shared_ptr<Connection> getConnection();private:// 单例#1 构造函数私有化ConnectionPool();};

CommonConnectionPool.cpp: 部分代码

// 线程安全的懒汉单例函数接口ConnectionPool* ConnectionPool::getConnectionPool(){static ConnectionPool pool; //静态对象初始化由编译器自动进行lock和unlockreturn &pool;}

3、queue队列容器

连接池的数据结构是queue队列,最早生成的连接connection放在队头,此时记录一个起始时间,这一点在后面最大空闲时间时会发挥作用:如果队头都没有超过最大空闲时间,那么其他的一定没有

CommonConnectionPool.cpp 的连接池构造函数:

// 连接池的构造ConnectionPool::ConnectionPool(){// 加载配置项了if (!loadConfigFile()){return;}// 创建初始数量的连接for (int i = 0; i < _initSize; ++i){Connection* p = new Connection();//创建一个新的连接p->connect(_ip, _port, _username, _password, _dbname);p->refreshAliveTime(); // 刷新一下开始空闲的起始时间_connectionQue.push(p);_connectionCnt++;}}

连接数量没有到达上限,继续创建新的连接

if (_connectionCnt < _maxSize){Connection* p = new Connection();p->connect(_ip, _port, _username, _password, _dbname);p->refreshAliveTime(); // 刷新一下开始空闲的起始时间_connectionQue.push(p);_connectionCnt++;}

扫描整个队列,释放多余的连接(高并发过后,新建的连接超过最大超时时间时)

unique_lock<mutex> lock(_queueMutex);while (_connectionCnt > _initSize){Connection* p = _connectionQue.front();if (p->getAliveTime() >= (_maxIdleTime * 1000)){_connectionQue.pop();_connectionCnt--;// 调用~Connection()释放连接delete p;}else{// 如果队头的连接没有超过_maxIdleTime,其他连接肯定没有break;}}

4、多线程编程

为了将多线程编程的相关操作应用到实际,也为了进行压力测试,用结果证明使用连接池之后对数据库的访问效率确实比不使用连接池的时候高很多,使用了多线程来进行数据库的访问操作,并且观察多线程下连接池对于性能的提升。

代码如下:

int main(){thread t1([]() {for (int i = 0; i < 250; ++i){Connection conn;char sql[1024] = { 0 };sprintf(sql, "insert into user(name,age,sex) values('%s',%d,'%s')","zhang san", 20, "M");conn.connect("127.0.0.1", 3306, "root", "991205", "chat");conn.update(sql);}});thread t2([]() {for (int i = 0; i < 250; ++i){Connection conn;char sql[1024] = { 0 };sprintf(sql, "insert into user(name,age,sex) values('%s',%d,'%s')","zhang san", 20, "M");conn.connect("127.0.0.1", 3306, "root", "991205", "chat");conn.update(sql);}});thread t3([]() {for (int i = 0; i < 250; ++i){Connection conn;char sql[1024] = { 0 };sprintf(sql, "insert into user(name,age,sex) values('%s',%d,'%s')","zhang san", 20, "M");conn.connect("127.0.0.1", 3306, "root", "991205", "chat");conn.update(sql);}});thread t4([]() {for (int i = 0; i < 250; ++i){Connection conn;char sql[1024] = { 0 };sprintf(sql, "insert into user(name,age,sex) values('%s',%d,'%s')","zhang san", 20, "male");conn.connect("127.0.0.1", 3306, "root", "991205", "chat");conn.update(sql);}});t1.join();t2.join();t3.join();t4.join();return 0;}

5、线程互斥、线程同步通信(生产者-消费者模型)、unique_lock

连接池中连接队列的连接的生产和消费需要保证其线程安全,于是我们需要引入互斥锁mutex,线程同步通信确保执行顺序,以及唯一锁。

代码如下:

class ConnectionPool{private:// 设置条件变量,用于连接生产线程和连接消费线程的通信condition_variable cv;// 维护连接队列的线程安全互斥锁mutex _queueMutex;};for (;;){unique_lock<mutex> lock(_queueMutex);while (!_connectionQue.empty()){// 队列不为空,此处生产线程进入等待状态cv.wait(lock);}// 连接数量没有达到上限,继续创建新的连接if (_connectionCnt < _maxSize){Connection* p = new Connection();p->connect(_ip, _port, _username, _password, _dbname);// 刷新一下开始空闲的起始时间p->refreshAliveTime();_connectionQue.push(p);_connectionCnt++;}// 通知消费者线程,可以消费连接了cv.notify_all();}
// 启动一个新的线程,作为连接的生产者 linux thread => pthread_createthread produce(std::bind(&ConnectionPool::produceConnectionTask, this));produce.detach();// 启动一个新的定时线程,扫描超过maxIdleTime时间的空闲连接,进行对于的连接回收thread scanner(std::bind(&ConnectionPool::scannerConnectionTask, this));scanner.detach();

6、CAS原子操作

对于连接池内的连接数量,生产者和消费者线程都会去改变其值,那么这个变量的修改就必须保证其原子性,于是使用C++11中提供的原子类:atomic_int

atomic_int _connectionCnt; // 记录连接所创建的connection连接的总数量 // 生产新连接时:_connectionCnt++;// 当新连接超过最大超时时间后被销毁时_connectionCnt--;

7、shared_ptr及lambda表达式

对于使用完成的连接,不能直接销毁该连接,而是需要将该连接归还给连接池的队列,供之后的其他消费者使用,于是我们使用智能指针,自定义其析构函数,完成放回的操作:

shared_ptr<Connection> sp(_connectionQue.front(),[&](Connection* pcon) {// 这里是在服务器应用线程中调用的,所以一定要考虑队列的线程安全操作unique_lock<mutex> lock(_queueMutex);pcon->refreshAliveTime();_connectionQue.push(pcon);});

8、压力测试

测试添加连接池后效率是否提升:

未使用连接池

单线程

int main(){clock_t begin = clock();for (int i = 0; i < 1000; ++i){Connection conn;char sql[1024] = { 0 };sprintf(sql, "insert into user(name,age,sex) values('%s',%d,'%s')","zhang san", 20, "M");conn.connect("127.0.0.1", 3306, "root", "123456", "chat");conn.update(sql);}clock_t end = clock();cout << (end - begin) << "ms" << endl;return 0;}

运行时间如下:

C++怎么实现数据库连接池

多线程

int main(){Connection conn;conn.connect("127.0.0.1", 3306, "root", "991205", "chat");clock_t begin = clock();thread t1([]() {for (int i = 0; i < 250; ++i){Connection conn;char sql[1024] = { 0 };sprintf(sql, "insert into user(name,age,sex) values('%s',%d,'%s')","zhang san", 20, "M");conn.connect("127.0.0.1", 3306, "root", "123456", "chat");conn.update(sql);}});thread t2([]() {for (int i = 0; i < 250; ++i){Connection conn;char sql[1024] = { 0 };sprintf(sql, "insert into user(name,age,sex) values('%s',%d,'%s')","zhang san", 20, "M");conn.connect("127.0.0.1", 3306, "root", "123456", "chat");conn.update(sql);}});thread t3([]() {for (int i = 0; i < 250; ++i){Connection conn;char sql[1024] = { 0 };sprintf(sql, "insert into user(name,age,sex) values('%s',%d,'%s')","zhang san", 20, "M");conn.connect("127.0.0.1", 3306, "root", "123456", "chat");conn.update(sql);}});thread t4([]() {for (int i = 0; i < 250; ++i){Connection conn;char sql[1024] = { 0 };sprintf(sql, "insert into user(name,age,sex) values('%s',%d,'%s')","zhang san", 20, "M");conn.connect("127.0.0.1", 3306, "root", "123456", "chat");conn.update(sql);}});t1.join();t2.join();t3.join();t4.join();clock_t end = clock();cout << (end - begin) << "ms" << endl;return 0;}

运行时间如下:

C++怎么实现数据库连接池

使用连接池

单线程

int main(){clock_t begin = clock();ConnectionPool* cp = ConnectionPool::getConnectionPool();for (int i = 0; i < 1000; ++i){shared_ptr<Connection> sp = cp->getConnection();char sql[1024] = { 0 };sprintf(sql, "insert into user(name,age,sex) values('%s',%d,'%s')","zhang san", 20, "M");sp->update(sql);}clock_t end = clock();cout << (end - begin) << "ms" << endl;return 0;}

运行时间如下

C++怎么实现数据库连接池

多线程

int main(){clock_t begin = clock();thread t1([]() {ConnectionPool* cp = ConnectionPool::getConnectionPool();for (int i = 0; i < 250; ++i){char sql[1024] = { 0 };sprintf(sql, "insert into user(name,age,sex) values('%s',%d,'%s')","zhang san", 20, "M");shared_ptr<Connection> sp = cp->getConnection();sp->update(sql);}});thread t2([]() {ConnectionPool* cp = ConnectionPool::getConnectionPool();for (int i = 0; i < 250; ++i){char sql[1024] = { 0 };sprintf(sql, "insert into user(name,age,sex) values('%s',%d,'%s')","zhang san", 20, "M");shared_ptr<Connection> sp = cp->getConnection();sp->update(sql);}});thread t3([]() {ConnectionPool* cp = ConnectionPool::getConnectionPool();for (int i = 0; i < 250; ++i){char sql[1024] = { 0 };sprintf(sql, "insert into user(name,age,sex) values('%s',%d,'%s')","zhang san", 20, "M");shared_ptr<Connection> sp = cp->getConnection();sp->update(sql);}});thread t4([]() {ConnectionPool* cp = ConnectionPool::getConnectionPool();for (int i = 0; i < 250; ++i){char sql[1024] = { 0 };sprintf(sql, "insert into user(name,age,sex) values('%s',%d,'%s')","zhang san", 20, "M");shared_ptr<Connection> sp = cp->getConnection();sp->update(sql);}});t1.join();t2.join();t3.join();t4.join();clock_t end = clock();cout << (end - begin) << "ms" << endl;return 0;}

C++怎么实现数据库连接池

比较

在使用了连接池之后,性能确实提升了不少

  • 数据量1000,单线程从1417ms变成697ms

  • 数据量1000,多线程从420ms变成了307ms

读到这里,这篇“C++怎么实现数据库连接池”文章已经介绍完毕,想要掌握这篇文章的知识点还需要大家自己动手实践使用过才能领会,如果想了解更多相关内容的文章,欢迎关注编程网其他教程频道。

--结束END--

本文标题: C++怎么实现数据库连接池

本文链接: https://www.lsjlt.com/news/352864.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • C++怎么实现数据库连接池
    本文小编为大家详细介绍“C++怎么实现数据库连接池”,内容详细,步骤清晰,细节处理妥当,希望这篇“C++怎么实现数据库连接池”文章能帮助大家解决疑惑,下面跟着小编的思路慢慢深入,一起来学习新知识吧。项目背景为了提高Mysql数据库的访问瓶颈...
    99+
    2023-07-05
  • c#怎么用mysql数据库连接池
    c#用mysql数据库连接池的示例:class DbConn{private const int MaxPool=10;//最大连接数private const int MinPool=5;//最小连接数private const bool...
    99+
    2022-10-13
  • JDBC数据库连接池 怎么实现
    本篇内容介绍了“JDBC数据库连接池 怎么实现”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!什么情况下使用连接池对于一个简单的数据库应用,由...
    99+
    2023-06-02
  • 数据库连接池的原理?连接池使用什么数据结构实现?实现连接池?
    早期我们怎么进行数据库操作呢? 1、原理:一般来说,Java应用程序访问数据库的过程是: 加载数据库驱动程序; 通过jdbc建立数据库连接; 访问数据库,执行SQL语句; 断开数据库连接。 2、代码 1 //查询所有用户 ...
    99+
    2015-09-26
    数据库连接池的原理?连接池使用什么数据结构实现?实现连接池?
  • Python中怎么实现数据库连接池
    这篇文章将为大家详细讲解有关Python中怎么实现数据库连接池,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。示例:#-*-coding:utf-8-*-  import ...
    99+
    2023-06-17
  • 基于C++实现Mysql数据库连接池实例
    目录项目技术点项目意义项目实现Connection设计ConnectionPool设计项目复杂接口细节刨析项目技术点 C语言进行mysql数据库编程无锁单例基于STL队列加C++11新特性保证线程安全实现的生产者消费者模...
    99+
    2022-12-07
    C++数据库连接池 C++mysql连接池
  • 项目之C++如何实现数据库连接池
    目录前言项目背景功能点介绍1、初始连接量(initSize)2、最大连接量(maxSize)3、最大空闲时间(maxIdleTime)4、连接超时时间(connectionTimeO...
    99+
    2023-03-23
    C++数据库连接池 数据库连接池 C++连接池
  • jdbc连接池怎么实现数据库访问
    JDBC连接池是一种管理数据库连接的技术,它可以提供可用的数据库连接给应用程序使用,从而提高数据库访问的性能和效率。以下是实现JDB...
    99+
    2023-09-15
    jdbc 数据库
  • C#数据库连接池的创建
    本篇内容主要讲解“C#数据库连接池的创建”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“C#数据库连接池的创建”吧!使用C#数据库连接池连接到数据库服务器通常由几个需要软长时间的步骤组成。必须建立...
    99+
    2023-06-18
  • 数据库连接池
    连接池是管理数据库连接的一种机制,能够控制连接的个数,默认情况下可以预先创建可用的连接。有四种常见的连接池框架1、Apache的DBCP连接池(Tomcat内置了DBCP)2、C3P0连接池3、proxco...
    99+
    2022-10-18
  • 基于MysqlConnector/C++的数据库连接池的实现是怎样的
    基于MysqlConnector/C++的数据库连接池的实现是怎样的,针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。1.连接池的介绍:1.1...
    99+
    2022-10-19
  • java数据库连接池如何实现
    Java数据库连接池可以通过以下步骤实现:1. 导入相应的依赖库:你需要导入数据库驱动程序和连接池的相关依赖库。一般来说,你需要使用...
    99+
    2023-09-16
    java 数据库
  • 如何用C++自己实现mysql数据库的连接池?
    为什么是mysql? 现在几乎所有的后台应用都要用到数据库,什么关系型的、非关系型的;正当关系的,不正当关系的;主流的和非主流的, 大到Oracle,小到sqlite,以及包括现在逐渐流行的基于物联网的时序数据库,比如涛思的TDengi...
    99+
    2015-05-02
    如何用C++自己实现mysql数据库的连接池?
  • 如何利用C++实现mysql数据库的连接池详解
    目录为什么是mysql? 为什么要搞资源池? mysql资源池实现的案例源码 头文件:MysqlPool.h实现文件:MysqlPool.cpp测试函数总结为什么是mysql? 现...
    99+
    2022-11-12
  • DBUtils数据库连接池
    使用数据库连接池技术,可以重复使用多个数据库连接,避免每次执行数据库操作都建立连接和关闭连接,也避免了大型应用同时占用多个数据库连接。 以连接mysql为例 import pymysql from DBUtils.PooledDB i...
    99+
    2023-01-31
    数据库 连接池 DBUtils
  • Scala数据库连接池的简单实现
    在使用JDBC的时候,数据库据连接是非常宝贵的资源。为了复用这些资源,可以将连接保存在一个队列中。当需要的时候可以从队列中取出未使用的连接。如果没有可用连接,则可以在一定时间内等待,...
    99+
    2023-02-09
    Scala数据库连接池 Scala 连接池
  • Python怎么封装数据库连接池
    这篇文章主要介绍“Python怎么封装数据库连接池”,在日常操作中,相信很多人在Python怎么封装数据库连接池问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”Python怎么封装数据库连接池”的疑惑有所帮助!...
    99+
    2023-07-02
  • 怎么配置mysql数据库连接池
    配置mysql数据库连接池的方法在CATALINA_HOME/conf/server.xml中添加如下配置信息;<!--声明连接池-->    <Resource name="jdbc/mysql&qu...
    99+
    2022-10-25
  • 怎么创建mysql数据库连接池
    在mysql中利用DBUtils创建数据库连接池的方法具体方法如下:import pymysqlfrom DBUtils.PooledDB import PooledDB, SharedDBConnectionPOOL = PooledDB...
    99+
    2022-10-05
  • python数据库连接池中数据库连接池技术的优点是什么
    本篇文章给大家分享的是有关python数据库连接池中数据库连接池技术的优点是什么,小编觉得挺实用的,因此分享给大家学习,希望大家阅读完这篇文章后可以有所收获,话不多说,跟着小编一起来看看吧。昨天测试了一下开500个线程去请求数据库,不过这个...
    99+
    2023-06-17
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作