This commit is contained in:
陈梓阳 2024-01-04 09:00:44 +08:00
parent c53d2927bb
commit eab3695f53
27 changed files with 258 additions and 466 deletions

View File

@ -1,4 +1,4 @@
namespace ConsoleApp2; namespace ConsoleApp2.Const;
public static class ProcessStep public static class ProcessStep
{ {

View File

@ -1,6 +1,5 @@
using System.Text; namespace ConsoleApp2;
namespace ConsoleApp2.Entities;
public class DataRecord public class DataRecord
{ {
@ -9,7 +8,7 @@ public class DataRecord
value = string.Empty; value = string.Empty;
if (record.Headers is null) if (record.Headers is null)
throw new InvalidOperationException("Cannot get field when headers of a record have not been set."); throw new InvalidOperationException("Cannot get field when headers of a record have not been set.");
var idx = Array.IndexOf(record.Headers, columnName); var idx = Array.IndexOf(record.Headers, columnName); //可能可以优化
if (idx == -1) if (idx == -1)
return false; return false;
value = record.Fields[idx]; value = record.Fields[idx];

View File

@ -1,6 +1,5 @@
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using ConsoleApp2.Entities;
namespace ConsoleApp2.Helpers; namespace ConsoleApp2.Helpers;

View File

@ -1,249 +0,0 @@
using System.Security.Cryptography;
using System.Text;
namespace ConsoleApp2.Helpers;
public static class HashExtensions
{
/// <summary>
/// 计算32位MD5码
/// </summary>
/// <param name="word">字符串</param>
/// <param name="toUpper">返回哈希值格式 true英文大写false英文小写</param>
/// <returns></returns>
public static string ToMd5Hash(this string word, bool toUpper = true)
{
try
{
var MD5CSP = MD5.Create();
var bytValue = Encoding.UTF8.GetBytes(word);
var bytHash = MD5CSP.ComputeHash(bytValue);
MD5CSP.Clear();
//根据计算得到的Hash码翻译为MD5码
var sHash = "";
foreach (var t in bytHash)
{
long i = t / 16;
var sTemp = i > 9 ? ((char)(i - 10 + 0x41)).ToString() : ((char)(i + 0x30)).ToString();
i = t % 16;
if (i > 9)
{
sTemp += ((char)(i - 10 + 0x41)).ToString();
}
else
{
sTemp += ((char)(i + 0x30)).ToString();
}
sHash += sTemp;
}
//根据大小写规则决定返回的字符串
return toUpper ? sHash : sHash.ToLower();
}
catch (System.Exception ex)
{
throw new System.Exception(ex.Message);
}
}
public static string ToMd5Hash(this Stream stream, bool toUpper = true)
{
using var md5Hash = MD5.Create();
var bytes = md5Hash.ComputeHash(stream);
return ToHashString(bytes, toUpper);
}
/// <summary>
/// 计算SHA-1码
/// </summary>
/// <param name="word">字符串</param>
/// <param name="toUpper">返回哈希值格式 true英文大写false英文小写</param>
/// <returns></returns>
public static string ToSHA1Hash(this string word, bool toUpper = true)
{
try
{
var SHA1CSP = SHA1.Create();
var bytValue = Encoding.UTF8.GetBytes(word);
var bytHash = SHA1CSP.ComputeHash(bytValue);
SHA1CSP.Clear();
//根据计算得到的Hash码翻译为SHA-1码
var sHash = "";
foreach (var t in bytHash)
{
long i = t / 16;
var sTemp = i > 9 ? ((char)(i - 10 + 0x41)).ToString() : ((char)(i + 0x30)).ToString();
i = t % 16;
if (i > 9)
{
sTemp += ((char)(i - 10 + 0x41)).ToString();
}
else
{
sTemp += ((char)(i + 0x30)).ToString();
}
sHash += sTemp;
}
//根据大小写规则决定返回的字符串
return toUpper ? sHash : sHash.ToLower();
}
catch (System.Exception ex)
{
throw new System.Exception(ex.Message);
}
}
/// <summary>
/// 计算SHA-256码
/// </summary>
/// <param name="word">字符串</param>
/// <param name="toUpper">返回哈希值格式 true英文大写false英文小写</param>
/// <returns></returns>
public static string ToSHA256Hash(this string word, bool toUpper = true)
{
try
{
var SHA256CSP = SHA256.Create();
var bytValue = Encoding.UTF8.GetBytes(word);
var bytHash = SHA256CSP.ComputeHash(bytValue);
SHA256CSP.Clear();
//根据计算得到的Hash码翻译为SHA-1码
var sHash = "";
foreach (var t in bytHash)
{
long i = t / 16;
var sTemp = i > 9 ? ((char)(i - 10 + 0x41)).ToString() : ((char)(i + 0x30)).ToString();
i = t % 16;
if (i > 9)
{
sTemp += ((char)(i - 10 + 0x41)).ToString();
}
else
{
sTemp += ((char)(i + 0x30)).ToString();
}
sHash += sTemp;
}
//根据大小写规则决定返回的字符串
return toUpper ? sHash : sHash.ToLower();
}
catch (System.Exception ex)
{
throw new System.Exception(ex.Message);
}
}
/// <summary>
/// 计算SHA-256码
/// </summary>
/// <param name="stream"></param>
/// <param name="toUpper"></param>
/// <returns></returns>
public static string ToSHA256Hash(this Stream stream, bool toUpper = true)
{
using var sha256Hash = SHA256.Create();
var bytes = sha256Hash.ComputeHash(stream);
return ToHashString(bytes, toUpper);
}
/// <summary>
/// 计算SHA-384码
/// </summary>
/// <param name="word">字符串</param>
/// <param name="toUpper">返回哈希值格式 true英文大写false英文小写</param>
/// <returns></returns>
public static string ToSHA384Hash(this string word, bool toUpper = true)
{
try
{
var SHA384CSP = SHA384.Create();
var bytValue = Encoding.UTF8.GetBytes(word);
var bytHash = SHA384CSP.ComputeHash(bytValue);
SHA384CSP.Clear();
//根据计算得到的Hash码翻译为SHA-1码
var sHash = "";
foreach (var t in bytHash)
{
long i = t / 16;
var sTemp = i > 9 ? ((char)(i - 10 + 0x41)).ToString() : ((char)(i + 0x30)).ToString();
i = t % 16;
if (i > 9)
{
sTemp += ((char)(i - 10 + 0x41)).ToString();
}
else
{
sTemp += ((char)(i + 0x30)).ToString();
}
sHash += sTemp;
}
//根据大小写规则决定返回的字符串
return toUpper ? sHash : sHash.ToLower();
}
catch (System.Exception ex)
{
throw new System.Exception(ex.Message);
}
}
/// <summary>
/// 计算SHA-512码
/// </summary>
/// <param name="word">字符串</param>
/// <param name="toUpper">返回哈希值格式 true英文大写false英文小写</param>
/// <returns></returns>
public static string ToSHA512Hash(this string word, bool toUpper = true)
{
try
{
var SHA512CSP = SHA512.Create();
var bytValue = Encoding.UTF8.GetBytes(word);
var bytHash = SHA512CSP.ComputeHash(bytValue);
SHA512CSP.Clear();
//根据计算得到的Hash码翻译为SHA-1码
var sHash = "";
foreach (var t in bytHash)
{
long i = t / 16;
var sTemp = i > 9 ? ((char)(i - 10 + 0x41)).ToString() : ((char)(i + 0x30)).ToString();
i = t % 16;
if (i > 9)
{
sTemp += ((char)(i - 10 + 0x41)).ToString();
}
else
{
sTemp += ((char)(i + 0x30)).ToString();
}
sHash += sTemp;
}
//根据大小写规则决定返回的字符串
return toUpper ? sHash : sHash.ToLower();
}
catch (System.Exception ex)
{
throw new System.Exception(ex.Message);
}
}
private static string ToHashString(byte[] bytes, bool toUpper = true)
{
var builder = new StringBuilder();
foreach (var t in bytes)
{
builder.Append(t.ToString("x2"));
}
var str = builder.ToString();
return toUpper ? str.ToUpper() : str.ToLower();
}
}

View File

@ -0,0 +1,6 @@
namespace ConsoleApp2.HostedServices.Abstractions;
public interface IInputService
{
public Task ExecuteAsync(CancellationToken cancellationToken);
}

View File

@ -0,0 +1,6 @@
namespace ConsoleApp2.HostedServices.Abstractions;
public interface IOutputService
{
public Task ExecuteAsync(CancellationToken cancellationToken);
}

View File

@ -0,0 +1,6 @@
namespace ConsoleApp2.HostedServices.Abstractions;
public interface ITransformService
{
public Task ExecuteAsync(CancellationToken cancellationToken);
}

View File

@ -1,35 +1,36 @@
using ConsoleApp2.Helpers; using ConsoleApp2.Const;
using ConsoleApp2.Helpers;
using ConsoleApp2.HostedServices.Abstractions;
using ConsoleApp2.Options; using ConsoleApp2.Options;
using ConsoleApp2.Services; using ConsoleApp2.Services;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
namespace ConsoleApp2.HostedServices; namespace ConsoleApp2.HostedServices;
public class CsvInputService : BackgroundService /// <summary>
/// 从MyDumper导出的CSV文件中导入表头和数据
/// </summary>
public class InputService : IInputService
{ {
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IOptions<CsvOptions> _csvOptions; private readonly IOptions<CsvOptions> _csvOptions;
private readonly TaskManager _taskManager; // TBD
private readonly DataRecordQueue _producerQueue; private readonly DataRecordQueue _producerQueue;
private readonly ProcessContext _context; private readonly ProcessContext _context;
public CsvInputService(ILogger<CsvInputService> logger, public InputService(ILogger<InputService> logger,
IOptions<CsvOptions> csvOptions, IOptions<CsvOptions> csvOptions,
[FromKeyedServices(ProcessStep.Producer)]TaskManager taskManager,
[FromKeyedServices(ProcessStep.Producer)]DataRecordQueue producerQueue, [FromKeyedServices(ProcessStep.Producer)]DataRecordQueue producerQueue,
ProcessContext context) ProcessContext context)
{ {
_logger = logger; _logger = logger;
_csvOptions = csvOptions; _csvOptions = csvOptions;
_taskManager = taskManager;
_producerQueue = producerQueue; _producerQueue = producerQueue;
_context = context; _context = context;
} }
protected override async Task ExecuteAsync(CancellationToken cancellationToken) public async Task ExecuteAsync(CancellationToken cancellationToken)
{ {
var inputDir = _csvOptions.Value.InputDir; var inputDir = _csvOptions.Value.InputDir;
_logger.LogInformation("***** Csv input service start, working dir: {InputDir}, thread id: {ThreadId} *****", inputDir, Environment.CurrentManagedThreadId); _logger.LogInformation("***** Csv input service start, working dir: {InputDir}, thread id: {ThreadId} *****", inputDir, Environment.CurrentManagedThreadId);
@ -50,7 +51,7 @@ public class CsvInputService : BackgroundService
{ {
var csvPath = Path.Combine(inputDir, csvFile); var csvPath = Path.Combine(inputDir, csvFile);
// var source = new JsvSource(csvPath, headers, _logger); // var source = new JsvSource(csvPath, headers, _logger);
var source = new NewCsvSource(csvPath, headers, logger: _logger); var source = new CsvSource(csvPath, headers, _csvOptions.Value.Delimiter, _csvOptions.Value.QuoteChar, _logger);
while (await source.ReadAsync()) while (await source.ReadAsync())
{ {

View File

@ -0,0 +1,30 @@
using ConsoleApp2.HostedServices.Abstractions;
using Microsoft.Extensions.Hosting;
namespace ConsoleApp2.HostedServices;
public class MainHostedService : BackgroundService
{
private readonly IInputService _input;
private readonly ITransformService _transform;
private readonly IOutputService _output;
public MainHostedService(IInputService input, ITransformService transform, IOutputService output)
{
_input = input;
_transform = transform;
_output = output;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var tasks = new List<Task>()
{
Task.Run(async () => await _input.ExecuteAsync(stoppingToken), stoppingToken),
Task.Run(async () => await _transform.ExecuteAsync(stoppingToken), stoppingToken),
Task.Run(async () => await _output.ExecuteAsync(stoppingToken), stoppingToken),
};
await Task.WhenAll(tasks);
// await Task.Run(async () => await _output.ExecuteAsync(stoppingToken), stoppingToken);
}
}

View File

@ -1,58 +1,62 @@
using ConsoleApp2.Entities; using ConsoleApp2.Const;
using ConsoleApp2.HostedServices.Abstractions;
using ConsoleApp2.Options; using ConsoleApp2.Options;
using ConsoleApp2.Services; using ConsoleApp2.Services;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using MySqlConnector;
namespace ConsoleApp2.HostedServices; namespace ConsoleApp2.HostedServices;
public class MysqlOutputService : BackgroundService /// <summary>
/// 数据导出服务将数据导出至MySql服务
/// </summary>
public class OutputService : IOutputService
{ {
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly DataRecordQueue _consumerQueue; private readonly DataRecordQueue _consumerQueue;
private readonly IOptions<DatabaseOptions> _options; private readonly IOptions<DatabaseOutputOptions> _options;
private readonly ProcessContext _context; private readonly ProcessContext _context;
private readonly TaskManager _taskManager;
public MysqlOutputService(ILogger<MysqlOutputService> logger, public OutputService(ILogger<OutputService> logger,
[FromKeyedServices(ProcessStep.Consumer)]DataRecordQueue consumerQueue, [FromKeyedServices(ProcessStep.Consumer)] DataRecordQueue consumerQueue,
IOptions<DatabaseOptions> options, IOptions<DatabaseOutputOptions> options,
ProcessContext context) ProcessContext context,
TaskManager taskManager)
{ {
_logger = logger; _logger = logger;
_consumerQueue = consumerQueue; _consumerQueue = consumerQueue;
_options = options; _options = options;
_context = context; _context = context;
_taskManager = taskManager;
} }
protected override async Task ExecuteAsync(CancellationToken stoppingToken) public async Task ExecuteAsync(CancellationToken stoppingToken)
{ {
_logger.LogInformation("***** Mysql output service started *****"); _logger.LogInformation("***** Mysql output service started *****");
var tasks = new List<Task>();
var records = new List<DataRecord>(); var records = new List<DataRecord>();
while (!_context.IsTransformCompleted || _consumerQueue.Count > 0) while (!_context.IsTransformCompleted || _consumerQueue.Count > 0)
{ {
if (!_consumerQueue.TryDequeue(out var record)) continue; if (!_consumerQueue.TryDequeue(out var record)) continue;
records.Add(record); records.Add(record);
if (records.Count >= 200) if (records.Count >= _options.Value.FlushCount)
{ {
var recordsCopy = records; var recordsCopy = records;
tasks.Add(Task.Run(async () => await FlushAsync(recordsCopy), stoppingToken)); _taskManager.CreateTask(async () => await FlushAsync(recordsCopy), stoppingToken);
records = []; records = [];
} }
if (tasks.Count >= 10) if (_taskManager.TaskCount >= _options.Value.MaxTask)
{ {
await Task.WhenAll(tasks); await _taskManager.WaitAll();
tasks.Clear(); _taskManager.ClearTask();
} }
} }
await Task.WhenAll(tasks); await _taskManager.WaitAll();
await FlushAsync(records); await FlushAsync(records);
_context.CompleteOutput(); _context.CompleteOutput();
@ -63,15 +67,9 @@ public class MysqlOutputService : BackgroundService
private async Task FlushAsync(IEnumerable<DataRecord> records) private async Task FlushAsync(IEnumerable<DataRecord> records)
{ {
var count = 0; var count = 0;
await using var output = new MySqlDestination(new MySqlConnectionStringBuilder await using var output = new MySqlDestination(
{ _options.Value.ConnectionString ?? throw new InvalidOperationException("Connection string is required"),
Server = _options.Value.Host, _logger, true);
Port = _options.Value.Port,
Database = _options.Value.Database,
UserID = _options.Value.User,
Password = _options.Value.Password,
ConnectionTimeout = 180,
}.ConnectionString, _logger, true);
foreach (var record in records) foreach (var record in records)
{ {

View File

@ -1,64 +0,0 @@
using ConsoleApp2.Entities;
using ConsoleApp2.Helpers;
using ConsoleApp2.Services;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace ConsoleApp2.HostedServices;
public class SqlFileOutputService : BackgroundService
{
private readonly string _outputFile = "D:/DumpOutput/cferp_test_1.sql"; //
private readonly DataRecordQueue _consumerQueue;
private readonly ILogger _logger;
private readonly ProcessContext _context;
public SqlFileOutputService(
ILogger<SqlFileOutputService> logger,
[FromKeyedServices(ProcessStep.Consumer)]
DataRecordQueue consumerQueue,
ProcessContext context)
{
_logger = logger;
_consumerQueue = consumerQueue;
_context = context;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("***** Sql file output service started, thread id: {ThreadId} *****", Environment.CurrentManagedThreadId);
var count = 0;
var tableRecords = new Dictionary<string, IList<DataRecord>>();
while (!_context.IsTransformCompleted || _consumerQueue.Count > 0)
{
if (!_consumerQueue.TryDequeue(out var record)) continue;
tableRecords.AddOrUpdate(record.TableName, [record], (key, value) =>
{
value.Add(record);
return value;
});
++count;
if (count >= 200)
{
await File.AppendAllTextAsync(_outputFile,
MySqlDestination.SerializeRecords(tableRecords), stoppingToken);
tableRecords.Clear();
_context.AddOutput(count);
count = 0;
}
}
await File.AppendAllTextAsync(_outputFile,
MySqlDestination.SerializeRecords(tableRecords), stoppingToken);
tableRecords.Clear();
_context.AddOutput(count);
_context.CompleteOutput();
_logger.LogInformation("***** Sql file output service completed *****");
}
}

View File

@ -1,4 +1,5 @@
using System.Diagnostics; using System.Diagnostics;
using ConsoleApp2.Const;
using ConsoleApp2.Services; using ConsoleApp2.Services;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
@ -6,6 +7,9 @@ using Microsoft.Extensions.Logging;
namespace ConsoleApp2.HostedServices; namespace ConsoleApp2.HostedServices;
/// <summary>
/// 任务监控
/// </summary>
public class TaskMonitorService : BackgroundService public class TaskMonitorService : BackgroundService
{ {
private readonly IHostApplicationLifetime _lifetime; private readonly IHostApplicationLifetime _lifetime;
@ -93,7 +97,7 @@ public class TaskMonitorService : BackgroundService
_logger.LogInformation("Queue monitor: producer queue: {ProducerQueue}, consumer queue: {ConsumerQueue}", _logger.LogInformation("Queue monitor: producer queue: {ProducerQueue}, consumer queue: {ConsumerQueue}",
_producerQueue.Count, _consumerQueue.Count); _producerQueue.Count, _consumerQueue.Count);
await Task.Delay(2000); await Task.Delay(5000);
lastTime = time; lastTime = time;
lastInputCount = inputCount; lastInputCount = inputCount;

View File

@ -1,14 +1,17 @@
using ConsoleApp2.Helpers; using ConsoleApp2.Const;
using ConsoleApp2.HostedServices.Abstractions;
using ConsoleApp2.Options; using ConsoleApp2.Options;
using ConsoleApp2.Services; using ConsoleApp2.Services;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
namespace ConsoleApp2.HostedServices; namespace ConsoleApp2.HostedServices;
public class DataTransformService : BackgroundService /// <summary>
/// 数据处理服务,对导入后的数据进行处理
/// </summary>
public class TransformService : ITransformService
{ {
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IOptions<DataTransformOptions> _options; private readonly IOptions<DataTransformOptions> _options;
@ -17,21 +20,20 @@ public class DataTransformService : BackgroundService
private readonly ProcessContext _context; private readonly ProcessContext _context;
public DataTransformService(ILogger<DataTransformService> logger, public TransformService(ILogger<TransformService> logger,
IOptions<DataTransformOptions> options, // TBD: database filter IOptions<DataTransformOptions> options,
[FromKeyedServices(ProcessStep.Producer)]DataRecordQueue producerQueue, [FromKeyedServices(ProcessStep.Producer)]DataRecordQueue producerQueue,
[FromKeyedServices(ProcessStep.Consumer)]DataRecordQueue consumerQueue, [FromKeyedServices(ProcessStep.Consumer)]DataRecordQueue consumerQueue,
ProcessContext context) ProcessContext context)
{ {
_logger = logger; _logger = logger;
// _taskManager = taskManager;
_options = options; _options = options;
_producerQueue = producerQueue; _producerQueue = producerQueue;
_consumerQueue = consumerQueue; _consumerQueue = consumerQueue;
_context = context; _context = context;
} }
protected override async Task ExecuteAsync(CancellationToken cancellationToken) public async Task ExecuteAsync(CancellationToken cancellationToken)
{ {
_logger.LogInformation("***** Data transform service started, thread id: {ThreadId} *****", Environment.CurrentManagedThreadId); _logger.LogInformation("***** Data transform service started, thread id: {ThreadId} *****", Environment.CurrentManagedThreadId);
while (!_context.IsInputCompleted || _producerQueue.Count > 0) while (!_context.IsInputCompleted || _producerQueue.Count > 0)
@ -58,7 +60,6 @@ public class DataTransformService : BackgroundService
field = string.IsNullOrEmpty(field) ? "''" : $"0x{field}"; field = string.IsNullOrEmpty(field) ? "''" : $"0x{field}";
break; break;
default: default:
field = field;
break; break;
} }
@ -66,6 +67,8 @@ public class DataTransformService : BackgroundService
record[i] = field; record[i] = field;
} }
// TODO: 数据处理/过滤/复制
_consumerQueue.Enqueue(record); _consumerQueue.Enqueue(record);
_context.AddTransform(); _context.AddTransform();
} }

View File

@ -1,11 +1,14 @@
using ConsoleApp2.Services; using ConsoleApp2.Const;
using ConsoleApp2.HostedServices.Abstractions;
using ConsoleApp2.Services;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace ConsoleApp2.HostedServices; namespace ConsoleApp2.HostedServices;
public class VoidOutputService : BackgroundService // 空输出服务,测试用
public class VoidOutputService : IOutputService
{ {
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly DataRecordQueue _consumerQueue; private readonly DataRecordQueue _consumerQueue;
@ -19,7 +22,7 @@ public class VoidOutputService : BackgroundService
_logger = logger; _logger = logger;
} }
protected override Task ExecuteAsync(CancellationToken stoppingToken) public Task ExecuteAsync(CancellationToken stoppingToken)
{ {
_logger.LogInformation("***** Void output service started, thread id: {ThreadId} *****", Environment.CurrentManagedThreadId); _logger.LogInformation("***** Void output service started, thread id: {ThreadId} *****", Environment.CurrentManagedThreadId);
while (!_context.IsTransformCompleted || _consumerQueue.Count > 0) while (!_context.IsTransformCompleted || _consumerQueue.Count > 0)

View File

@ -3,26 +3,17 @@
public class CsvOptions public class CsvOptions
{ {
/// <summary> /// <summary>
/// The directory to input csv and sql file. /// MyDumper导出的CSV文件目录
/// </summary> /// </summary>
public string InputDir { get; set; } = "./"; public string InputDir { get; set; } = "./";
/// <summary>
/// The output directory.
/// </summary>
public string OutputDir { get; set; } = "./Output";
/// <summary> /// <summary>
/// The ASCII char that fields are enclosed by. Default is '"'. /// 字符串的包围符号,默认为双引号"
/// </summary> /// </summary>
public char QuoteChar { get; set; } = '"'; public char QuoteChar { get; set; } = '"';
/// <summary> /// <summary>
/// The ASCII char that fields are separated by. Default is ','. /// 每个字段的分割符,默认逗号,
/// </summary> /// </summary>
public char DelimiterChar { get; set; } = ','; public string Delimiter { get; set; } = ",";
/// <summary>
/// The max number of threads to use.
/// </summary>
public int MaxThreads { get; set; } = 12;
} }

View File

@ -1,6 +1,4 @@
using ConsoleApp2.Entities; namespace ConsoleApp2.Options;
namespace ConsoleApp2.Options;
public enum ColumnType public enum ColumnType
{ {
@ -12,6 +10,10 @@ public enum ColumnType
public class DataTransformOptions public class DataTransformOptions
{ {
public Func<DataRecord, string>? DatabaseFilter { get; set; } public Func<DataRecord, string>? DatabaseFilter { get; set; }
/// <summary>
/// 配置导入数据的特殊列
/// </summary>
public Dictionary<string, ColumnType> ColumnTypeConfig { get; set; } = new(); // "table.column" -> type public Dictionary<string, ColumnType> ColumnTypeConfig { get; set; } = new(); // "table.column" -> type
public ColumnType GetColumnType(string table, string column) public ColumnType GetColumnType(string table, string column)

View File

@ -1,10 +0,0 @@
namespace ConsoleApp2.Options;
public class DatabaseOptions
{
public string Host { get; set; }
public uint Port { get; set; }
public string Database { get; set; }
public string User { get; set; }
public string Password { get; set; }
}

View File

@ -0,0 +1,17 @@
namespace ConsoleApp2.Options;
public class DatabaseOutputOptions
{
/// <summary>
/// 数据库连接字符串
/// </summary>
public string? ConnectionString { get; set; }
/// <summary>
/// 输出服务的最大任务(Task)数
/// </summary>
public int MaxTask { get; set; }
/// <summary>
/// 每个任务每次提交到数据库的记录数量每N条构建一次SQL语句
/// </summary>
public int FlushCount { get; set; }
}

View File

@ -1,26 +1,17 @@
using ConsoleApp2; using ConsoleApp2.Const;
using ConsoleApp2.HostedServices; using ConsoleApp2.HostedServices;
using ConsoleApp2.HostedServices.Abstractions;
using ConsoleApp2.Options; using ConsoleApp2.Options;
using ConsoleApp2.Services; using ConsoleApp2.Services;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using MySqlConnector;
using Serilog; using Serilog;
// 运行之前把Mysql max_allowed_packets 调大
// 加入数据库过滤 // 运行之前把process_step表的外键删掉
// HostedService不是并行的完善TaskManager手动开启线程
// 测试BlockingCollection对速度的影响
// 重新同步数据
// Json列和Blob列不一致
/* JSV导出带转义的列有误
* order_data_block表id 4153969
* order_data_parts表Spec列
* order_module表Name列
* process_group表Items列
*/
await RunProgram(); await RunProgram();
return; return;
@ -32,15 +23,12 @@ async Task RunProgram()
host.Configuration.AddCommandLine(args); host.Configuration.AddCommandLine(args);
host.Services.Configure<CsvOptions>(option => host.Services.Configure<CsvOptions>(option =>
{ {
option.DelimiterChar = ','; option.Delimiter = ",";
option.QuoteChar = '"'; option.QuoteChar = '"';
option.InputDir = "D:/Dump/MyDumper"; option.InputDir = "D:/Dump";
option.OutputDir = "D:/DumpOutput";
option.MaxThreads = 12;
}); });
host.Services.Configure<DataTransformOptions>(options => host.Services.Configure<DataTransformOptions>(options =>
{ {
//TODO: Database Filter
options.DatabaseFilter = record => "cferp_test_1"; options.DatabaseFilter = record => "cferp_test_1";
options.ColumnTypeConfig = new() options.ColumnTypeConfig = new()
{ {
@ -73,14 +61,20 @@ async Task RunProgram()
{ "order_block_plan.BlockInfo", ColumnType.Text }, { "order_block_plan.BlockInfo", ColumnType.Text },
}; };
}); });
// 加入数据库过滤后删除
host.Services.Configure<DatabaseOptions>(options => host.Services.Configure<DatabaseOutputOptions>(options =>
{ {
options.Host = "localhost"; options.ConnectionString = new MySqlConnectionStringBuilder
options.Port = 33306; {
options.Database = "cferp_test_1"; Server = "127.0.0.1",
options.User = "root"; Port = 33306,
options.Password = "123456"; Database = "cferp_test_1",
UserID = "root",
Password = "123456",
MaximumPoolSize = 50, // 这个值应当小于 max_connections
}.ConnectionString;
options.MaxTask = 16;
options.FlushCount = 200;
}); });
host.Services.AddLogging(builder => host.Services.AddLogging(builder =>
{ {
@ -91,14 +85,13 @@ async Task RunProgram()
host.Services.AddSingleton<ProcessContext>(); host.Services.AddSingleton<ProcessContext>();
host.Services.AddKeyedSingleton<DataRecordQueue>(ProcessStep.Producer); host.Services.AddKeyedSingleton<DataRecordQueue>(ProcessStep.Producer);
host.Services.AddKeyedSingleton<DataRecordQueue>(ProcessStep.Consumer); host.Services.AddKeyedSingleton<DataRecordQueue>(ProcessStep.Consumer);
host.Services.AddKeyedSingleton<TaskManager>(ProcessStep.Producer); host.Services.AddTransient<TaskManager>();
host.Services.AddKeyedSingleton<TaskManager>(ProcessStep.Consumer);
host.Services.AddHostedService<MainHostedService>();
host.Services.AddHostedService<TaskMonitorService>(); host.Services.AddHostedService<TaskMonitorService>();
host.Services.AddHostedService<CsvInputService>(); host.Services.AddSingleton<IInputService, InputService>();
host.Services.AddHostedService<DataTransformService>(); host.Services.AddSingleton<ITransformService, TransformService>();
host.Services.AddHostedService<MysqlOutputService>(); host.Services.AddSingleton<IOutputService, OutputService>();
var app = host.Build(); var app = host.Build();
await app.RunAsync(); await app.RunAsync();

49
ConsoleApp2/README.md Normal file
View File

@ -0,0 +1,49 @@
## 说明
使用该程序来对MyDumper导出的CSV数据进行读取转换然后导出到其他数据库中。
1. 用MyDumper从数据库导出CSV数据
使用MyDumper Docker镜像
```sh
docker run --rm --net=host -v D:/Dump:/home/backup mydumper/mydumper:v0.15.2-6 mydumper `
-h 127.0.0.1 -P 33306 -u root -p 123456 `
-B cferp_test --no-schemas --csv --hex-blob `
-o /home/backup
```
将挂载卷,数据库连接和输出目录替换
不导出数据库结构(--no-schemas)
导出完的目录下应当包含.sql文件以及.dat文件
2. 在Program.cs中修改`CsvOptions`配置
```cs
host.Services.Configure<CsvOptions>(option =>
{
option.Delimiter = ",";
option.QuoteChar = '"';
option.InputDir = "D:/Dump/Test";
});
```
将`option.InputDir`配置为MyDumper导出的数据目录
3. 在Program.cs中修改`DatabaseOutputOptions`配置
```cs
host.Services.Configure<DatabaseOutputOptions>(options =>
{
options.ConnectionString = new MySqlConnectionStringBuilder
{
Server = "127.0.0.1",
Port = 33306,
Database = "cferp_test_1",
UserID = "root",
Password = "123456",
MaximumPoolSize = 50,
}.ConnectionString;
options.MaxTask = 16;
options.FlushCount = 200;
});
```
将`MySqlConnectionStringBuilder`的属性修改为程序要导出至的数据库
> 后续将这些配置通过命令行传递
4. 运行程序
> 注意,测试数据库`cferp_test`中的`order_process_step`表存在外键,如果要导出到和测试库同结构的数据库,记得先把外键删除。

View File

@ -1,24 +1,26 @@
using System.Text; using System.Text;
using ConsoleApp2.Entities;
using ConsoleApp2.Helpers; using ConsoleApp2.Helpers;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace ConsoleApp2.Services; namespace ConsoleApp2.Services;
public class NewCsvSource /// <summary>
/// CSV文件读取
/// </summary>
public class CsvSource
{ {
private readonly string _filePath; private readonly string _filePath;
private readonly StreamReader _reader; private readonly StreamReader _reader;
private readonly ILogger? _logger; private readonly ILogger? _logger;
private readonly string _tableName; private readonly string _tableName;
public DataRecord Current { get; protected set; } public DataRecord Current { get; private set; }
public string[]? Headers { get; } public string[]? Headers { get; }
public string? CurrentRaw { get; private set; } public string? CurrentRaw { get; private set; }
public string Delimiter { get; private set; } public string Delimiter { get; private set; }
public char QuoteChar { get; private set; } public char QuoteChar { get; private set; }
public NewCsvSource(string filePath, string[]? headers = null, string delimiter = ",", char quoteChar = '"', public CsvSource(string filePath, string[]? headers = null, string delimiter = ",", char quoteChar = '"',
ILogger? logger = null) ILogger? logger = null)
{ {
_filePath = filePath; _filePath = filePath;

View File

@ -1,9 +1,11 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using ConsoleApp2.Entities;
namespace ConsoleApp2.Services; namespace ConsoleApp2.Services;
/// <summary>
/// 数据队列
/// </summary>
public class DataRecordQueue : IDisposable public class DataRecordQueue : IDisposable
{ {
private readonly BlockingCollection<DataRecord> _queue; private readonly BlockingCollection<DataRecord> _queue;
@ -17,7 +19,7 @@ public class DataRecordQueue : IDisposable
public DataRecordQueue() public DataRecordQueue()
{ {
_queue = new BlockingCollection<DataRecord>(); _queue = new BlockingCollection<DataRecord>(200_000); // 队列最长为20W条记录
} }
public bool TryDequeue([MaybeNullWhen(false)] out DataRecord record) public bool TryDequeue([MaybeNullWhen(false)] out DataRecord record)

View File

@ -1,10 +1,13 @@
using ConsoleApp2.Entities; using ConsoleApp2.Helpers;
using ConsoleApp2.Helpers;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using ServiceStack.Text; using ServiceStack.Text;
namespace ConsoleApp2.Services; namespace ConsoleApp2.Services;
/// <summary>
/// 读取Jsv格式文件
/// </summary>
[Obsolete]
public class JsvSource : IDisposable public class JsvSource : IDisposable
{ {
private readonly string _filePath; private readonly string _filePath;

View File

@ -1,11 +1,13 @@
using System.Text; using System.Text;
using ConsoleApp2.Entities;
using ConsoleApp2.Helpers; using ConsoleApp2.Helpers;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using MySqlConnector; using MySqlConnector;
namespace ConsoleApp2.Services; namespace ConsoleApp2.Services;
/// <summary>
/// Mysql导出
/// </summary>
public class MySqlDestination : IDisposable, IAsyncDisposable public class MySqlDestination : IDisposable, IAsyncDisposable
{ {
private readonly Dictionary<string, IList<DataRecord>> _recordCache; private readonly Dictionary<string, IList<DataRecord>> _recordCache;
@ -13,8 +15,6 @@ public class MySqlDestination : IDisposable, IAsyncDisposable
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly bool _prettyOutput; private readonly bool _prettyOutput;
public static int AddCount;
public MySqlDestination(string connStr, ILogger logger, bool prettyOutput = false) public MySqlDestination(string connStr, ILogger logger, bool prettyOutput = false)
{ {
_conn = new MySqlConnection(connStr); _conn = new MySqlConnection(connStr);
@ -29,7 +29,6 @@ public class MySqlDestination : IDisposable, IAsyncDisposable
_recordCache.AddOrUpdate(record.TableName, [record], (key, value) => _recordCache.AddOrUpdate(record.TableName, [record], (key, value) =>
{ {
value.Add(record); value.Add(record);
Interlocked.Increment(ref AddCount);
return value; return value;
}); });
return Task.CompletedTask; return Task.CompletedTask;
@ -60,6 +59,10 @@ public class MySqlDestination : IDisposable, IAsyncDisposable
_logger.LogCritical(e, "Error when flushing records, sql: {Sql}", cmd.CommandText.Omit(1000)); _logger.LogCritical(e, "Error when flushing records, sql: {Sql}", cmd.CommandText.Omit(1000));
throw; throw;
} }
finally
{
await cmd.DisposeAsync();
}
} }
public static string SerializeRecords(IDictionary<string, IList<DataRecord>> tableRecords, public static string SerializeRecords(IDictionary<string, IList<DataRecord>> tableRecords,
@ -91,22 +94,7 @@ public class MySqlDestination : IDisposable, IAsyncDisposable
for (var j = 0; j < record.Fields.Length; j++) for (var j = 0; j < record.Fields.Length; j++)
{ {
var field = record.Fields[j]; var field = record.Fields[j];
#region HandleFields
// if (field == "\\N")
// sb.Append("NULL");
// else if (DumpDataHelper.CheckHexField(field))
// {
// // if (StringExtensions.CheckJsonHex(field))
// sb.Append($"0x{field}");
// }
// else
// sb.Append($"'{field}'");
sb.Append(field); sb.Append(field);
#endregion
if (j != record.Fields.Length - 1) if (j != record.Fields.Length - 1)
sb.Append(','); sb.Append(',');
} }
@ -127,11 +115,13 @@ public class MySqlDestination : IDisposable, IAsyncDisposable
public void Dispose() public void Dispose()
{ {
_conn.Close();
_conn.Dispose(); _conn.Dispose();
} }
public async ValueTask DisposeAsync() public async ValueTask DisposeAsync()
{ {
await _conn.CloseAsync();
await _conn.DisposeAsync(); await _conn.DisposeAsync();
} }
} }

View File

@ -1,5 +1,8 @@
namespace ConsoleApp2.Services; namespace ConsoleApp2.Services;
/// <summary>
/// 处理上下文类,标识处理进度
/// </summary>
public class ProcessContext public class ProcessContext
{ {
private int _inputCount; private int _inputCount;

View File

@ -3,14 +3,17 @@ using Microsoft.Extensions.Logging;
namespace ConsoleApp2.Services; namespace ConsoleApp2.Services;
/// <summary>
/// 快速批量创建和等待任务
/// </summary>
public class TaskManager public class TaskManager
{ {
private readonly ConcurrentBag<Task> _tasks; private readonly ConcurrentBag<Task> _tasks;
private readonly ILogger _logger; private readonly ILogger _logger;
public int TaskCount => _tasks.Count;
public int RunningTaskCount => _tasks.Count(task => !task.IsCompleted); public int RunningTaskCount => _tasks.Count(task => !task.IsCompleted);
public IReadOnlyCollection<Task> Tasks => _tasks; public IReadOnlyCollection<Task> Tasks => _tasks;
public bool MainTaskCompleted { get; set; }
public TaskManager(ILogger<TaskManager> logger) public TaskManager(ILogger<TaskManager> logger)
{ {
@ -18,11 +21,22 @@ public class TaskManager
_logger = logger; _logger = logger;
} }
public Task<TResult> CreateTask<TResult>(Func<TResult> func) public void CreateTask<TResult>(Func<TResult> func, CancellationToken cancellationToken = default)
{ {
var task = Task.Factory.StartNew(func); var task = Task.Factory.StartNew(func, cancellationToken);
_tasks.Add(task); _tasks.Add(task);
_logger.LogDebug("New task created."); _logger.LogDebug("New task created");
return task; }
public async Task WaitAll()
{
await Task.WhenAll(_tasks);
}
public void ClearTask()
{
if(RunningTaskCount != 0)
throw new InvalidOperationException("Unable to clear task. There are still running tasks");
_tasks.Clear();
} }
} }

View File

@ -1,6 +0,0 @@
namespace ConsoleApp2.Services;
public class TsvSource
{
}