环境

Sqlserver 2016

.net 4.5.2

目前测试数据1300万 大约3-4分钟.(限制一次读取条数 和 线程数是 要节省服务器资源,如果调太大服务器其它应用可能就跑不了了), SqlServerDBHelper为数据库帮助类.没有什么特别的处理. 配置连接串时记录把连接池开起来

另外.以下代码中每次写都创建了连接 .之前试过一个连接反复用. 130次大约有20多次 数据库会出问题.并且需要的时间是7-8分钟 左右.

配置文件: xxx.json

 [ {
/*连接字符串 */
"ConnStr": "",
"FilePath": "读取的文件地址",
/*数据库表名称 */
"TableName": "写入的数据库表名",
/*导入前执行的语句 */
"ExecBeforeSql": "",
/*导入后执行的语句 */
"ExecAfterSql": "",
/*映射关系 */
"Mapping": [
{
"DBName": "XXX",
"TxtName": "DDD"
}
],
/*过滤数据的正则 当前只实现了小数据一次性读完的检查*/
"FilterRegex": [],
/*检查数据合法性(从数据库获取字段属性进行验证) */
"CheckData": false,
/*列分隔符*/
"Separator": "\t",
/*表头的行数*/
"HeaderRowsNum": 1
}
]

读取代码 : 注意 ConfigurationManager.AppSettings["frpage"] 和 ConfigurationManager.AppSettings["fr"] 需要自己配置好

 //读取配置文件信息
