workflow系列教程(7)mysql任务

2024-03-05 06:36

本文主要是介绍workflow系列教程(7)mysql任务,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

往期教程

如果觉得写的可以,请给一个点赞+关注支持一下

观看之前请先看,往期的博客教程,否则这篇博客没办法看懂

  • workFlow c++异步网络库编译教程与简介

  • C++异步网络库workflow入门教程(1)HTTP任务

  • C++异步网络库workflow系列教程(2)redis任务

  • workflow系列教程(3)Series串联任务流

  • workflow系列教程(4)Parallel并联任务流

  • workflow系列教程(5-1)HTTP Server

  • workflow系列教程(5-2)实现HTTP反向代理

  • workflow系列教程(6)实现静态资源服务器

异步MySQL客户端:mysql_cli

MySQL URL的格式

mysql://username:password@host:port/dbname?character_set=charset&character_set_results=charset

  • 如果以SSL连接访问MySQL,则scheme设为mysqls://。MySQL server 5.7及以上支持;

  • username和password按需填写;

  • port默认为3306;

  • dbname为要用的数据库名,一般如果SQL语句只操作一个db的话建议填写;

  • character_set为client的字符集,等价于使用官方客户端启动时的参数--default-character-set的配置,默认utf8,具体可以参考MySQL官方文档character-set.html。

  • character_set_results为client、connection和results的字符集,如果想要在SQL语句里使用SET NAME来指定这些字符集的话,请把它配置到url的这个位置。

MySQL URL示例:

mysql://root:password@127.0.0.1

mysql://@test.mysql.com:3306/db1?character_set=utf8&character_set_results=utf8

mysqls://localhost/db1?character_set=big5

创建并启动MySQL任务

用户可以使用WFTaskFactory创建MySQL任务,创建接口与回调函数的用法都与workflow其他任务类似:

using mysql_callback_t = std::function<void (WFMySQLTask *)>;WFMySQLTask *create_mysql_task(const std::string& url, int retry_max, mysql_callback_t callback);void set_query(const std::string& query);

用户创建完WFMySQLTask之后,可以对req调用 set_query() 写入SQL语句。

如果没调用过 set_query() ,task就被start起来的话,则用户会在callback里得到WFT_ERR_MYSQL_QUERY_NOT_SET

其他包括callback、series、user_data等与workflow其他task用法类似。

大致使用示例如下:

int main(int argc, char *argv[])
{...WFMySQLTask *task = WFTaskFactory::create_mysql_task(url, RETRY_MAX, mysql_callback);task->get_req()->set_query("SHOW TABLES;");...task->start();...
}

支持的命令

目前支持的命令为COM_QUERY,已经能涵盖用户基本的增删改查、建库删库、建表删表、预处理、使用存储过程和使用事务的需求。

因为我们的交互命令中不支持选库(USE命令),所以,如果SQL语句中有涉及到跨库的操作,则可以通过db_name.table_name的方式指定具体哪个库的哪张表。

其他所有命令都可以拼接到一起通过 set_query() 传给WFMySQLTask(包括INSERT/UPDATE/SELECT/DELETE/PREPARE/CALL)。

拼接的命令会被按序执行直到命令发生错误,前面的命令会执行成功。

举个例子:

req->set_query("SELECT * FROM table1; CALL procedure1(); INSERT INTO table3 (id) VALUES (1);");

结果解析

与workflow其他任务类似,可以用task->get_resp()拿到MySQLResponse,我们可以通过MySQLResultCursor遍历结果集。

一次请求所对应的回复中,其数据是一个三维结构:

  • 一个回复中包含了一个或多个结果集(result set);
  • 一个结果集的类型可能是MYSQL_STATUS_GET_RESULT或者MYSQL_STATUS_OK
  • MYSQL_STATUS_GET_RESULT类型的结果集包含了一行或多行(row);
  • 一行包含了一列或多个列,或者说一到多个阈(Field/Cell),具体数据结构为MySQLFieldMySQLCell

结果集的两种类型,可以通过cursor->get_cursor_status()进行判断:

MYSQL_STATUS_GET_RESULTMYSQL_STATUS_OK
SQL命令SELECT(包括存储过程中的每一个SELECT)INSERT / UPDATE / DELETE / …
对应语义读操作,一个结果集表示一份读操作返回的二维表写操作,一个结果集表示一个写操作是否成功
主要接口fetch_fields();fetch_row(&row_arr);…get_insert_id();get_affected_rows();…

