C# Excel导入、导出【源码下载】 - 方木一 - 博客园

mikel阅读(667)

来源: C# Excel导入、导出【源码下载】 – 方木一 – 博客园

本篇主要介绍C#的Excel导入、导出。

目录

1. 介绍:描述第三方类库NPOI以及Excel结构

2. Excel导入:介绍C#如何调用NPOI进行Excel导入,包含:流程图、NOPI以及C#代码

3. Excel导出:介绍C#如何调用NPOI进行Excel导出,包含:流程图、NOPI以、C#代码以及代码分析

4. 源码下载:展示运行图及源码下载

1. 介绍

1.1 第三方类库:NPOI

说明:NPOI是POI项目的.NET 版本,可用于Excel、Word的读写操作。

优点:不用装Office环境。

下载地址:http://npoi.codeplex.com/releases

1.2 Excel结构介绍

工作簿(Workbook):每个Excel文件可理解为一个工作簿。

工作表(Sheet):一个工作簿(Workbook)可以包含多个工作表。

行(row):一个工作表(Sheet)可以包含多个行。

2. Excel导入

2.1 操作流程

2.2 NPOI操作代码

说明:把Excel文件转换为List<T>

步骤:

①读取Excel文件并以此初始化一个工作簿(Workbook);

②从工作簿上获取一个工作表(Sheet);默认为工作薄的第一个工作表;

③遍历工作表所有的行(row);默认从第二行开始遍历,第一行(序号0)为单元格头部;

④遍历行的每一个单元格(cell),根据一定的规律赋值给对象的属性。

/// <summary>
/// 从Excel2003取数据并记录到List集合里
/// </summary>
/// <param name="cellHeard">单元头的Key和Value:{ { "UserName", "姓名" }, { "Age", "年龄" } };</param>
/// <param name="filePath">保存文件绝对路径</param>
/// <param name="errorMsg">错误信息</param>
/// <returns>转换好的List对象集合</returns>
private static List<T> Excel2003ToEntityList<T>(Dictionary<stringstring> cellHeard, string filePath, out StringBuilder errorMsg) where T : new()
{
    errorMsg = new StringBuilder(); // 错误信息,Excel转换到实体对象时,会有格式的错误信息
    List<T> enlist = new List<T>(); // 转换后的集合
    List<string> keys = cellHeard.Keys.ToList(); // 要赋值的实体对象属性名称
    try
    {
        using (FileStream fs = File.OpenRead(filePath))
        {
            HSSFWorkbook workbook = new HSSFWorkbook(fs);
            HSSFSheet sheet = (HSSFSheet)workbook.GetSheetAt(0); // 获取此文件第一个Sheet页
            for (int i = 1; i <= sheet.LastRowNum; i++) // 从1开始,第0行为单元头
            {
                // 1.判断当前行是否空行,若空行就不在进行读取下一行操作,结束Excel读取操作
                if (sheet.GetRow(i) == null)
                {
                    break;
                }
                T en = new T();
                string errStr = ""// 当前行转换时,是否有错误信息,格式为:第1行数据转换异常:XXX列;
                for (int j = 0; j < keys.Count; j++)
                {
                    // 2.若属性头的名称包含'.',就表示是子类里的属性,那么就要遍历子类,eg:UserEn.TrueName
                    if (keys[j].IndexOf(".") >= 0)
                    {
                        // 2.1解析子类属性
                        string[] properotyArray = keys[j].Split(new string[] { "." }, StringSplitOptions.RemoveEmptyEntries);
                        string subClassName = properotyArray[0]; // '.'前面的为子类的名称
                        string subClassProperotyName = properotyArray[1]; // '.'后面的为子类的属性名称
                        System.Reflection.PropertyInfo subClassInfo = en.GetType().GetProperty(subClassName); // 获取子类的类型
                        if (subClassInfo != null)
                        {
                            // 2.1.1 获取子类的实例
                            var subClassEn = en.GetType().GetProperty(subClassName).GetValue(en, null);
                            // 2.1.2 根据属性名称获取子类里的属性信息
                            System.Reflection.PropertyInfo properotyInfo = subClassInfo.PropertyType.GetProperty(subClassProperotyName);
                            if (properotyInfo != null)
                            {
                                try
                                {
                                    // Excel单元格的值转换为对象属性的值,若类型不对,记录出错信息
                                    properotyInfo.SetValue(subClassEn, GetExcelCellToProperty(properotyInfo.PropertyType, sheet.GetRow(i).GetCell(j)), null);
                                }
                                catch (Exception e)
                                {
                                    if (errStr.Length == 0)
                                    {
                                        errStr = "第" + i + "行数据转换异常:";
                                    }
                                    errStr += cellHeard[keys[j]] + "列;";
                                }
                                
                            }
                        }
                    }
                    else
                    {
                        // 3.给指定的属性赋值
                        System.Reflection.PropertyInfo properotyInfo = en.GetType().GetProperty(keys[j]);
                        if (properotyInfo != null)
                        {
                            try
                            {
                                // Excel单元格的值转换为对象属性的值,若类型不对,记录出错信息
                                properotyInfo.SetValue(en, GetExcelCellToProperty(properotyInfo.PropertyType, sheet.GetRow(i).GetCell(j)), null);
                            }
                            catch (Exception e)
                            {
                                if (errStr.Length == 0)
                                {
                                    errStr = "第" + i + "行数据转换异常:";
                                }
                                errStr += cellHeard[keys[j]] + "列;";
                            }
                        }
                    }
                }
                // 若有错误信息,就添加到错误信息里
                if (errStr.Length > 0)
                {
                    errorMsg.AppendLine(errStr);
                }
                enlist.Add(en);
            }
        }
        return enlist;
    }
    catch (Exception ex)
    {
        throw ex;
    }
}

2.3 C#逻辑操作代码

说明:对Excel转换后的List<T>进行后续操作;如:检测有效性、持久化存储等等

步骤:

①调用2.2代码,把Excel文件转换为List<T>。

②对List<T>进行有效性检测:必填项是否为空、是否有重复记录等等。

③对List<T>进行持久化存储操作。如:存储到数据库。

④返回操作结果。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
public void ImportExcel(HttpContext context)
{
    StringBuilder errorMsg = new StringBuilder(); // 错误信息
    try
    {
        #region 1.获取Excel文件并转换为一个List集合
        // 1.1存放Excel文件到本地服务器
        HttpPostedFile filePost = context.Request.Files["filed"]; // 获取上传的文件
        string filePath = ExcelHelper.SaveExcelFile(filePost); // 保存文件并获取文件路径
        // 单元格抬头
        // key:实体对象属性名称,可通过反射获取值
        // value:属性对应的中文注解
        Dictionary<stringstring> cellheader = new Dictionary<stringstring> {
            "Name""姓名" },
            "Age""年龄" },
            "GenderName""性别" },
            "TranscriptsEn.ChineseScores""语文成绩" },
            "TranscriptsEn.MathScores""数学成绩" },
        };
        // 1.2解析文件,存放到一个List集合里
        List<UserEntity> enlist = ExcelHelper.ExcelToEntityList<UserEntity>(cellheader, filePath, out errorMsg);
        #endregion
        #region 2.对List集合进行有效性校验
        #region 2.1检测必填项是否必填
        for (int i = 0; i < enlist.Count; i++)
        {
            UserEntity en = enlist[i];
            string errorMsgStr = "第" + (i + 1) + "行数据检测异常:";
            bool isHaveNoInputValue = false// 是否含有未输入项
            if (string.IsNullOrEmpty(en.Name))
            {
                errorMsgStr += "姓名列不能为空;";
                isHaveNoInputValue = true;
            }
            if (isHaveNoInputValue) // 若必填项有值未填
            {
                en.IsExcelVaildateOK = false;
                errorMsg.AppendLine(errorMsgStr);
            }
        }
        #endregion
        #region 2.2检测Excel中是否有重复对象
        for (int i = 0; i < enlist.Count; i++)
        {
            UserEntity enA = enlist[i];
            if (enA.IsExcelVaildateOK == false// 上面验证不通过,不进行此步验证
            {
                continue;
            }
            for (int j = i + 1; j < enlist.Count; j++)
            {
                UserEntity enB = enlist[j];
                // 判断必填列是否全部重复
                if (enA.Name == enB.Name)
                {
                    enA.IsExcelVaildateOK = false;
                    enB.IsExcelVaildateOK = false;
                    errorMsg.AppendLine("第" + (i + 1) + "行与第" + (j + 1) + "行的必填列重复了");
                }
            }
        }
        #endregion
        // TODO:其他检测
        #endregion
        // 3.TODO:对List集合持久化存储操作。如:存储到数据库
        
        // 4.返回操作结果
        bool isSuccess = false;
        if (errorMsg.Length == 0)
        {
            isSuccess = true// 若错误信息成都为空,表示无错误信息
        }
        var rs = new { success = isSuccess,  msg = errorMsg.ToString(), data = enlist };
        System.Web.Script.Serialization.JavaScriptSerializer js = new System.Web.Script.Serialization.JavaScriptSerializer();
        context.Response.ContentType = "text/plain";
        context.Response.Write(js.Serialize(rs)); // 返回Json格式的内容
    }
    catch (Exception ex)
    {
     throw ex;
    }
}