List<dynamic> dt = JsonConvert.DeserializeObject<List<dynamic>>(File.ReadAllText(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "config\\ImportTxt.json")));
LogUtil.Info("开始读取txt数据,读取配置:" + dt.Count + "条");
if (dt.Count == )
{
return;
} List<Task> li = new List<Task>();
foreach (dynamic row in dt)
{
LogUtil.Info("开始处理数据:" + JsonConvert.SerializeObject(row));
li.Add(ProcessRow(row)); }
Task.WaitAll(li.ToArray());
LogUtil.Info("数据读取完毕");
        public async Task ProcessRow(dynamic row)
{
await Task.Run(() =>
{
AutoResetEvent AE = new AutoResetEvent(false);
DataTable Data = null;
string error = "", ConnStr, TableName, ExecBeforeSql, ExecAfterSql;
Boolean IsCheck = Convert.ToBoolean(row["CheckData"]);
TableName = Convert.ToString(row.TableName);
ConnStr = Convert.ToString(row.ConnStr);
ExecBeforeSql = Convert.ToString(row.ExecBeforeSql);
ExecAfterSql = Convert.ToString(row.ExecAfterSql);
int HeaderRowsNum = Convert.ToInt32(row.HeaderRowsNum);
string Separator = Convert.ToString(row.Separator); Dictionary<string, string> dic = new Dictionary<string, string>(); //文件达到多大时就分行读取
int fr = ;
if (!int.TryParse(ConfigurationManager.AppSettings["fr"], out fr))
{
fr = ;
}
fr = fr * * ; //分行读取一次读取多少
int page = ;
if (!int.TryParse(ConfigurationManager.AppSettings["frpage"], out page))
{
page = ;
} foreach (var dyn in row.Mapping)
{
dic.Add(Convert.ToString(dyn.TxtName), Convert.ToString(dyn.DBName));
} List<string> regex = new List<string>();
foreach (string item in row["FilterRegex"])
{
regex.Add(item);
}
string fpath = "", cpath = ""; cpath = Convert.ToString(row["FilePath"]);
string rootPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "tmp");
if (!Directory.Exists(rootPath))
{
Directory.CreateDirectory(rootPath);
} fpath = Path.Combine(rootPath, Path.GetFileName(cpath));
File.Copy(cpath, fpath, true);
LogUtil.Info("拷文件到本地已经完成.从本地读取数据操作");
int threadCount = Environment.ProcessorCount * ; FileInfo fi = new FileInfo(fpath);
//如果文件大于100M就需要分批读取.一次50万条
if (fi.Length > fr)
{ long sumCount = ;
StreamReader sr = new StreamReader(fi.OpenRead());
int headRow = ;
string rowstr = ""; List<Thread> li_th = new List<Thread>();
bool last = false;
int ij = ;
LogUtil.Info("生成StreamReader成功 ");
#region 逐行读取 while (sr.Peek() > -)
{
rowstr = sr.ReadLine();
#region 将行数据写入DataTable
if (headRow < HeaderRowsNum)
{
Data = new DataTable();
foreach (string scol in rowstr.Split(new string[] { Separator }, StringSplitOptions.RemoveEmptyEntries))
{
Data.Columns.Add(scol.Trim(), typeof(string));
}
headRow++;
continue;
}
else
{ //行数据
if (headRow > )
{
for (int i = ; i < headRow && sr.Peek() > -; i++)
{
rowstr += " " + sr.ReadLine();
}
}
Data.Rows.Add(rowstr.Split(new string[] { Separator }, StringSplitOptions.RemoveEmptyEntries));
if (Data.Rows.Count < page && sr.Peek() > -)
{
continue;
}
}
last = (sr.Peek() == -);
#endregion sumCount += Data.Rows.Count; ProcessPath(Data, page, sr, ref ij, TableName, ExecBeforeSql, ExecAfterSql, dic, IsCheck, li_th); #region 检查线程等待
if ((ij > && (ij % threadCount) == ) || last)
{
LogUtil.Info("完成一批次当前共写数据: " + sumCount);
while (true)
{
bool isok = true;
foreach (var item in li_th)
{
if (item.IsAlive)
{
isok = false;
Application.DoEvents();
Thread.Sleep();
}
}
if (isok)
{
li_th.Clear();
break;
}
} //最后一页要等所有的执行完才能执行
if (sr.Peek() == -)
{
WriteTODB(TableName, Data, ExecBeforeSql, ExecAfterSql, dic, false, true);
LogUtil.Info("最后一次写入完成");
}
LogUtil.Info(" 线程退出开始新的循环...");
}
Data.Clear();
#endregion
}
sr.Dispose();
#endregion
}
else
{
using (SQLServerDBHelper sdb = new SQLServerDBHelper())
{
sdb.OpenConnection();
#region 一次性读取处理
Data = LoadDataTableFromTxt(fpath, ref error, Separator, HeaderRowsNum, regex, IsCheck, dic, TableName);
if (IsCheck)
{
DataRow[] rows = Data.Select("ErrorMsg is not null");
if (rows.Length > )
{
LogUtil.Info($"读取{TableName} 数据出错 : {JsonConvert.SerializeObject(rows)}");
return;
}
} LogUtil.Info($"读取{TableName} 的txt数据完成.共读取数据:{Data.Rows.Count}条");
if (Data.Rows.Count == || !string.IsNullOrWhiteSpace(error))
{
if (!string.IsNullOrWhiteSpace(error))
{
LogUtil.Info("读取数据出错,地址:" + Convert.ToString(row["FilePath"]) + " \r\n 错误:" + error);
}
return;
}
sdb.BgeinTransaction();
try
{
WriteTODB(TableName, Data, ExecBeforeSql, ExecAfterSql, dic, sdb: sdb);
sdb.CommitTransaction();
LogUtil.Info(TableName + "数据更新完毕 !!");
}
catch (Exception ex)
{ LogUtil.Info(TableName + " 更新数据出错,错误:" + ex.Message + " \r\n 堆栈:" + ex.StackTrace);
sdb.RollbackTransaction();
}
#endregion } } GC.Collect();
}); } private void ProcessPath(DataTable Data, int page, StreamReader sr, ref int ij, string TableName, string ExecBeforeSql, string ExecAfterSql, Dictionary<string, string> dic, bool IsCheck, List<Thread> li_th)
{
int threadCount = Environment.ProcessorCount * ; string error = "";
PoolModel p = new PoolModel { TableName = TableName, ExecBeforeSql = ExecBeforeSql, ExecAfterSql = ExecAfterSql, dic = dic };
p.Data = Data.Copy();
if (IsCheck)
{
using (SQLServerDBHelper sdb = new SQLServerDBHelper())
{
error = CheckData(Data, TableName, dic, sdb);
}
DataRow[] rows = Data.Select("ErrorMsg is not null");
if (rows.Length > || !string.IsNullOrWhiteSpace(error))
{
LogUtil.Info($"读取{TableName} 数据出错 : {JsonConvert.SerializeObject(rows)}\r\n错误: " + error);
return;
}
} ij++;
if (ij == )
{ WriteTODB(p.TableName, p.Data, p.ExecBeforeSql, p.ExecAfterSql, p.dic, true, false);
LogUtil.Info("首次写入完成");
} else if (sr.Peek() > -)
{ Thread t = new Thread(d =>
{ PoolModel c = d as PoolModel;
try
{
WriteTODB(c.TableName, c.Data, c.ExecBeforeSql, c.ExecAfterSql, c.dic, false, false);
}
catch (ThreadAbortException)
{
LogUtil.Error("线程退出.................");
}
catch (Exception ex)
{ LogUtil.Error(c.TableName + "写入数据失败:" + ex.Message + "\r\n堆栈:" + ex.StackTrace + "\r\n 数据: " + JsonConvert.SerializeObject(c.Data));
ExitApp();
return;
} });
t.IsBackground = true;
t.Start(p);
li_th.Add(t);
} } public void ExitApp()
{
Application.Exit();
} public void WriteTODB(string TableName, DataTable Data, string ExecBeforeSql, string ExecAfterSql, Dictionary<string, string> dic, bool first = true, bool last = true, SQLServerDBHelper sdb = null)
{
bool have = false;
if (sdb == null)
{
sdb = new SQLServerDBHelper();
have = true;
} if (first && !string.IsNullOrWhiteSpace(ExecBeforeSql))
{
LogUtil.Info(TableName + "执行前Sql :" + ExecBeforeSql);
sdb.ExecuteNonQuery(ExecBeforeSql);
}
sdb.BulkCopy(Data, TableName, dic);
if (last && !string.IsNullOrWhiteSpace(ExecAfterSql))
{
LogUtil.Info(TableName + "执行后Sql :" + ExecAfterSql);
sdb.ExecuteNonQuery(ExecAfterSql);
}
LogUtil.Info(TableName + "本次执行完成 ");
if (have)
{
sdb.Dispose();
}
} public string CheckData(DataTable dt, string dbTableName, Dictionary<string, string> dic, SQLServerDBHelper sdb)
{
if (string.IsNullOrWhiteSpace(dbTableName))
{
return "表名不能为空!";
}
if (dic.Count == )
{
return "映射关系数据不存在!"; } List<string> errorMsg = new List<string>();
List<string> Cols = new List<string>();
dic.Foreach(c =>
{
if (!dt.Columns.Contains(c.Key))
{
errorMsg.Add(c.Key);
}
Cols.Add(c.Key);
}); if (errorMsg.Count > )
{
return "数据列不完整,请与映射表的数据列数量保持一致!列:" + string.Join(",", errorMsg);
} //如果行数据有错误信息则添加到这一列的值里
dt.Columns.Add(new DataColumn("ErrorMsg", typeof(string)) { DefaultValue = "" });
string sql = @"--获取SqlServer中表结构
SELECT syscolumns.name as ColName,systypes.name as DBType,syscolumns.isnullable,
syscolumns.length
FROM syscolumns, systypes
WHERE syscolumns.xusertype = systypes.xusertype
AND syscolumns.id = object_id(@tb) ; ";
DataSet ds = sdb.GetDataSet(sql, new SqlParameter[] { new SqlParameter("@tb", dbTableName) });
EnumerableRowCollection<DataRow> TableDef = ds.Tables[].AsEnumerable(); // string colName="";
Object obj_val; //将表结构数据重组成字典.
var dic_Def = TableDef.ToDictionary(c => Convert.ToString(c["ColName"]), d =>
{
string DBType = "";
string old = Convert.ToString(d["DBType"]).ToUpper();
DBType = GetCSharpType(old);
return new { ColName = Convert.ToString(d["ColName"]), DBType = DBType, SqlType = old, IsNullble = Convert.ToBoolean(d["isnullable"]), Length = Convert.ToInt32(d["length"]) };
}); DateTime now = DateTime.Now;
foreach (DataRow row in dt.Rows)
{
errorMsg.Clear();
foreach (string colName in Cols)
{
if (dic.ContainsKey(colName))
{
if (!dic_Def.ContainsKey(dic[colName]))
{
return "Excel列名:" + colName + " 映射数据表字段:" + dic[colName] + "在当前数据表中不存在!";
}
//去掉数据两边的空格
row[colName] = obj_val = Convert.ToString(row[colName]).Trim();
var info = dic_Def[dic[colName]];
//是否是DBNULL
if (obj_val.Equals(DBNull.Value))
{
if (!info.IsNullble)
{
errorMsg.Add("列" + colName + "不能为空!"); }
}
else
{
if (info.DBType == "String")
{
//time类型不用验证长度(日期的 时间部分如 17:12:30.0000)
if (info.SqlType == "TIME")
{
if (!DateTime.TryParse(now.ToString("yyyy-MM-dd") + " " + obj_val.ToString(), out now))
{
errorMsg.Add("列" + colName + "填写的数据无效应为日期的时间部分如:17:30:12"); }
}
else if (Convert.ToString(obj_val).Length > info.Length)
{
errorMsg.Add("列" + colName + "长度超过配置长度:" + info.Length);
}
}
else
{
Type t = Type.GetType("System." + info.DBType);
try
{ //如果数字中有千分位在这一步可以处理掉重新给这个列赋上正确的数值
row[colName] = Convert.ChangeType(obj_val, t); ;
}
catch (Exception ex)
{
errorMsg.Add("列" + colName + "填写的数据" + obj_val + "无效应为" + info.SqlType + "类型.");
} } }
} }
row["ErrorMsg"] = string.Join(" || ", errorMsg);
} return "";
} /// <summary>
/// wm 2018年11月28日13:37
/// 将数据库常用类型转为C# 中的类名(.Net的类型名)
/// </summary>
/// <param name="old"></param>
/// <returns></returns>
private string GetCSharpType(string old)
{
string DBType = "";
switch (old)
{
case "INT":
case "BIGINT":
case "SMALLINT":
DBType = "Int32";
break;
case "DECIMAL":
case "FLOAT":
case "NUMERIC":
DBType = "Decimal";
break;
case "BIT":
DBType = "Boolean";
break;
case "TEXT":
case "CHAR":
case "NCHAR":
case "VARCHAR":
case "NVARCHAR":
case "TIME":
DBType = "String";
break;
case "DATE":
case "DATETIME":
DBType = "DateTime";
break;
default:
throw new Exception("GetCSharpType数据类型" + DBType + "无法识别!"); } return DBType;
} public class PoolModel
{
public string TableName { get; set; }
public DataTable Data { get; set; }
public string ExecBeforeSql { get; set; }
public string ExecAfterSql { get; set; }
public Dictionary<string, string> dic { get; set; } }
    /// <summary>
/// wm 2018年11月28日13:32
/// 获取Txt数据并对数据进行校验返回一个带有ErrorMsg列的DataTable,如果数据校验失败则该字段存放失败的原因
/// 注意:在使用该方法前需要数据表应该已经存在
/// </summary>
/// <param name="isCheck">是否校验数据合法性(数据需要校验则会按传入的dbTableName获取数据库表的结构出来验证)</param>
/// <param name="map">如果需要验证数据则此处需要传映射关系 key Excel列名,Value 数据库列名</param>
/// <param name="dbTableName">验证数据合法性的表(即数据会插入到的表)</param>
/// <param name="error">非数据验证上的异常返回</param>
/// <param name="Regexs">用来过滤数据的正则</param>
/// <param name="path">读取文件的路径</param>
/// <param name="Separator">列分隔符</param>
/// <param name="HeaderRowsNum">表头的行数</param>
/// <returns>如果需求验证则返回一个带有ErrorMsg列的DataTable,如果数据校验失败则该字段存放失败的原因, 不需要验证则数据读取后直接返回DataTable</returns>
public DataTable LoadDataTableFromTxt(string path, ref string error, string Separator, int HeaderRowsNum, List<string> Regexs = null, bool isCheck = false, Dictionary<string, string> map = null, string dbTableName = "", SQLServerDBHelper sdb = null)
{
DataTable dt = new DataTable();
error = "";
if (isCheck && (map == null || map.Count == || string.IsNullOrWhiteSpace(dbTableName)))
{
error = "参数标明需要对表格数据进行校验,但没有指定映射表集合或数据表名.";
return dt;
}
string txts = File.ReadAllText(path);
#region 把读出来的方便数据转成DataTable Regexs?.ForEach(c =>
{
txts = new Regex(c).Replace(txts, "");
});
////替换掉多表的正则
//Regex mu_re = new Regex(@"\+[-+]{4,}\s+\+[-+\s|\w./]{4,}\+"); //FTP new Regex(@"\+[-+]{4,}\s+\+[-+\s|\w./]{4,}\+"); //原来以-分隔的 new Regex(@"-{5,}(\s)+-{5,}\s+\|.+(\s)?\|.+(\s)?\|-{5,}");
////去掉所有横线
//Regex mu_r = new Regex(@"[+-]{4,}"); //FTP new Regex(@"[+-]{4,}"); //原 new Regex(@"(\|-{5,})|(-{5,})");
//string s1 = mu_re.Replace(txts, "");
//string s2 = mu_r.Replace(s1, "");
// string[] tts = s2.Split(new string[] { "\r\n" }, StringSplitOptions.None);
string[] tts = txts.Split(new string[] { "\r\n" }, StringSplitOptions.None);
string[] vals;
string s1;
//生成表头默认第一行时表头直到遇到第一个只有一个|的内容为止(有几行表头,下面的内容就会有几行)
int headerNum = -;//记录表头有几列 DataRow dr;
//处理col重复的问题,如果有重复按第几个来命名 比如 A1 A2
Dictionary<string, int> col_Rep = new Dictionary<string, int>();
string colName = "";
bool isre = false;//记录当前是否有重复列
int empty_HeaderRow = ;
for (int i = ; i < tts.Length; i++)
{
s1 = tts[i]; //还未获取出表头
if (headerNum < HeaderRowsNum)
{
vals = s1.Split(new string[] { Separator }, StringSplitOptions.RemoveEmptyEntries);
foreach (string col in vals)
{
colName = col.Trim(); if (col_Rep.Keys.Contains(colName))
{
col_Rep[colName]++;
isre = true;
//重复列处理
//colName += col_Rep[colName];
continue;
}
else
{
col_Rep.Add(colName, );
}
dt.Columns.Add(colName, typeof(string));
}
headerNum = (i == (HeaderRowsNum - )) ? HeaderRowsNum : ;
}
else
{
if (string.IsNullOrWhiteSpace(s1.Trim()) || string.IsNullOrWhiteSpace(s1.Replace(Separator, "")))
{
continue;
}
if (isre)
{
error = "列:" + string.Join(",", col_Rep.Where(c => c.Value > ).Select(c => c.Key)) + "存在重复";
return dt;
} //多行时把多行的数据加在一起处理
if (headerNum > )
{
for (int j = ; j < headerNum && (i + j) < tts.Length; j++)
{
//数据第一行最后没有| 如果没数据则直接换行了所以这里补一个空格防止数据被当空数据移除了
s1 += " " + tts[i + j];
}
}
vals = s1.Split(new string[] { Separator }, StringSplitOptions.RemoveEmptyEntries);
dr = dt.NewRow();
dr.ItemArray = vals;
dt.Rows.Add(dr);
//因为本次循环结束上面会去++ 所以这里只加headerNum-1次
i += (headerNum - );
} }
#endregion if (isCheck)
{
//dt.Columns.Remove("Item");
//dt.Columns["Item1"].ColumnName = "Item";
//dt.Columns.RemoveAt(dt.Columns.Count - 2);
error = CheckData(dt, dbTableName, map, sdb);
} return dt; }