由于拼接语句可能存在错误,因此这种情况,可以通过MySQLResultCursor拿到前面正确执行过的语句多个结果集,以及最后判断resp->get_packet_type()MYSQL_PACKET_ERROR时,通过resp->get_error_code()resp->get_error_msg()拿到具体错误信息。

一个包含n条SELECT语句的存储过程,会返回n个MYSQL_STATUS_GET_RESULT的结果集和1个MYSQL_STATUS_OK的结果集,用户自行忽略此MYSQL_STATUS_OK结果集即可。

具体使用从外到内的步骤应该是:

  1. 判断任务状态(代表通信层面状态):用户通过判断 task->get_state() 等于WFT_STATE_SUCCESS来查看任务执行是否成功;

  2. 判断回复包类型(代表返回包解析状态):调用 resp->get_packet_type() 查看最后一条MySQL语句的返回包类型,常见的几个类型为:

  • MYSQL_PACKET_OK:成功,可以用cursor遍历结果;
  • MYSQL_PACKET_EOF:成功,可以用cursor遍历结果;
  • MYSQL_PACKET_ERROR:失败或部分失败,成功的部分可以用cursor遍历结果;
  1. 遍历结果集。用户可以使用MySQLResultCursor读取结果集中的内容,因为MySQL server返回的数据是多结果集的,因此一开始cursor会自动指向第一个结果集的读取位置。

  2. 判断结果集状态(代表结果集读取状态):通过 cursor->get_cursor_status() 可以拿到的几种状态:

  • MYSQL_STATUS_GET_RESULT:此结果集为读请求类型;
  • MYSQL_STATUS_END:读结果集已读完最后一行;
  • MYSQL_STATUS_OK:此结果集为写请求类型;
  • MYSQL_STATUS_ERROR:解析错误;
  1. 读取MYSQL_STATUS_OK结果集中的基本内容:
  • unsigned long long get_affected_rows() const;
  • unsigned long long get_insert_id() const;
  • int get_warnings() const;
  • std::string get_info() const;
  1. 读取MYSQL_STATUS_GET_RESULT结果集中的columns中每个field:
  • int get_field_count() const;
  • const MySQLField *fetch_field();
    • const MySQLField *const *fetch_fields() const;
  1. 读取MYSQL_STATUS_GET_RESULT结果集中的每一行:按行读取可以使用 cursor->fetch_row() 直到返回值为false。其中会移动cursor内部对当前结果集的指向每行的offset:
  • int get_rows_count() const;
  • bool fetch_row(std::vector<MySQLCell>& row_arr);
  • bool fetch_row(std::map<std::string, MySQLCell>& row_map);
  • bool fetch_row(std::unordered_map<std::string, MySQLCell>& row_map);
  • bool fetch_row_nocopy(const void **data, size_t *len, int *data_type);
  1. 直接把当前MYSQL_STATUS_GET_RESULT结果集的所有行拿出:所有行的读取可以使用 cursor->fetch_all() ,内部用来记录行的cursor会直接移动到最后;当前cursor状态会变成MYSQL_STATUS_END
  • bool fetch_all(std::vector<std::vector<MySQLCell>>& rows);
  1. 返回当前MYSQL_STATUS_GET_RESULT结果集的头部:如果有必要重读这个结果集,可以使用 cursor->rewind() 回到当前结果集头部,再通过第7步或第8步进行读取;

  2. 拿到下一个结果集:因为MySQL server返回的数据包可能是包含多结果集的(比如每个select/insert/…语句为一个结果集;或者call procedure返回的多结果集数据),因此用户可以通过 cursor->next_result_set() 跳到下一个结果集,返回值为false表示所有结果集已取完。

  3. 返回第一个结果集:cursor->first_result_set() 可以让我们返回到所有结果集的头部,然后可以从第4步开始重新拿数据;

  4. MYSQL_STATUS_GET_RESULT结果集每列具体数据MySQLCell:第7步中读取到的一行,由多列组成,每列结果为MySQLCell,基本使用接口有:

  • int get_data_type(); 返回MYSQL_TYPE_LONG、MYSQL_TYPE_STRING…
  • bool is_TYPE() const; TYPE为int、string、ulonglong,判断是否是某种类型
  • TYPE as_TYPE() const; 同上,以某种类型读出MySQLCell的数据
  • void get_cell_nocopy(const void **data, size_t *len, int *data_type) const; nocopy接口