3. Excel导出

3.1 导出流程

3.2 NPOI操作代码

说明:把List<T>转换为Excel

步骤:

①创建一个工作簿(Workbook);

②在工作簿上创建一个工作表(Sheet);

③在工作表上创建第一行(row),第一行为列头,依次写入cellHeard的值(做为列名)。

④循环遍历List<T>集合,每循环一遍创建一个行(row),然后根据cellHeard的键(属性名称)依次从List<T>中的实体对象取值存放到单元格内。

代码:

3.3 C#逻辑操作代码

说明:对Excel转换后的List<T>进行后续操作;如:检测有效性、持久化存储等等

步骤:

①获取List<T>集合。

②调用3.2,将List<T>转换为Excel文件。

③服务器存储Excel文件并返回下载链接。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public void ExportExcel(HttpContext context)
{
    try
    {
        // 1.获取数据集合
        List<UserEntity> enlist = new List<UserEntity>() {
            new UserEntity{Name="刘一",Age=22,Gender="Male",TranscriptsEn=new TranscriptsEntity{ChineseScores=80,MathScores=90}},
            new UserEntity{Name="陈二",Age=23,Gender="Male",TranscriptsEn=new TranscriptsEntity{ChineseScores=81,MathScores=91} },
            new UserEntity{Name="张三",Age=24,Gender="Male",TranscriptsEn=new TranscriptsEntity{ChineseScores=82,MathScores=92} },
            new UserEntity{Name="李四",Age=25,Gender="Male",TranscriptsEn=new TranscriptsEntity{ChineseScores=83,MathScores=93} },
            new UserEntity{Name="王五",Age=26,Gender="Male",TranscriptsEn=new TranscriptsEntity{ChineseScores=84,MathScores=94} },
        };
        // 2.设置单元格抬头
        // key:实体对象属性名称,可通过反射获取值
        // value:Excel列的名称
        Dictionary<stringstring> cellheader = new Dictionary<stringstring> {
            "Name""姓名" },
            "Age""年龄" },
            "GenderName""性别" },
            "TranscriptsEn.ChineseScores""语文成绩" },
            "TranscriptsEn.MathScores""数学成绩" },
        };
        // 3.进行Excel转换操作,并返回转换的文件下载链接
        string urlPath = ExcelHelper.EntityListToExcel2003(cellheader, enlist, "学生成绩");
        System.Web.Script.Serialization.JavaScriptSerializer js = new System.Web.Script.Serialization.JavaScriptSerializer();
        context.Response.ContentType = "text/plain";
        context.Response.Write(js.Serialize(urlPath)); // 返回Json格式的内容
    }
    catch (Exception ex)
    {
        throw ex;
    }
}

3.4 代码分析

核心代码主要是cellheader与List<T>之间的映射关系:

4. 源码下载

4.1 运行图

4.2 下载地址

百度网盘: http://pan.baidu.com/s/1o69We8M

CSDN:http://download.csdn.net/download/polk6/8974195

2018/07/11更新,添加对Excel 2007的导入支持:

git:https://github.com/polk6/CSharp-Excel

记一次SQLServer的分页优化兼谈谈使用Row_Number()分页存在的问题 - 马非码 - 博客园

mikel阅读(669)

来源: 记一次SQLServer的分页优化兼谈谈使用Row_Number()分页存在的问题 – 马非码 – 博客园

最近有项目反应,在服务器CPU使用较高的时候,我们的事件查询页面非常的慢,查询几条记录竟然要4分钟甚至更长,而且在翻第二页的时候也是要这么多的时间,这肯定是不能接受的,也是让现场用SQLServerProfiler把语句抓取了上来。

用ROW_NUMBER()进行分页

我们看看现场抓上来的分页语句:

select top 20 a.*,ag.Name as AgentServerName,,d.Name as MgrObjTypeName,l.UserName as userName 
from eventlog as a 
    left join mgrobj as b on a.MgrObjId=b.Id and a.AgentBm=b.AgentBm 
    left join addrnode as c on b.AddrId=c.Id 
    left join mgrobjtype as d on b.MgrObjTypeId=d.Id 
    left join eventdir as e on a.EventBm=e.Bm 
    left join agentserver as ag on a.AgentBm=ag.AgentBm 
    left join loginUser as l on a.cfmoper=l.loginGuid 
where a.OrderNo not in  (
    select top 0 OrderNo  
    from eventlog  as a 
        left join mgrobj as b on a.MgrObjId=b.Id 
        left join addrnode as c on b.AddrId=c.Id  
    where 1=1 and a.AlarmTime>='2014-12-01 00:00:00' and a.AlarmTime<='2014-12-26 23:59:59' 
        and b.AddrId in ('02109000',……,'02109002') 
    order by  AlarmTime desc 
    )  
and 1=1 and a.AlarmTime>='2014-12-01 00:00:00' and a.AlarmTime<='2014-12-26 23:59:59' 
    and b.AddrId in ('02109000',……,'02109002') 
order by  AlarmTime DESC

这是典型的使用两次top来进行分页的写法,原理是:先查出pageSize*(pageIndex-1)(T1)的记录数,然后再TopPageSize条不在T1中的记录,就是当前页的记录。这种查询效率不高主要是使用了not in。参考我之前文章《程序猿是如何解决SQLServer占CPU100%的》提到的:“对于不使用SARG运算符的表达式,索引是没有用的”

那么改为使用ROW_NUMBER分页:

WITH cte AS(
    select a.*,ag.Name as AgentServerName,d.Name as MgrObjTypeName,l.UserName as userName,b.AddrId
            ,ROW_NUMBER() OVER(ORDER BY AlarmTime DESC) AS RowNo
        from eventlog as a WITH(FORCESEEK) 
            left join mgrobj as b on a.MgrObjId=b.Id and a.AgentBm=b.AgentBm 
            left join addrnode as c on b.AddrId=c.Id 
            left join mgrobjtype as d on b.MgrObjTypeId=d.Id 
            left join eventdir as e on a.EventBm=e.Bm 
            left join agentserver As ag on a.AgentBm=ag.AgentBm 
            left join loginUser as l on a.cfmoper=l.loginGuid 
        where a.AlarmTime>='2014-12-01 00:00:00' and a.AlarmTime<='2014-12-26 23:59:59' 
            AND b.AddrId in ('02109000',……,'02109002')
)
SELECT * FROM cte WHERE RowNo BETWEEN 1 AND 20;

执行时间从14秒提升到5秒,这说明Row_Number分页还是比较高效的,而且这种写法比top top分页优雅很多。

“欺骗”查询引擎让查询按你的期望去查询

但是为什么查询20条记录竟然要5秒呢,尤其在这个表是加上了时间索引的情况下——参考《程序猿是如何解决SQLServer占CPU100%的》中提到的索引。

我尝试去掉这句AND b.AddrId in ('02109000',……,'02109002'),结果不到1秒就把538条记录查询出来了,而加上地点限制这句,结果是204行。为什么结果集不大,花费的时间却相差这么多呢?查看执行计划,发现走的是另外的索引,而非时间索引。

把这个疑问放到了SQLServer群上,很快,高桑给了回复:要想达到跟去掉地点限制这句的效果,就使用AdddrId+'' in

什么意思?一时没看明白,是高桑没看懂我的语句?很快,有人补充,要欺骗查询引擎。“欺骗”?还是不懂,不过我照做了,把上述cte的语句原封不动的Copy出来,然后把这句AND b.AddrId in ('02109000',……,'02109002')更改为了AND b.AddrId+'' in ('02109000',……,'02109002'),一点执行,神了!!!不到1秒就执行完了。在把执行计划一对,果然走的是时间索引:

后来回味了一下,记起之前看到的查询引擎优化原理,如果你的条件中带有运算符或者使用函数等,则查询引擎会放弃优化,而执行表扫描。脑袋突然转过来了,在使用b.AddrId+''前查询引擎尝试把mgrObj表加入一起做优化,那么两个表联查,会导致预估的记录数大大增加,而使用了b.AddrId+'',查询引擎则会先按时间索引把记录刷选出来,这样就达到了效果,即强制先做cte在执行in条件,而不是在cte中进行in条件刷选。原来如此!有时候,查询引擎过度的优化,会导致相反的效果,而你如果能够知道优化的原理,那么就可以通过一些小的技巧让查询引擎按你的期望去进行优化

ROW_NUMBER()分页在页数较大时的问题