C#读取Txt大数据并更新到数据库的更多相关文章

  1. python3 读取txt文件数据,绘制趋势图,matplotlib模块

    python3 读取txt文件数据,绘制趋势图 test1.txt内容如下: 时间/min cpu使用率/% 内存使用率/% 01/12-17:06 0.01 7.61 01/12-17:07 0.0 ...

  2. 大数据技术体系 && NoSQL数据库的基本原理

    1.NoSQL产生的原因 目前关系型数据库难以应对日益增多的海量数据,横向的分布式扩展能力比较弱,因此构建出非关系型数据库(所谓的NoSQL),其目的是为了构建一种结构简单.分布式.易扩展.效率高且使 ...

  3. 大数据系列之分布式数据库HBase-0.9.8安装及增删改查实践

    若查看HBase-1.2.4版本内容及demo代码详见 大数据系列之分布式数据库HBase-1.2.4+Zookeeper 安装及增删改查实践 1. 环境准备: 1.需要在Hadoop启动正常情况下安 ...

  4. Excel---导出与读取(大数据量)

    Excel下载 首先大数据量的下载,一般的Excel下载操作是不可能完成的,会导致内存溢出 SXSSFWorkbook 是专门用于大数据了的导出 构造入参rowAccessWindowSize 这个参 ...

  5. mongo 大数据量更新注意事项

    1.大数据量最好在本地执行更新. 2.在客户端执行更新时需要注意serve活动时间(10分钟),10分钟内解决不了的使用batchSize  或者db.getCollection("&quo ...

  6. 3.C++逐行读取txt文件数据,利用getline -windows编程

      引言:今天学会了getline的用法,顺手编写一个逐行读取txt文件的程序.关于getline的用法可以看我之前的博客:2.C++标准库函数:getline函数 定界流输入截取函数 -zobol的 ...

  7. oracle大数据量更新引发的死锁问题解决方法及oracle分区和存储过程的思考

    前言 前几天上午在对数据库的一张表进行操作的时候,由于这张表是按照时间的一张统计表,正好到那天没有测试数据了,于是我想将表中所有的时间,统一更新到后一个月,于是对80w条数据的更新开始了.整个过程曲折 ...

  8. 开源大数据引擎:Greenplum 数据库架构分析

    Greenplum 数据库是最先进的分布式开源数据库技术,主要用来处理大规模的数据分析任务,包括数据仓库.商务智能(OLAP)和数据挖掘等.自2015年10月正式开源以来,受到国内外业内人士的广泛关注 ...

  9. 【大数据】安装关系型数据库MySQL安装大数据处理框架Hadoop

    作业来源于:https://edu.cnblogs.com/campus/gzcc/GZCC-16SE2/homework/3161 1. 简述Hadoop平台的起源.发展历史与应用现状. 列举发展过 ...

随机推荐

  1. android_aidl

    好久未更新博客了.人都是这样,刚开始对某一样东东冲劲十足,时间一长,很难坚持下去了,我这博客也是.所以我要打破成规,继续更新. 本次博客谈谈adil的用法.aidl的全称叫什么来着忘了,不过不要紧,重 ...

  2. js方法总结,不断更新...

    1. 返回<li> 元素父节点: document.getElementById("item1").parentNode; 2.   

  3. 如何利用Azure Automation以及Tag自动开关VM

    这是本博客第一篇技术相关的小贴士,在这里我不会详细介绍所涉及的技术组件的具体使用细节,因为我相信这些大家都可以通过官方文档了解到.如果你是一个看了官方文档依然一脸茫然的IT小白,个人建议是先从基础重新 ...

  4. 解决win10下MySQL 8.0登录Access denied for user 'root'@'localhost' (using password: YES)的问题

        近些时间在开始学MySQL,安装挺顺利的,按照网上现成的教程就能安装成功.     但是,在输入 mysql -uroot -p     再输入密码时,遇到了这个情况 Access denie ...

  5. Web安全之CSRF攻击(转载)

    CSRF是什么? CSRF(Cross Site Request Forgery),中文是跨站点请求伪造.CSRF攻击者在用户已经登录目标网站之后,诱使用户访问一个攻击页面,利用目标网站对用户的信任, ...

  6. Spark Streaming消费Kafka Direct保存offset到Redis,实现数据零丢失和exactly once

    一.概述 上次写这篇文章文章的时候,Spark还是1.x,kafka还是0.8x版本,转眼间spark到了2.x,kafka也到了2.x,存储offset的方式也发生了改变,笔者根据上篇文章和网上文章 ...

  7. ubuntu搭建环境

    1.终端输入 sudo apt- add-apt-repository ppa:ondrej/php  sudo add-apt-repository ppa:ondrej/php  sudo apt ...

  8. Another option to bootup evidence files

    When it comes to booting up evidence files acquired from target disk, you got two options. One is VF ...

  9. 网站安装SSL证书成为影响SEO排名的重要因素之一

    百度谷歌先后发声明倡导站长们使用https链接,同样的网站,https站点要比http站点拥有更好的排名权重.https已经是网站SEO必须要考虑的环节之一了,而https的必要条件就是安装SSL证书 ...

  10. Cobbler 自动安装CentOS7

    1. Cobbler介绍 Cobbler是一个Linux服务器安装的服务,可以通过网络启动(PXE)的方式来快速安装.重装物理服务器和虚拟机,同时还可以管理DHCP,DNS等.Cobbler可以使用命 ...