整体示例如下:

void task_callback(WFMySQLTask *task)
{// step-1. 判断任务状态 if (task->get_state() != WFT_STATE_SUCCESS){fprintf(stderr, "task error = %d\n", task->get_error());return;}MySQLResultCursor cursor(task->get_resp());bool test_first_result_set_flag = false;bool test_rewind_flag = false;// step-2. 判断回复包其他状态if (resp->get_packet_type() == MYSQL_PACKET_ERROR){fprintf(stderr, "ERROR. error_code=%d %s\n",task->get_resp()->get_error_code(),task->get_resp()->get_error_msg().c_str());}begin:// step-3. 遍历结果集do {// step-4. 判断结果集状态if (cursor.get_cursor_status() == MYSQL_STATUS_OK){// step-5. MYSQL_STATUS_OK结果集的基本内容fprintf(stderr, "OK. %llu rows affected. %d warnings. insert_id=%llu.\n",cursor.get_affected_rows(), cursor.get_warnings(), cursor.get_insert_id());}else if (cursor.get_cursor_status() == MYSQL_STATUS_GET_RESULT){fprintf(stderr, "field_count=%u rows_count=%u ",cursor.get_field_count(), cursor.get_rows_count());// step-6. 读取每个fields。这是个nocopy apiconst MySQLField *const *fields = cursor.fetch_fields();for (int i = 0; i < cursor.get_field_count(); i++){fprintf(stderr, "db=%s table=%s name[%s] type[%s]\n",fields[i]->get_db().c_str(), fields[i]->get_table().c_str(),fields[i]->get_name().c_str(), datatype2str(fields[i]->get_data_type()));}// step-8. 把所有行读出,也可以while (cursor.fetch_row(map/vector)) 按step-7拿每一行std::vector<std::vector<MySQLCell>> rows;cursor.fetch_all(rows);for (unsigned int j = 0; j < rows.size(); j++){// step-12. 具体每个cell的读取for (unsigned int i = 0; i < rows[j].size(); i++){fprintf(stderr, "[%s][%s]", fields[i]->get_name().c_str(),datatype2str(rows[j][i].get_data_type()));// step-12. 判断具体类型is_string()和转换具体类型as_string()if (rows[j][i].is_string()){std::string res = rows[j][i].as_string();fprintf(stderr, "[%s]\n", res.c_str());}else if (rows[j][i].is_int()){fprintf(stderr, "[%d]\n", rows[j][i].as_int());} // else if ...}}}// step-10. 拿下一个结果集} while (cursor.next_result_set());if (test_first_result_set_flag == false){test_first_result_set_flag = true;// step-11. 返回第一个结果集cursor.first_result_set();goto begin;}if (test_rewind_flag == false){test_rewind_flag = true;// step-9. 返回当前结果集头部cursor.rewind();goto begin;}return;
}

完整示例代码