事情到这里,还没完。后面同事又跟我反应,查询到后面的页数,又卡了!what?我重新执行上述语句,把时间范围放到2011-12-01到2014-12-26,记录数限制为为19981到20000,果然,查询要30秒左右,查看执行计划,都是一样的,为什么?

高桑怀疑是key lookup过多导致的,建议先分页取出rid 再做key lookup。不懂这么一句是什么意思。把执行计划和IO打印出来:

看看IO,很明显,主要是越到后面的页数,其他的几个关联表读取的页数就越多。我推测,在Row_Number分页的时候,如果有表连接,则按排序一致到返回的记录数位置,前面的记录都是要参与表连接的,这就导致了越到后面的分页,就越慢,因为要扫描的关联表就越多。

难道就没有了办法了吗?这个时候宋桑英勇的站了出来:“你给表后加一个forceseek提示可破”。这真是犹如天籁之音,马上进行尝试。

使用forceseek提示可以强制表走索引

查了下资料:

SQL Server2008中引入的提示ForceSeek,可以用它将索引查找来替换索引扫描

那么,就在eventlog表中加上这句看看会怎样?

果然,查询计划变了,开始提示,缺少了包含索引。赶紧加上,果然,按这个方式进行查询之后查询时间变为18秒,有进步!但是查看IO,跟上面一样,并没有变少。不过,总算学会了一个新的技能,而宋桑也很热心说晚上再帮忙看看。

把其他没参与where的表放到cte外面

根据上面的IO,很快,又有人提到,把其他left join的表放到cte外面。这是个办法,于是把除eventlogmgrobjaddrnode的表放到外面,语句如下:

WITH cte AS(
    select a*,b.AddrId,b.Name as MgrObjName,b.MgrObjTypeId          
            ,ROW_NUMBER() OVER(ORDER BY AlarmTime DESC) AS RowNo
        from eventlog as a
            left join mgrobj as b on a.MgrObjId=b.Id and a.AgentBm=b.AgentBm 
            left join addrnode as c on b.AddrId=c.Id 
        where a.AlarmTime>='2011-12-01 00:00:00' and a.AlarmTime<='2014-12-26 23:59:59' 
            AND b.AddrId+'' in ('02109000',……,'02109002')
)
SELECT a.* 
    ,ag.Name as AgentServerName
    ,d.Name as MgrObjTypeName,l.UserName as userName
FROM cte a left join eventdir as e on a.EventBm=e.Bm 
            left join mgrobjtype as d on a.MgrObjTypeId=d.Id 
            left join agentserver As ag on a.AgentBm=ag.AgentBm 
            left join loginUser as l on a.cfmoper=l.loginGuid 
WHERE RowNo BETWEEN 19980 AND 20000;

果然有效,IO大大减少了,然后速度也提升到了16秒。

'loginuser'。扫描计数 1,逻辑读取 63 次,物理读取 0 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
表 'agentserver'。扫描计数 1,逻辑读取 1617 次,物理读取 0 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
表 'mgrobjtype'。扫描计数 1,逻辑读取 126 次,物理读取 0 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
表 'eventdir'。扫描计数 1,逻辑读取 42 次,物理读取 0 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
表 'addrnode'。扫描计数 1,逻辑读取 119997 次,物理读取 0 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
表 'Worktable'。扫描计数 0,逻辑读取 0 次,物理读取 0 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
表 'eventlog'。扫描计数 1,逻辑读取 5027 次,物理读取 3 次,预读 5024 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
表 'mgrobj'。扫描计数 1,逻辑读取 24 次,物理读取 0 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。

我们看到,addrNode表还是扫描计数很大。那还能不能提升,这个时候,我想到了,先把addrNodemgrobjmgrobjtype三个表联合查询,放到一个临时表,然后再和eventloginner join,然后查询结果再和其他表做left join,这样还能减少IO。

使用临时表存储分页记录在进行表连接减少IO

IF OBJECT_ID('tmpMgrObj') IS NOT NULL DROP TABLE tmpMgrObj
SELECT m.Id,AddrId,MgrObjTypeId,AgentBM,m.Name,a.Name AS AddrName 
    INTO tmpMgrObj  
    FROM dbo.mgrobj m
        INNER JOIN dbo.addrnode a ON a.Id=m.AddrId
    WHERE AddrId IN('02109000',……,'02109002');
WITH cte AS(
    select a.*,b.AddrId,b.MgrObjTypeId          
            ,ROW_NUMBER() OVER(ORDER BY AlarmTime DESC) AS RowNo
            ,ag.Name as AgentServerName
    ,d.Name as MgrObjTypeName,l.UserName as userName
        from eventlog as a
            INNER join tmpMgrObj as b on a.MgrObjId=b.Id and a.AgentBm=b.AgentBm
            left join mgrobjtype as d on b.MgrObjTypeId=d.Id 
            left join agentserver As ag on a.AgentBm=ag.AgentBm 
            left join loginUser as l on a.cfmoper=l.loginGuid 
    WHERE AlarmTime>'2011-12-01 00:00:00' AND AlarmTime<='2014-12-26 23:59:59'
) 
SELECT * FROM cte WHERE RowNo BETWEEN 19980 AND 20000
IF OBJECT_ID('tmpMgrObj') IS NOT NULL DROP TABLE tmpMgrObj

这次查询仅用了10秒。我们来看看IO:

'Worktable'。扫描计数 0,逻辑读取 0 次,物理读取 0 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
表 'mgrobj'。扫描计数 1,逻辑读取 24 次,物理读取 2 次,预读 23 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
表 'addrnode'。扫描计数 1,逻辑读取 6 次,物理读取 3 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
----------'loginuser'。扫描计数 0,逻辑读取 24 次,物理读取 1 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
表 'Worktable'。扫描计数 0,逻辑读取 0 次,物理读取 0 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
表 'eventlog'。扫描计数 93,逻辑读取 32773 次,物理读取 515 次,预读 1536 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
表 'tmpMgrObj'。扫描计数 1,逻辑读取 3 次,物理读取 0 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
表 'mgrobjtype'。扫描计数 1,逻辑读取 6 次,物理读取 1 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
表 'agentserver'。扫描计数 1,逻辑读取 77 次,物理读取 2 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。

除了eventlog之外,其他的表的IO大大减少,有木有?

强制使用hash join

经网友提示,在大的页数时,可以强制使用hash join来减少IO,而且经过尝试,可以通过建立两个子查询来避免使用临时表。经过调整,最终优化的SQL语句如下:

SELECT  *
    ,ag.Name AS AgentServerName
    , l.UserName AS userName
FROM    ( 
    SELECT    a.*,ROW_NUMBER() OVER (ORDER BY AlarmTime DESC) AS RowNo
        , b.AddrName , b.Name AS MgrObjName
    FROM
        (SELECT    * 
            FROM      eventlog
            WHERE     AlarmTime>= '2011-12-01 00:00:00' AND AlarmTime< '2014-12-26 23:59:59') AS a
        INNER HASH JOIN (
            SELECT m.Id,AddrId,MgrObjTypeId,AgentBM,m.Name,a.Name AS AddrName,t.Name AS MgrObjTypeName
            FROM dbo.mgrobj m
                INNER JOIN dbo.addrnode a ON a.Id=m.AddrId
                INNER JOIN dbo.mgrobjtype t ON m.MgrObjTypeId=t.Id
            WHERE AddrId IN('02109000',……,'02109002')
        ) AS b ON a.MgrObjId=b.Id AND a.AgentBM=b.AgentBm
        
) tmp 
    LEFT JOIN agentserver AS ag ON tmp.AgentBm = ag.AgentBm
    LEFT JOIN eventdir AS e ON tmp.EventBm = e.Bm
    LEFT JOIN loginUser AS l ON tmp.cfmoper = l.loginGuid
WHERE tmp.RowNo BETWEEN 190001 AND 190020

在大的分页的时候,通过hash查询,不必扫描前面的页数,可以大大减少IO,但是,由于hash join是强制性的,所以使用的时候要注意,我这里应该是个特例。

查询分析器的提示:“警告: 由于使用了本地联接提示,联接次序得以强制实施。”

我们来看看对应情况下的IO:

'eventlog'。扫描计数 5,逻辑读取 5609 次,物理读取 34 次,预读 5636 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
表 'Worktable'。扫描计数 3,逻辑读取 375 次,物理读取 0 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
表 'Worktable'。扫描计数 0,逻辑读取 0 次,物理读取 0 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
表 'mgrobj'。扫描计数 5,逻辑读取 24 次,物理读取 8 次,预读 40 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
表 'mgrobjtype'。扫描计数 1,逻辑读取 6 次,物理读取 1 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
表 'addrnode'。扫描计数 3,逻辑读取 18 次,物理读取 6 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
表 'loginuser'。扫描计数 1,逻辑读取 60 次,物理读取 2 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
表 'eventdir'。扫描计数 1,逻辑读取 40 次,物理读取 0 次,预读 0 次,lob 逻辑读取 30 次,lob 物理读取 0 次,lob 预读 0 次。
表 'agentserver'。扫描计数 1,逻辑读取 1540 次,物理读取 1 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。