#include <assert.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
#include <vector>
#include <map>
#include "workflow/Workflow.h"
#include "workflow/WFTaskFactory.h"
#include "workflow/MySQLResult.h"
#include "workflow/WFFacilities.h"using namespace protocol;#define RETRY_MAX       0volatile bool stop_flag;void mysql_callback(WFMySQLTask *task);void get_next_cmd(WFMySQLTask *task)
{int len;char query[4096];WFMySQLTask *next_task;fprintf(stderr, "mysql> ");while ((fgets(query, 4096, stdin)) && stop_flag == false){len = strlen(query);if (len > 0 && query[len - 1] == '\n')query[len - 1] = '\0';if (strncmp(query, "quit", len) == 0 || strncmp(query, "exit", len) == 0){fprintf(stderr, "Bye\n");return;}if (len == 0 || strncmp(query, "\0", len) == 0){fprintf(stderr, "mysql> ");continue;}std::string *url = (std::string *)series_of(task)->get_context();next_task = WFTaskFactory::create_mysql_task(*url, RETRY_MAX, mysql_callback);next_task->get_req()->set_query(query);series_of(task)->push_back(next_task);	break;}return;
}void mysql_callback(WFMySQLTask *task)
{MySQLResponse *resp = task->get_resp();MySQLResultCursor cursor(resp);const MySQLField *const *fields;std::vector<MySQLCell> arr;if (task->get_state() != WFT_STATE_SUCCESS){fprintf(stderr, "error msg: %s\n",WFGlobal::get_error_string(task->get_state(),task->get_error()));return;}do {if (cursor.get_cursor_status() != MYSQL_STATUS_GET_RESULT &&cursor.get_cursor_status() != MYSQL_STATUS_OK){break;}fprintf(stderr, "---------------- RESULT SET ----------------\n");if (cursor.get_cursor_status() == MYSQL_STATUS_GET_RESULT){fprintf(stderr, "cursor_status=%d field_count=%u rows_count=%u\n",cursor.get_cursor_status(), cursor.get_field_count(),cursor.get_rows_count());//nocopy apifields = cursor.fetch_fields();for (int i = 0; i < cursor.get_field_count(); i++){if (i == 0){fprintf(stderr, "db=%s table=%s\n",fields[i]->get_db().c_str(), fields[i]->get_table().c_str());fprintf(stderr, "  ---------- COLUMNS ----------\n");}fprintf(stderr, "  name[%s] type[%s]\n",fields[i]->get_name().c_str(),datatype2str(fields[i]->get_data_type()));}fprintf(stderr, "  _________ COLUMNS END _________\n\n");while (cursor.fetch_row(arr)){fprintf(stderr, "  ------------ ROW ------------\n");for (size_t i = 0; i < arr.size(); i++){fprintf(stderr, "  [%s][%s]", fields[i]->get_name().c_str(),datatype2str(arr[i].get_data_type()));if (arr[i].is_string()){std::string res = arr[i].as_string();if (res.length() == 0)fprintf(stderr, "[\"\"]\n");else fprintf(stderr, "[%s]\n", res.c_str());} else if (arr[i].is_int()) {fprintf(stderr, "[%d]\n", arr[i].as_int());} else if (arr[i].is_ulonglong()) {fprintf(stderr, "[%llu]\n", arr[i].as_ulonglong());} else if (arr[i].is_float()) {const void *ptr;size_t len;int data_type;arr[i].get_cell_nocopy(&ptr, &len, &data_type);size_t pos;for (pos = 0; pos < len; pos++)if (*((const char *)ptr + pos) == '.')break;if (pos != len)pos = len - pos - 1;elsepos = 0;fprintf(stderr, "[%.*f]\n", (int)pos, arr[i].as_float());} else if (arr[i].is_double()) {const void *ptr;size_t len;int data_type;arr[i].get_cell_nocopy(&ptr, &len, &data_type);size_t pos;for (pos = 0; pos < len; pos++)if (*((const char *)ptr + pos) == '.')break;if (pos != len)pos = len - pos - 1;elsepos= 0;fprintf(stderr, "[%.*lf]\n", (int)pos, arr[i].as_double());} else if (arr[i].is_date()) {fprintf(stderr, "[%s]\n", arr[i].as_string().c_str());} else if (arr[i].is_time()) {fprintf(stderr, "[%s]\n", arr[i].as_string().c_str());} else if (arr[i].is_datetime()) {fprintf(stderr, "[%s]\n", arr[i].as_string().c_str());} else if (arr[i].is_null()) {fprintf(stderr, "[NULL]\n");} else {std::string res = arr[i].as_binary_string();if (res.length() == 0)fprintf(stderr, "[\"\"]\n");else fprintf(stderr, "[%s]\n", res.c_str());}}fprintf(stderr, "  __________ ROW END __________\n");}}else if (cursor.get_cursor_status() == MYSQL_STATUS_OK){fprintf(stderr, "  OK. %llu ", cursor.get_affected_rows());if (cursor.get_affected_rows() == 1)fprintf(stderr, "row ");elsefprintf(stderr, "rows ");fprintf(stderr, "affected. %d warnings. insert_id=%llu. %s\n",cursor.get_warnings(), cursor.get_insert_id(),cursor.get_info().c_str());}fprintf(stderr, "________________ RESULT SET END ________________\n\n");} while (cursor.next_result_set());if (resp->get_packet_type() == MYSQL_PACKET_ERROR){fprintf(stderr, "ERROR. error_code=%d %s\n",task->get_resp()->get_error_code(),task->get_resp()->get_error_msg().c_str());}else if (resp->get_packet_type() == MYSQL_PACKET_OK) // just check origin APIs{fprintf(stderr, "OK. %llu ", task->get_resp()->get_affected_rows());if (task->get_resp()->get_affected_rows() == 1)fprintf(stderr, "row ");elsefprintf(stderr, "rows ");fprintf(stderr, "affected. %d warnings. insert_id=%llu. %s\n",task->get_resp()->get_warnings(),task->get_resp()->get_last_insert_id(),task->get_resp()->get_info().c_str());}get_next_cmd(task);return;
}static void sighandler(int signo)
{stop_flag = true;
}int main(int argc, char *argv[])
{WFMySQLTask *task;if (argc != 2){fprintf(stderr, "USAGE: %s <url>\n""      url format: mysql://root:password@host:port/dbname?character_set=charset\n""      example: mysql://root@test.mysql.com/test\n",argv[0]);return 0;}signal(SIGINT, sighandler);signal(SIGTERM, sighandler);std::string url = argv[1];if (strncasecmp(argv[1], "mysql://", 8) != 0 &&strncasecmp(argv[1], "mysqls://", 9) != 0){url = "mysql://" + url;}const char *query = "show databases";stop_flag = false;task = WFTaskFactory::create_mysql_task(url, RETRY_MAX, mysql_callback);task->get_req()->set_query(query);WFFacilities::WaitGroup wait_group(1);SeriesWork *series = Workflow::create_series_work(task,[&wait_group](const SeriesWork *series) {wait_group.done();});series->set_context(&url);series->start();wait_group.wait();return 0;
}

WFMySQLConnection

由于我们是高并发异步客户端,这意味着我们对一个server的连接可能会不止一个。而MySQL的事务和预处理都是带状态的,为了保证一次事务或预处理独占一个连接,用户可以使用我们封装的二级工厂WFMySQLConnection来创建任务,每个WFMySQLConnection保证独占一个连接,

1. WFMySQLConnection的创建与初始化

创建一个WFMySQLConnection的时候需要传入一个id,必须全局唯一,之后的调用内部都会由这个id去唯一找到对应的那个连接。

初始化需要传入url,之后在这个connection上创建的任务就不需要再设置url了。

class WFMySQLConnection
{
public:WFMySQLConnection(int id);int init(const std::string& url);...
};

2. 创建任务与关闭连接

通过 create_query_task() ,写入SQL请求和回调函数即可创建任务,该任务一定从这一个connection发出。

有时候我们需要手动关闭这个连接。因为当我们不再使用它的时候,这个连接会一直保持到MySQL server超时。期间如果使用同一个id和url去创建WFMySQLConnection的话就可以复用到这个连接。

因此我们建议如果不准备复用连接,应使用 create_disconnect_task() 创建一个任务,手动关闭这个连接。

class WFMySQLConnection
{
public:...WFMySQLTask *create_query_task(const std::string& query,mysql_callback_t callback);WFMySQLTask *create_disconnect_task(mysql_callback_t callback);
}

WFMySQLConnection相当于一个二级工厂,我们约定任何工厂对象的生命周期无需保持到任务结束,以下代码完全合法:

    WFMySQLConnection *conn = new WFMySQLConnection(1234);conn->init(url);auto *task = conn->create_query_task("SELECT * from table", my_callback);conn->deinit();delete conn;task->start();

3. 注意事项

如果在使用事务期间已经开始BEGIN但还没有COMMIT或ROLLBACK,且期间连接发生过中断,则连接会被框架内部自动重连,用户会在下一个task请求中拿到ECONNRESET错误。此时还没COMMIT的事务语句已经失效,需要重新再发一遍。

4. 预处理

用户也可以通过WFMySQLConnection来做预处理PREPARE,因此用户可以很方便地用作防SQL注入。如果连接发生了重连,也会得到一个ECONNRESET错误。

5. 完整示例

WFMySQLConnection conn(1);
conn.init("mysql://root@127.0.0.1/test");// test transaction
const char *query = "BEGIN;";
WFMySQLTask *t1 = conn.create_query_task(query, task_callback);
query = "SELECT * FROM check_tiny FOR UPDATE;";
WFMySQLTask *t2 = conn.create_query_task(query, task_callback);
query = "INSERT INTO check_tiny VALUES (8);";
WFMySQLTask *t3 = conn.create_query_task(query, task_callback);
query = "COMMIT;";
WFMySQLTask *t4 = conn.create_query_task(query, task_callback);
WFMySQLTask *t5 = conn.create_disconnect_task(task_callback);
((*t1) > t2 > t3 > t4 > t5).start();

这篇关于workflow系列教程(7)mysql任务的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/775595

相关文章

mysql递归查询语法WITH RECURSIVE的使用

《mysql递归查询语法WITHRECURSIVE的使用》本文主要介绍了mysql递归查询语法WITHRECURSIVE的使用,WITHRECURSIVE用于执行递归查询,特别适合处理层级结构或递归... 目录基本语法结构:关键部分解析:递归查询的工作流程:示例:员工与经理的层级关系解释:示例:树形结构的数

SQL常用操作精华之复制表、跨库查询、删除重复数据

《SQL常用操作精华之复制表、跨库查询、删除重复数据》:本文主要介绍SQL常用操作精华之复制表、跨库查询、删除重复数据,这些SQL操作涵盖了数据库开发中最常用的技术点,包括表操作、数据查询、数据管... 目录SQL常用操作精华总结表结构与数据操作高级查询技巧SQL常用操作精华总结表结构与数据操作复制表结

查看MySQL数据库版本的四种方法

《查看MySQL数据库版本的四种方法》查看MySQL数据库的版本信息可以通过多种方法实现,包括使用命令行工具、SQL查询语句和图形化管理工具等,以下是详细的步骤和示例代码,需要的朋友可以参考下... 目录方法一:使用命令行工具1. 使用 mysql 命令示例:方法二:使用 mysqladmin 命令示例:方

MySQL 复合查询案例详解

《MySQL复合查询案例详解》:本文主要介绍MySQL复合查询案例详解,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录基本查询回顾多表笛卡尔积子查询与where子查询多行子查询多列子查询子查询与from总结合并查询(不太重要)union基本查询回顾查询

Django之定时任务django-crontab的实现

《Django之定时任务django-crontab的实现》Django可以使用第三方库如django-crontab来实现定时任务的调度,本文主要介绍了Django之定时任务django-cront... 目录crontab安装django-crontab注册应用定时时间格式定时时间示例设置定时任务@符号

MySQL复合查询从基础到多表关联与高级技巧全解析

《MySQL复合查询从基础到多表关联与高级技巧全解析》本文主要讲解了在MySQL中的复合查询,下面是关于本文章所需要数据的建表语句,感兴趣的朋友跟随小编一起看看吧... 目录前言:1.基本查询回顾:1.1.查询工资高于500或岗位为MANAGER的雇员,同时还要满足他们的姓名首字母为大写的J1.2.按照部门

Linux搭建单机MySQL8.0.26版本的操作方法

《Linux搭建单机MySQL8.0.26版本的操作方法》:本文主要介绍Linux搭建单机MySQL8.0.26版本的操作方法,本文通过图文并茂的形式给大家讲解的非常详细,感兴趣的朋友一起看看吧... 目录概述环境信息数据库服务安装步骤下载前置依赖服务下载方式一:进入官网下载,并上传到宿主机中,适合离线环境

MySQL主从同步延迟问题的全面解决方案

《MySQL主从同步延迟问题的全面解决方案》MySQL主从同步延迟是分布式数据库系统中的常见问题,会导致从库读取到过期数据,影响业务一致性,下面我将深入分析延迟原因并提供多层次的解决方案,需要的朋友可... 目录一、同步延迟原因深度分析1.1 主从复制原理回顾1.2 延迟产生的关键环节二、实时监控与诊断方案

Android实现定时任务的几种方式汇总(附源码)

《Android实现定时任务的几种方式汇总(附源码)》在Android应用中,定时任务(ScheduledTask)的需求几乎无处不在:从定时刷新数据、定时备份、定时推送通知,到夜间静默下载、循环执行... 目录一、项目介绍1. 背景与意义二、相关基础知识与系统约束三、方案一:Handler.postDel

慢sql提前分析预警和动态sql替换-Mybatis-SQL

《慢sql提前分析预警和动态sql替换-Mybatis-SQL》为防止慢SQL问题而开发的MyBatis组件,该组件能够在开发、测试阶段自动分析SQL语句,并在出现慢SQL问题时通过Ducc配置实现动... 目录背景解决思路开源方案调研设计方案详细设计使用方法1、引入依赖jar包2、配置组件XML3、核心配