这次的IO表现非常的好,没有因为查询后面的页数增大而导致较大的IO,查询时间从没有使用hash join的50秒提升为只需12秒,查询时间的开销应该耗费了在hash查找上了。

再看看对应的查询计划,这个时候,主要是因为排序的开销较大。

我们再看看他的预估的和执行的区别,为什么会让排序占如此大的开销?

很明显,预估的时候只需对刷选的结果排序,但是实际执行是对前面所有的页数进行了排序,最终排序占了大部分的开销。那么,这种情况能破吗?请留下您的回复!

其他优化参考

在另外的群上讨论时,发现使用ROW_NUMBER分页查询到后面的页数会越来越慢的这个问题的确困扰了不少的人。

有的人提出,谁会这么无聊,把页数翻到几千页以后?一开始我也是这么想的,但是跟其他人交流之后,发现确实有这么一种场景,我们的软件提供了最后一页这个功能,结果……当然,一种方法就是在设计软件的时候,就去掉这个最后一页的功能;另外一种思路,就是查询页数过半之后,就反向查询,那么查询最后一页其实也就是查询第一页。

还有一些人提出,把查询出来的内容,放到一个临时表,这个临时表中的加入自增Id的索引,这样,可以通过辨别Id来进行快速刷选记录。这也是一种方法,我打算稍后尝试。但是这种方法也是存在问题的,就是无法做到通用,必须根据每个表进行临时表的构建,另外,在超大数据查询时,插入的记录过多,因为索引的存在也是会慢的,而且每次都这么做,估计CPU也挺吃紧。但是不管怎么样,这是一种思路。

你有什么好的建议?不妨把你的想法在评论中提出来,一起讨论讨论。

总结

现在,我们来总结下在这次优化过程中学习到什么内容:

  • 在SQLServer中,ROW_NUMBER的分页应该是最高效的了,而且兼容SQLServer2005以后的数据库
  • 通过“欺骗”查询引擎的小技巧,可以控制查询引擎部分的优化过程
  • ROW_NUMBER分页在大页数时存在性能问题,可以通过一些小技巧进行规避
    • 尽量通过cte利用索引
    • 把不参与where条件的表放到分页的cte外面
    • 如果参与where条件的表过多,可以考虑把不参与分页的表先做一个临时表,减少IO
    • 在较大页数的时候强制使用hash join可以减少io,从而获得很好的性能
  • 使用with(forceseek)可以强制查询因此进行索引查询

最后,感谢SQLServer群的高桑、宋桑、肖桑和其他群友的大力帮助,这个杜绝吹水的群非常的棒,让我这个程序猿学到了很多数据库的知识!

注:经网友提示,2015-01-07 09:15做以下更新

  • 可以在记录数超过10000条,则采用hash join强制进行hash连接,减少IO(感谢27楼riccc)
  • 去掉最先给定的结果中采用left join而不是inner join的连接——left join的结果相当于没有用上addrId in ()的条件(感谢32楼夏浩)

参考文章

EasyExcel 轻松灵活读取Excel内容 - 日拱一兵 - 博客园

mikel阅读(720)

来源: EasyExcel 轻松灵活读取Excel内容 – 日拱一兵 – 博客园

写在前面

Java 后端程序员应该会遇到读取 Excel 信息到 DB 等相关需求,脑海中可能突然间想起 Apache POI 这个技术解决方案,但是当 Excel 的数据量非常大的时候,你也许发现,POI 是将整个 Excel 的内容全部读出来放入到内存中,所以内存消耗非常严重,如果同时进行包含大数据量的 Excel 读操作,很容易造成内存溢出问题

但 EasyExcel 的出现很好的解决了 POI 相关问题,原本一个 3M 的 Excel 用 POI 需要100M左右内存, 而 EasyExcel 可以将其降低到几 M,同时再大的 Excel 都不会出现内存溢出的情况,因为是逐行读取 Excel 的内容 (老规矩,这里不用过分关心下图,脑海中有个印象即可,看完下面的用例再回看这个图,就很简单了)

另外 EasyExcel 在上层做了模型转换的封装,不需要 cell 等相关操作,让使用者更加简单和方便,且看

简单读

假设我们 excel 中有以下内容:

我们需要新建 User 实体,同时为其添加成员变量

@Data
public class User {

    /**
     * 姓名
     */
    @ExcelProperty(index = 0)
    private String name;

    /**
     * 年龄
     */
    @ExcelProperty(index = 1)
    private Integer age;
}

你也许关注到了 @ExcelProperty 注解,同时使用了 index 属性 (0 代表第一列,以此类推),该注解同时支持以「列名」name 的方式匹配,比如:

@ExcelProperty("姓名")
private String name;

按照 github 文档的说明:

不建议 index 和 name 同时用,要么一个对象只用index,要么一个对象只用name去匹配

  1. 如果读取的 Excel 模板信息列固定,这里建议以 index 的形式使用,因为如果用名字去匹配,名字重复,会导致只有一个字段读取到数据,所以 index 是更稳妥的方式
  2. 如果 Excel 模板的列 index 经常有变化,那还是选择 name 方式比较好,不用经常性修改实体的注解 index 数值

所以大家可以根据自己的情况自行选择

编写测试用例

EasyExcel 类中重载了很多个 read 方法,这里不一一列举说明,请大家自行查看;同时 sheet 方法也可以指定 sheetNo,默认是第一个 sheet 的信息

上面代码的 new UserExcelListener() 异常醒目,这也是 EasyExcel 逐行读取 Excel 内容的关键所在,自定义 UserExcelListener 继承 AnalysisEventListener

@Slf4j
public class UserExcelListener extends AnalysisEventListener<User> {

    /**
     * 批处理阈值
     */
    private static final int BATCH_COUNT = 2;
    List<User> list = new ArrayList<User>(BATCH_COUNT);

    @Override
    public void invoke(User user, AnalysisContext analysisContext) {
        log.info("解析到一条数据:{}", JSON.toJSONString(user));
        list.add(user);
        if (list.size() >= BATCH_COUNT) {
            saveData();
            list.clear();
        }
    }

    @Override
    public void doAfterAllAnalysed(AnalysisContext analysisContext) {
        saveData();
        log.info("所有数据解析完成!");
    }

    private void saveData(){
        log.info("{}条数据,开始存储数据库!", list.size());
        log.info("存储数据库成功!");
    }
}

到这里请回看文章开头的 EasyExcel 原理图,invoke 方法逐行读取数据,对应的就是订阅者 1;doAfterAllAnalysed 方法对应的就是订阅者 2,这样你理解了吗?

打印结果:

从这里可以看出,虽然是逐行解析数据,但我们可以自定义阈值,完成数据的批处理操作,可见 EasyExcel 操作的灵活性

自定义转换器

这是最基本的数据读写,我们的业务数据通常不可能这么简单,有时甚至需要将其转换为程序可读的数据

性别信息转换

比如 Excel 中新增「性别」列,其性别为男/女,我们需要将 Excel 中的性别信息转换成程序信息: 「1: 男;2:女」

首先在 User 实体中添加成员变量 gender:

@ExcelProperty(index = 2)
private Integer gender;

EasyExcel 支持我们自定义 converter,将 excel 的内容转换为我们程序需要的信息,这里新建 GenderConverter,用来转换性别信息

public class GenderConverter implements Converter<Integer> {

    public static final String MALE = "男";
    public static final String FEMALE = "女";

    @Override
    public Class supportJavaTypeKey() {
        return Integer.class;
    }

    @Override
    public CellDataTypeEnum supportExcelTypeKey() {
        return CellDataTypeEnum.STRING;
    }

    @Override
    public Integer convertToJavaData(CellData cellData, ExcelContentProperty excelContentProperty, GlobalConfiguration globalConfiguration) throws Exception {
        String stringValue = cellData.getStringValue();
        if (MALE.equals(stringValue)){
            return 1;
        }else {
            return 2;
        }
    }

    @Override
    public CellData convertToExcelData(Integer integer, ExcelContentProperty excelContentProperty, GlobalConfiguration globalConfiguration) throws Exception {
        return null;
    }
}

上面程序的 Converter 接口的泛型是指要转换的 Java 数据类型,与 supportJavaTypeKey 方法中的返回值类型一致

打开注解 @ExcelProperty 查看,该注解是支持自定义 Converter 的,所以我们为 User 实体添加 gender 成员变量,并指定 converter

/**
 * 性别 1:男;2:女
 */
@ExcelProperty(index = 2, converter = GenderConverter.class)
private Integer gender;

来看运行结果:

数据按照我们预期做出了转换,从这里也可以看出,Converter 可以一次定义到处是用的便利性

日期信息转换

日期信息也是我们常见的转换数据,比如 Excel 中新增「出生年月」列,我们要解析成 yyyy-MM-dd 格式,我们需要将其进行格式化,EasyExcel 通过 @DateTimeFormat 注解进行格式化

在 User 实体中添加成员变量 birth,同时应用 @DateTimeFormat 注解,按照要求做格式化

/**
 * 出生日期
 */
@ExcelProperty(index = 3)
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private String birth;

来看运行结果:

如果这里你指定 birth 的类型为 Date,试试看,你得到的结果是什么?

到这里都是以测试的方式来编写程序代码,作为 Java Web 开发人员,尤其在目前主流 Spring Boot 的架构下,所以如何实现 Web 方式读取 Excel 的信息呢?

web 读

简单 Web

很简单,只是将测试用例的关键代码移动到 Controller 中即可,我们新建一个 UserController,在其添加 upload 方法

@RestController
@RequestMapping("/users")
@Slf4j
public class UserController {
    @PostMapping("/upload")
    public String upload(MultipartFile file) throws IOException {
        EasyExcel.read(file.getInputStream(), User.class, new UserExcelListener()).sheet().doRead();
        return "success";
    }
}

其实在写测试用例的时候你也许已经发现,listener 是以 new 的形式作为参数传入到 EasyExcel.read 方法中的,这是不符合 Spring IoC 的规则的,我们通常读取 Excel 数据之后都要针对读取的数据编写一些业务逻辑的,而业务逻辑通常又会写在 Service 层中,我们如何在 listener 中调用到我们的 service 代码呢?

先不要向下看,你脑海中有哪些方案呢?

匿名内部类方式

匿名内部类是最简单的方式,我们需要先新建 Service 层的信息:
新建 IUser 接口:

public interface IUser {
    public boolean saveData(List<User> users);
}

新建 IUser 接口实现类 UserServiceImpl:

@Service
@Slf4j
public class UserServiceImpl implements IUser {
    @Override
    public boolean saveData(List<User> users) {
        log.info("UserService {}条数据,开始存储数据库!", users.size());
        log.info(JSON.toJSONString(users));
        log.info("UserService 存储数据库成功!");
        return true;
    }
}

接下来,在 Controller 中注入 IUser:

@Autowired
private IUser iUser;

修改 upload 方法,以匿名内部类重写 listener 方法的形式来实现:

@PostMapping("/uploadWithAnonyInnerClass")
    public String uploadWithAnonyInnerClass(MultipartFile file) throws IOException {
        EasyExcel.read(file.getInputStream(), User.class, new AnalysisEventListener<User>(){
            /**
             * 批处理阈值
             */
            private static final int BATCH_COUNT = 2;
            List<User> list = new ArrayList<User>();

            @Override
            public void invoke(User user, AnalysisContext analysisContext) {
                log.info("解析到一条数据:{}", JSON.toJSONString(user));
                list.add(user);
                if (list.size() >= BATCH_COUNT) {
                    saveData();
                    list.clear();
                }
            }

            @Override
            public void doAfterAllAnalysed(AnalysisContext analysisContext) {
                saveData();
                log.info("所有数据解析完成!");
            }

            private void saveData(){
                iUser.saveData(list);
            }
        }).sheet().doRead();
        return "success";
    }

查看结果:

这种实现方式,其实这只是将 listener 中的内容全部重写,并在 controller 中展现出来,当你看着这么臃肿的 controller 是不是非常难受?很显然这种方式不是我们的最佳编码实现

构造器传参

在之前分析 SpringBoot 统一返回源码时,不知道你是否发现,Spring 底层源码多数以构造器的形式传参,所以我们可以将为 listener 添加有参构造器,将 Controller 中依赖注入的 IUser 以构造器的形式传入到 listener :

@Slf4j
public class UserExcelListener extends AnalysisEventListener<User> {

    private IUser iUser;

    public UserExcelListener(IUser iUser){
        this.iUser = iUser;
    }

    // 省略相应代码...

    private void saveData(){
        iUser.saveData(list); //调用 userService 中的 saveData 方法
    }
    

更改 Controller 方法:

@PostMapping("/uploadWithConstructor")
public String uploadWithConstructor(MultipartFile file) throws IOException {
    EasyExcel.read(file.getInputStream(), User.class, new UserExcelListener(iUser)).sheet().doRead();
    return "success";
}

运行结果: 同上

这样更改后,controller 代码看着很清晰,但如果后续业务还有别的 Service 需要注入,我们难道要一直添加有参构造器吗?很明显,这种方式同样不是很灵活。

其实在使用匿名内部类的时候,你也许会想到,我们可以通过 Java8 lambda 的方式来解决这个问题

Lambda 传参

为了解决构造器传参的痛点,同时我们又希望 listener 更具有通用性,没必要为每个 Excel 业务都新建一个 listener,因为 listener 都是逐行读取 Excel 数据,只需要将我们的业务逻辑代码传入给 listener 即可,所以我们需用到 Consumer<T> ,将其作为构造 listener 的参数。

新建一个工具类 ExcelDemoUtils,用来构造 listener:

我们看到,getListener 方法接收一个 Consumer<List<T>> 的参数,这样下面代码被调用时,我们的业务逻辑也就会被相应的执行了:

consumer.accept(linkedList);

继续改造 Controller 方法:

运行结果: 同上

到这里,我们只需要将业务逻辑定制在 batchInsert 方法中:

  1. 满足 Controller RESTful API 的简洁性
  2. listener 更加通用和灵活,它更多是扮演了抽象类的角色,具体的逻辑交给抽象方法的实现来完成
  3. 业务逻辑可扩展性也更好,逻辑更加清晰

总结

到这里,关于如何使用 EasyExcel 读取 Excel 信息的基本使用方式已经介绍完了,还有很多细节内容没有讲,大家可以自行查阅 EasyExcel Github 文档去发现更多内容。灵活使用 Java 8 的函数式接口,更容易让你提高代码的复用性,同时看起来更简洁规范

除了读取 Excel 的读取,还有 Excel 的写入,如果需要将其写入到指定位置,配合 HuTool 的工具类 FileWriter 的使用是非常方便的,针对 EasyExcel 的使用,如果大家有什么问题,也欢迎到博客下方探讨

完整代码请在公众号回复「demo」,点开链接,查看「easy-excel-demo」文件夹的内容即可,另外个人博客由于特殊原因暂时关闭首页,其他目录访问一切正常,更多文章可以从 https://dayarch.top/archives 入口查看

感谢

非常感谢 EasyExcel 的作者 🌹🌹,让 Excel 的读写更加方便

灵魂追问

  1. 除了 Consumer,如果需要返回值的业务逻辑,需要用到哪个函数式接口呢?
  2. 当出现复杂表头的时候要如何处理呢?
  3. 将 DB 数据写入到 Excel 并下载,如何实现呢?
  4. 从 EasyExcel 的设计上,你学到了什么,欢迎博客下方留言讨论

提高效率工具

C#调用RabbitMQ实现消息队列 - kiba518 - 博客园

mikel阅读(932)

来源: C#调用RabbitMQ实现消息队列 – kiba518 – 博客园

前言

我在刚接触使用中间件的时候,发现,中间件的使用并不是最难的,反而是中间件的下载,安装,配置才是最难的。

所以,这篇文章我们从头开始学习RabbitMq,真正的从头开始。

关于消息队列

其实消息队列没有那么神秘,我们这样想一下,用户访问网站,最终是要将数据以HTTP的协议的方式,通过网络传输到主机的某个端口上的。

那么,接收数据的方式是什么呢?自然是端口监听啦。

那消息队列是什么就很好解释了?

它就是端口监听,接到数据后,将数据排列起来。

那这件事,我们不用中间件能做吗?

当然能做啦,写个TCP/UDP/Socket的软件就可以做啦。

举个简单的例子,如下图:

既然自己可以做消息队列,那为什么要用RabbitMQ?

因为,RabbitMQ成熟的开源中间件,可靠性有保证,bug少,性能也非常好。

C#代码默认是使用托管内存的,所以,想写出媲美RabbitMQ性能的消息队列,就必须离开我们常用的托管内存,使用非托管内存,但这个代价就太大了;而且最终能否达到RabbitMQ的性能水平还是个未知数。

还有就是RabbitMQ除了基础的消息队列管理,还有很多很强大的额外功能,而自己开发消息队列,很难如此尽善尽美。

—————————————————————————————————-

我们还会发现,在消息队列里有很多概念,什么消息总线啊,什么工作队列啊等等。

要怎么理解这些概念呢?

很简单,不要去理解。这些概念其实是人家代码架构的模式,不要去理解他们,【记】就完了,人家的中间件就是按照这个模式工作的。

比如,我写了一个接收消息的总控制器,然后我为他命名为总线,那这个控制器就是总线,没有理由,这就是定义。

准备工作

首先,我们访问官网【https://www.rabbitmq.com/】,点击Get Started。

然后,网站会自动跳转到当前首页Get Started的锚点位置,如下图:

Get Started锚点:

然后我们点击DownLoad+Installation,进入到下载界面。

在下载页面中,我们找到安装指南,然后在点击官网推荐的Windows系统的安装包,如下图:

现在,我们进入了Windows安装指南界面了。

首先,我们看一下预览信息,如下图:

在预览里,我们得知,安装RabbitMQ有两种方法,一种是使用Chocolatey安装,一种是使用官方安装包安装。

Chocolatey是什么呢?随手百度一下,原来他是一个软件包管理工具,也就是说,Chocolatey是类似于Nuget的一种工具。

由于Chocolatey的使用,我不是很熟悉,所以,这里选择使用官方安装包安装。

点击【Using the official installer】,我们进入了【Using the official installer】对应的锚点,如下图。

在【Using the official installer】段落里找到有推荐标志的安装包,然后下载。

下载完成后,我们可以得到这样一个安装包,如下图:

除了下载安装包,我们还会发现,在【Using the official installer】段落里,有提醒我们,RabbitMQ是有依赖的,依赖一个Erlang语言的框架(类似于C#语言的NetFramework)。

我们可以发现,在依赖的段落里,官网非常坑的给出了三个链接网址,如下:

supported version of Erlang:https://www.rabbitmq.com/which-erlang.html

Windows installer:https://www.erlang.org/downloads

Erlang Solutions:https://www.erlang-solutions.com/resources/download.html

因为,我们是无法通过文字描述来判断,哪一个是真的依赖框架的下载地址,所以只好每个都点击进去看看。。。

打开网址后发现,在后两个网址中都可以找到框架下载地址,但第二个地址明显更友好一点,所以我们在第二个网址内下载Erlang的框架。

下载完成得到如下图文件:

PS:这里下载的是OTP的22.1的版本,我的理解是Erlang等于C#语言,而OTP等于NetFramework。

安装Erlang\OTP

首先,我们运行otp_win64_22.1.exe,安装依赖框架Erlang\OTP。

安装完成后,设置环境变量如下:

然后运行CMD,输入erl,测试安装是否成功,如下图:

安装成功。

安装rabbitmq-server

安装完依赖后,我们接着安装rabbitmq-server-3.8.0.exe。

【rabbitmq-server-3.8.0.exe】?从这个文件名上,我们发现了一个问题,那就是,我们即将安装的RabbitMQ,是一个服务端啊。

什么?服务端?难道还有客户端???

其实这也很好理解,想一下最开始我举的那个例子,消息队列是需要一个监听端口的服务端的,然后客户端向这个服务端发送请求。

这样是不是就很好的理解RabbitMQ了呢:)

—————————————————————————————————-

安装完RabbitMQ服务端后,我们还是启动CMD,用命令行来查看下安装状态。

首先输入下面的命令,将路径定位到RabbitMQ的路径下:

【CD /D C:\Program Files\RabbitMQ Server\rabbitmq_server-3.8.0\sbin】

然后输入rabbitmqctl status查看状态。

启动管理工具的命令行:rabbitmq-plugins enable rabbitmq_management。

启动成功后,在浏览器输入地址http://127.0.0.1:15672/,进入管理页面,账户密码都是guest。

RabbitMQ还有很多常用命令,大家可以自行百度。

—————————————————————————————————-

到此,RabbitMQ服务端的环境配置好了,正常情况,这些配置应该在服务器进行,但我为了测试方便,就把服务端也安装在本机了,因此我下面调用RabbitMQ时,连接的主机IP都是localhost。

RabbitMQ应用

首先创建两个控制台应用程序,KibaRabbitMQSend和KibaRabbitMQReceived。

然后引入RabbitMQ的开源类库。

在C#里使用RabbitMQ开源类库非常简单,可以去官网下载一个.NET版本的RabbitMQ客户端类库,也可以直接在Nuget上搜索RabbitMQ,然后安装,如下图:

KibaRabbitMQSend

安装完RabbitMQ开源类库后,我们编写代码,实现向RabbitMQ服务器发送消息,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static void Main(string[] args)
{
    var factory = new ConnectionFactory();
    factory.HostName = "localhost";//主机名,Rabbit会拿这个IP生成一个endpoint,这个很熟悉吧,就是socket绑定的那个终结点。
    factory.UserName = "guest";//默认用户名,用户可以在服务端自定义创建,有相关命令行
    factory.Password = "guest";//默认密码
    using (var connection = factory.CreateConnection())//连接服务器,即正在创建终结点。
    {
        //创建一个通道,这个就是Rabbit自己定义的规则了,如果自己写消息队列,这个就可以开脑洞设计了
        //这里Rabbit的玩法就是一个通道channel下包含多个队列Queue
        using (var channel = connection.CreateModel())
        {
             channel.QueueDeclare("kibaQueue"falsefalsefalsenull);//创建一个名称为kibaqueue的消息队列
             var properties = channel.CreateBasicProperties();
             properties.DeliveryMode = 1;
             string message = "I am Kiba518"//传递的消息内容
             channel.BasicPublish("""kibaQueue", properties, Encoding.UTF8.GetBytes(message)); //生产消息
             Console.WriteLine($"Send:{message}");
        }
    }
}

运行代码。

然后我们使用命令行rabbitmqctl list_queues,去RabbitMQ的服务器查看当前消息队列,如下图:

可以看到,我们的消息已经发送成功了。

KibaRabbitMQReceived

现在我们编写接收消息代码,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
static void Main(string[] args)
{
    var factory = new ConnectionFactory();
    factory.HostName = "localhost";
    factory.UserName = "guest";
    factory.Password = "guest";
    using (var connection = factory.CreateConnection())
    {
        using (var channel = connection.CreateModel())
        {
            channel.QueueDeclare("kibaQueue"falsefalsefalsenull);
            /* 这里定义了一个消费者,用于消费服务器接受的消息
             * C#开发需要注意下这里,在一些非面向对象和面向对象比较差的语言中,是非常重视这种设计模式的。
             * 比如RabbitMQ使用了生产者与消费者模式,然后很多相关的使用文章都在拿这个生产者和消费者来表述。
             * 但是,在C#里,生产者与消费者对我们而言,根本算不上一种设计模式,他就是一种最基础的代码编写规则。
             * 所以,大家不要复杂的名词吓到,其实,并没那么复杂。
             * 这里,其实就是定义一个EventingBasicConsumer类型的对象,然后该对象有个Received事件,
             * 该事件会在服务接收到数据时触发。
             */
            var consumer = new EventingBasicConsumer(channel);//消费者
            channel.BasicConsume("kibaQueue"true, consumer);//消费消息
            consumer.Received += (model, ea) =>
            {
                var body = ea.Body;
                var message = Encoding.UTF8.GetString(body);
            };
        }
    }
}

运行代码。

然后我们使用命令行rabbitmqctl list_queues,去RabbitMQ的服务器查看当前消息队列,如下图:

可以看到,消息已经被使用了。

—————————————————————————————————-

现在我们在发送代码出做一个for循环,看看消息接收速度是什么样的,代码如下,for循环了100次,每次间隔3秒。

1
2
3
4
5
6
7
8
9
10
for (int i = 0; i < 100; i++)
{
    channel.QueueDeclare("kibaQueue"falsefalsefalsenull);//创建一个名称为kibaQueue的消息队列
    var properties = channel.CreateBasicProperties();
    properties.DeliveryMode = 1;
    string message = "I am Kiba518"//传递的消息内容
    channel.BasicPublish("""kibaQueue", properties, Encoding.UTF8.GetBytes(message)); //生产消息
    Console.WriteLine($"Send:{message}");
    Thread.Sleep(3000);
}

效果图如下:

可以看到,发送消息和接收消息,几乎是同步的,效果非常理想。

服务器端应用

在上文,我们的RabbitMQ服务是安装在我的本机上的;现在我们把服务移植到服务器上,然后再来测试一下。

在服务器端安装RabbitMQ和在本机安装的步骤是一样的,但是安装完成后,我们需要设置下防火墙的入站规则和出站规则,将5672的UDP端口开放一下。

为什么要开放端口是5672?因为RabbitMQ的默认的消息接收和发送端口就是5672,我们可以使用断点查看一下。

如上图,可以看到,在我们没有设置端口的时候,Endpoint的端口的默认值是5672。

配置完端口后,我们修改代码中的HostName为我们的服务器地址,如下。

1
factory.HostName = "1.1.1.1";

重新运行代码,会发现在运行到factory.CreateConnection()的时候,系统提示一个异常【RabbitMQ.Client.Exceptions.BrokerUnreachableException:“None of the specified endpoints were reachable”】,如下图:

这是因为我们使用的账号是guest,guest账号默认是不支持远程连接的。

解决办法很简单,新建一个账户即可。

创建用户

在服务器端打开浏览器,输入http://127.0.0.1:15672/,进入管理页面。

点击菜单栏的Admin选项,进入用户管理界面创建用户kiba,密码123456,如下图:

创建完用户后,得到如下界面。

如上图所示,刚刚创建的用户还没有任何访问权限。

现在我们点击用户名,进入权限管理页面设置权限。

如上图所示,页面默认为我们设置了一个可读,可写,可管理配置的权限;所以,我们只要点击Set premission就可以了。

设置完权限,我们回到用户管理页面。

如上图所示,权限设置成功。

现在我们回到代码,修改用户名密码如下。

1
2
3
factory.HostName = "1.1.1.1";
factory.UserName = "kiba";
factory.Password = "123456";

运行代码,不再抛异常,接受发送消息正常。

—————————————————————————————————-

设置用户权限也可以通过命令的方式设置,如下:

rabbitmqctl set_permissions -p “/” kiba “.” “.” “.*”

—————————————————————————————————-

到此C#调用RabbitMQ实现消息队列就讲完了。

代码已经传到Github上了,欢迎大家下载。

Github地址:https://github.com/kiba518/KibaRabbitMQ

—————————————————————————————————-

注:此文章为原创,任何形式的转载都请联系作者获得授权并注明出处!
若您觉得这篇文章还不错,请点击下方的【推荐】,非常感谢!

https://www.cnblogs.com/kiba/p/11703073.html

asp.net-mvc – Chrome 27中SignalR和跨域请求的问题 - 脚本之家

mikel阅读(984)

来源: asp.net-mvc – Chrome 27中SignalR和跨域请求的问题 – 脚本之家

编辑:这显然只是Chrome中的一个问题,它在FF和IE中运行良好Chrome版本:27.0.1453.116

我在localhost:13371上有一个简单的JS / HTML站点,我正在尝试访问localhost:13371上的SignalR中心.

无论何时发出请求,我都会收到以下错误:

XMLHttpRequest cannot load http://localhost:13370/signalr/hubs/negotiate?_=1372338722032. Origin http://localhost:13371 is not allowed by Access-Control-Allow-Origin.

我已经尝试过了:

>在Application_Start上启用SignalR上的跨域:

  1. RouteTable.Routes.MapHubs(new HubConfiguration { EnableCrossDomain = true });

>在SignalR服务器的Web.Config中启用跨域:

  1. <system.webServer>
  2. <httpProtocol>
  3. <customHeaders>
  4. <clear />
  5. <add name=“Access-Control-Allow-Origin” value=“*” />
  6. <add name=“Access-Control-Allow-Methods” value=“*” />
  7. <add name=“Access-Control-Allow-Headers” value=“*” />
  8. </customHeaders>
  9. </httpProtocol>
  10. </system.webServer>

>在JavaScript中设置信号器中心的连接URL:

  1. $.connection.hub.url = ‘http://localhost:13370/signalr/hubs’;

>在JQuery中启用CORS:

  1. $.support.cors = true;

>在Application_BeginRequest中手动设置响应头:

  1. protected void Application_BeginRequest(object sender,EventArgs e)
  2. {
  3. HttpContext.Current.Response.AddHeader(“Access-Control-Allow-Origin”,“*”);
  4.  
  5. if (HttpContext.Current.Request.HttpMethod == “OPTIONS”)
  6. {
  7. //These headers are handling the “pre-flight” OPTIONS call sent by the browser
  8. HttpContext.Current.Response.AddHeader(“Access-Control-Allow-Methods”,“GET,POST,PUT,DELETE”);
  9. HttpContext.Current.Response.AddHeader(“Access-Control-Allow-Headers”,“*”);
  10. HttpContext.Current.Response.AddHeader(“Access-Control-Max-Age”,“1728000”);
  11. HttpContext.Current.Response.End();
  12. }
  13. }

以上都没有,也没有上述任何组合起作用.

附加信息:

>这两个站点都在Windows 7上的IIS中运行
> SignalR站点在端口13370上运行,作为.NET 4.0下MVC 4站点的一部分.
> JS / HTML站点是端口13371上的简单Web服务器,根本没有托管代码.
> SignalR站点肯定会响应浏览器对相关资源的直接请求.
> SignalR版本是1.1.2

解决方法

编辑:我现在找到了两个解决方案……方法1.摆脱你可能不需要的垃圾:

正如我在Stack Overflow question中发现的那样,基本上我在上面列出的“我尝试过的东西”中添加的几乎所有内容都是不必要的.修复步骤:

>删除我在上面尝试过的所有内容.这意味着在Web.Config或其他地方(如Global.asax)没有指定自定义标头,没有自定义JQuery设置等.
> ..除了RouteTable.Routes.MapHubs(new HubConfiguration {EnableCrossDomain = true});在Application_Start中.
>你还需要设置$.connection.hub.url =’http:// localhost:13370 / signalsr / hubs’;

… 而已.这可能是最好的解决方案,也是我最终使用的解决方案.

方法2.如果您在Chrome中仍然遇到问题,请使用jsonp:

如果你在Chrome中仍然遇到这样的问题,可以使用jsonp让协商脚本正确下载…将以下内容添加到我的JavaScript中心开始解决问题:

  1. //detect chrome
  2. var isChrome = navigator.userAgent.toLowerCase().indexOf(‘chrome’) > 1;
  3.  
  4. //set the connection url.
  5. $.connection.hub.url = ‘http://localhost:13370/signalr/hubs’;
  6.  
  7. //use jsonp if chrome
  8. $.connection.hub.start({
  9. jsonp: isChrome
  10. });

可能有一种更好的方法来测试浏览器功能并相应地设置jsonp ……看着用户代理感觉很脏……但是这解决了我在过渡时期的问题.我希望这有助于其他人.

总结

以上是脚本之家为你收集整理的asp.net-mvc – Chrome 27中SignalR和跨域请求的问题全部内容,希望文章能够帮你解决asp.net-mvc – Chrome 27中SignalR和跨域请求的问题所遇到的程序开发问题。

SignalR 和跨域问题 (ASP.NET MVC) - chen.s - 博客园

mikel阅读(1192)

来源: SignalR 和跨域问题 (ASP.NET MVC) – chen.s – 博客园

SignalR 有三块,

Server:
Hub Server
Custom Message Sender Service (Sender Proxy)

Client:
Message Sender
Message Receiver

1. Hub Server & Sender Proxy Service
Hub Server 和 Custom Message Sender Service 在一个app中,“Custom Message Sender Service” 通过api直接控制Hub Server行为。(connection filtering, status, deliver message, etc )

2. 跨域
Sender, Receiver 和 Hub Server 可能都不在同一个域里。 会有跨域访问的问题。

3. 发消息的客户端
可以有两种方式发消息给Hub Server
3.1 通过hubproxy.server.func(), 走的是signalR的persistent connection. (这种情形下一般只发一次消息,persistent connection占用了资源,浪费。 当然也可以建立完释放,persistent connection -> send message -> close persistent connection)。 这种模式下,跨域由SignalR Hub框架控制。
3.2 通过ajax调用 “Custom Message Sender Service (Proxy)”, 由Proxy调用Hub的服务。 这种结构更干净,发送者和SignalR connection没有关系, 走单独Http请求。 跨域由 “Custom Message Sender Service (Proxy)”, 一般由服务端暴露服务(MVC/WebAPI). 但是MVC和WebApi的跨域管理不太一样。(见“MVC和WebApi的跨域管理”)
3.3 有一种情况可以考虑用signalR的persistent connection发消息。 就是发消息的人,同时又是收消息的人。 那么一个persistent connection可以同时用来收发消息。

4. 收消息的客户端
这个只有一种,“持久连接,保持监听”。
4.1 SignalR保持连接的方式由协议本身定义,由服务器和浏览器支持。 WebSocket / iFrame / longPooling。 具体可google
4.2 WebSocket的效率要比其他高很多,比较reliable,比如disconnect, 其他方式可能不能直接反应客户端关闭的动作,需要等待超时发生触发disconnect. 代码和排错上有时比较费解。
4.3 【Hub Server服务端】微软的WebApp PaaS上, by default, “WebSocket”不enable。要去管理界面把它打开。

5. Hub Server
5.1 Owin中map /signalr 作为SignalR routing的开始
(https://docs.microsoft.com/en-us/aspnet/signalr/overview/guide-to-the-api/hubs-api-guide-server#tracing If you are adding SignalR functionality to an ASP.NET MVC application, make sure that the SignalR route is added before the other routes. For more information, see Tutorial: Getting Started with SignalR 2 and MVC 5.)
5.2 跨域访问由Owin框架自己处理, 如果再由IIS web.config来控制CORS, 会产生冲突。
5.3 Hub Server中的方法,只是个签名,具体实现不重要(无需写实现,除非客户端用hub.server.func()发消息)。 主要作用
5.3.1 生成客户端代理类。
5.3.2 让代理类绑定本地方法,以被服务端调用

 6. Sender Message Proxy
6.1 如果用MVC做,有个很大的问题就是,跨域。
6.1.1 如果浏览器是IE, 问题不是很大。 貌似preflight request 被省略了。 request 被直接提交到MVC层, 可以自定义Action属性, 加入”Access-Control-Allow-Origin“。
6.1.2 如果浏览器是Chrome, “preflight request”不会被忽略. http option verb会发过来。 这个时候MVC就有问题了, 无论从owin还是application_beginRequest,都无法获得这个request,无法修改http头,使得CORS通过。 (MVC 不能intercept http option request,而webapi能,webapi 是不是因为引用了system.web.http.webhost 才能intercept?)
6.1.3 对于simple CORS, IE 和 Chrome (Chrome 无须preflight), 如果用Get/Post传数据,由于不走preflight, 自定义Action属性修改http header是可行的。 但是如果用ajax, 有content type header的话, 那就必须要走preflight.

7. IIS setting, web.config
实际使用当中,如果使用MVC 和 SignalR, 客户端用ajax post json (content type = application / json), 那么需要考虑直接使用直接从服务器设置应用CORS。 这个时候SignalR的CORS需要被取消(//map.UseCors(CorsOptions.AllowAll) )

<add name=”Access-Control-Allow-Origin” value=”https://localhost:44300″ />
<add name=”Access-Control-Allow-Headers” value=”Content-Type” />
<add name=”Access-Control-Allow-Credentials” value=”true” />

 

CORS 介绍

一篇很好的文章介绍CORS:https://staticapps.org/articles/cross-domain-requests-with-cors/

注意Simple CORS / Complex CORS 的区别

备注
尽量少用IE来做测试,用Chrome或firefox比较标准,会得到和预期一致的结果。 IE的结果经常莫名其妙,比如SignalR不enable CORS (map.UseCors(CorsOptions.AllowAll);)在localhost下,跨域的连接居然也能成功。

 

常用代码

var heartBeat GlobalHost.DependencyResolver.Resolve<ITransportHeartbeat>();
var connectionAlive = heartBeat.GetConnections().FirstOrDefault(c=>c.ConnectionId == connection.ConnectionId);
if (connectionAlive.IsAlive)
{//Do whatever...}

======================================================

IHubContext context = GlobalHost.ConnectionManager.GetHubContext<MyHub>();
IHubContext context = GlobalHost.ConnectionManager.GetHubContext(“MyHub”);

======================================================

public class EnableCorsAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
filterContext.RequestContext.HttpContext.Response.AddHeader(“Access-Control-Allow-Origin”, “*”);
base.OnActionExecuting(filterContext);
}
}

asp.net core 使用SignalR跨域请求出现的坑 - a914541185的博客 - CSDN博客

mikel阅读(1846)

来源: asp.net core 使用SignalR跨域请求出现的坑 – a914541185的博客 – CSDN博客

前段时间因为工作需要,认识到了SignalR,这个东西本身挺好用,但是在处理跨域问题上是遭遇了很大的坑。

我在本地通过localhost连接的时候毫无问题,但是当我在前端使用IP来连接我的后台,则会出现连接失败的问题。查阅了很多,询问公司内的大牛之后,找到一个正确解决方案,特记录如下:

首先,在Strartup.cs文件下处理跨域问题

public const string CorsName = “SignalRCors”;

// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});

services.AddSignalR();
services.AddCors(options =>
{
options.AddPolicy(CorsName,
policy => policy.AllowAnyOrigin()
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials());
});
services.AddMvc((opt) =>
{
opt.Filters.Add(new CorsAuthorizationFilterFactory(CorsName));
}).SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
//services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler(“/Error”);
app.UseHsts();
}
app.UseCors(CorsName);
之后右键你的项目进入属性-调试,记下你的http端口号以及htts的端口号

然后找到applicationhost.config文件,一般路径是项目文件夹\.vs\config\applicationhost.config。找到如下代码

<sites>
<site name=”WebSite1″ id=”1″ serverAutoStart=”true”>
<application path=”/”>
<virtualDirectory path=”/” physicalPath=”%IIS_SITES_HOME%\WebSite1″ />
</application>
<bindings>
<binding protocol=”http” bindingInformation=”:8080:localhost” />
</bindings>
</site>
<site name=”signalrTotest” id=”2″>
<application path=”/” applicationPool=”Clr4IntegratedAppPool”>
<virtualDirectory path=”/” physicalPath=”C:\Users\JC.YX\Desktop\signalrTotest\signalrTotest” />
</application>
<bindings>
<binding protocol=”https” bindingInformation=”:44331:localhost” />
<binding protocol=”http” bindingInformation=”:35000:localhost” />
</bindings>
</site>
可以看到有https和http的端口设置,以及允许localhost连接。这时我们只需要在它们下面添加自己的IP地址就可以了。添加之后的代码如下

<sites>
<site name=”WebSite1″ id=”1″ serverAutoStart=”true”>
<application path=”/”>
<virtualDirectory path=”/” physicalPath=”%IIS_SITES_HOME%\WebSite1″ />
</application>
<bindings>
<binding protocol=”http” bindingInformation=”:8080:localhost” />
</bindings>
</site>
<site name=”signalrTotest” id=”2″>
<application path=”/” applicationPool=”Clr4IntegratedAppPool”>
<virtualDirectory path=”/” physicalPath=”C:\Users\JC.YX\Desktop\signalrTotest\signalrTotest” />
</application>
<bindings>
<binding protocol=”https” bindingInformation=”:44331:localhost” />
<binding protocol=”https” bindingInformation=”:44331:192.168.0.104″ />
<binding protocol=”http” bindingInformation=”:35000:localhost” />
<binding protocol=”http” bindingInformation=”:35000:192.168.0.104″ />
</bindings>
</site>
这时,将你的前端代码修改为对应IP地址加端口号,启动IIS服务,你可以尽情的和朋友在聊天室里聊天了

var connection = new signalR.HubConnectionBuilder().withUrl(“http://yourIP:35000/chatHub”).build();
注意:如果你前端写的是http协议,请在启动IIS服务之后,将默认打开的网址修改为http协议加对应的端口号。
————————————————
版权声明:本文为CSDN博主「是豆腐啊」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/a914541185/article/details/83181919

CefSharp High DPI问题的解决

mikel阅读(2094)

来源: CefSharp High DPI问题的解决

使用CefSharp控件,在部分高分辨率的电脑中(显示缩放比例非100%,而是120%或者125%等)会出现以下一些情况:

技术分享图片

技术分享图片

显示的页面中出现了黑边,且按钮定位也偏了,比如点击【图层】按钮,需要点击上面黑色边框中的相对位置。

 

解决方案:

1、在当前电脑的桌面,右键-显示设置,把显示比例调整为100%(需要重启电脑生效),这时再看,显示就正常了;

2、在当前项目中,添加一个【应用程序清单文件】(app.manifest),在根节点(assembly)下,添加以下代码,重新运行程序,则和方法1的效果就一样了:

  <application xmlns="urn:schemas-microsoft-com:asm.v3">
    <windowsSettings>
      <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
    </windowsSettings>
  </application>

3、使用CefSharp自带的方法:

找到Cef.Initialize(setting);语句,在其下添加Cef.EnableHighDPISupport();

当程序加载了ChromiumWebBrowser控件后,就会把当前的显示比例调整为100%了

            Cef.Initialize(setting);
            Cef.EnableHighDPISupport();

 

优先选择 【方法3】

使用cefsharp 浏览器放大-布布扣-bubuko.com

mikel阅读(709)

来源: 使用cefsharp 浏览器放大-布布扣-bubuko.com

(1)如果浏览器位置有问题,需要设置 Cef.EnableHighDPISupport();

(2)如果要放大浏览器,需要设置  browser.SetZoomLevel(1.25);

        ChromiumWebBrowser webbrowser = new ChromiumWebBrowser(""); 
        private void Form1_Load(object sender, EventArgs e)
        {


            Cef.EnableHighDPISupport();

            this.FormBorderStyle = FormBorderStyle.None;
            this.WindowState = FormWindowState.Maximized;



            string url = System.Configuration.ConfigurationManager.AppSettings["url"];
            webbrowser = new ChromiumWebBrowser(url);

            // Add it to the form and fill it to the form window.
           
            
            webbrowser.Dock = DockStyle.Fill;
            webbrowser.FrameLoadEnd += webbrowser_FrameLoadEnd;
            this.Controls.Add(webbrowser);

void webbrowser_FrameLoadEnd(object sender, FrameLoadEndEventArgs e)         {

ChromiumWebBrowser browser = (ChromiumWebBrowser)sender;

browser.SetZoomLevel(1.25);

}