jquery操作select(取值,设置选中) - 海乐学习 - 博客园

mikel阅读(1078)

来源: jquery操作select(取值,设置选中) – 海乐学习 – 博客园

JQuery操作select(增加,删除,清空)

复制代码
1. $("#select_id").change(function(){//code...}); //为Select添加事件,当选择其中一项时触发
 
2. var checkText=$("#select_id").find("option:selected").text(); //获取Select选择的
 
3. var checkValue=$("#select_id").val(); //获取Select选择的Value
 
4. var checkIndex=$("#select_id ").get(0).selectedIndex; //获取Select选择的索引值
 
5. var maxIndex=$("#select_id option:last").attr("index"); //获取Select最大的索引值
 
jQuery添加/删除Select的Option项:
 
1. $("#select_id").append("<option value='Value'>Text</option>"); //为Select追加一个Option(下拉项)
 
2. $("#select_id").prepend("<option value='0'>请选择</option>"); //为Select插入一个Option(第一个位置)
 
3. $("#select_id option:last").remove(); //删除Select中索引值最大Option(最后一个)
 
4. $("#select_id option[index='0']").remove(); //删除Select中索引值为0的Option(第一个)
 
5. $("#select_id option[value='3']").remove(); //删除Select中Value='3'的Optiona
 
5. $("#select_id option").remove(); //删除Select中Text='4'的Optiona
 
内容清空:$("#charCity").empty();
复制代码

 

JQuery获取Select选择的Text和Value: 

复制代码
$("#select_id").change(function(){//code...}); //为Select添加事件,当选择其中一项时触发
 
var checkText=$("#select_id").find("option:selected").text(); //获取Select选择的text
 
var checkValue=$("#select_id").val(); //获取Select选择的Value
 
var checkIndex=$("#select_id ").get(0).selectedIndex; //获取Select选择的索引值
 
var maxIndex=$("#select_id option:last").attr("index"); //获取Select最大的索引值
复制代码

 

比如<select class=”selector”></select>

1、设置value为pxx的项选中

     $(".selector").val("pxx");

2、设置text为pxx的项选中

    $(".selector").find("option").attr("selected",true);

这里有一个中括号的用法,中括号里的等号的前面是属性名称,不用加引号。很多时候,中括号的运用可以使得逻辑变得很简单。

3、获取当前选中项的value

    $(".selector").val();

4、获取当前选中项的text

    $(".selector").find("option:selected").text();

这里用到了冒号,掌握它的用法并举一反三也会让代码变得简洁。

 

很多时候用到select的级联,即第二个select的值随着第一个select选中的值变化。这在JQuery中是非常简单的。

如:

复制代码
$(".selector1").change(function(){
     // 先清空第二个
      $(".selector2").empty();
     // 实际的应用中,这里的option一般都是用循环生成多个了
      var option = $("<option>").val(1).text("pxx");
      $(".selector2").append(option);
});
复制代码

jQuery获取Select选择的Text和Value:
语法解释:

$("#select_id").change(function(){//code...});   //为Select添加事件,当选择其中一项时触发
var checkText=$("#select_id").find("option:selected").text();  //获取Select选择的Text
var checkValue=$("#select_id").val();  //获取Select选择的Value
var checkIndex=$("#select_id ").get(0).selectedIndex;  //获取Select选择的索引值
var maxIndex=$("#select_id option:last").attr("index");  //获取Select最大的索引值

jQuery设置Select选择的 Text和Value:
语法解释:

$("#select_id ").get(0).selectedIndex=1;  //设置Select索引值为1的项选中
$("#select_id ").val(4);   // 设置Select的Value值为4的项选中
$("#select_id option").attr("selected", true);   //设置Select的Text值为jQuery的项选中

 

jQuery添加/删除Select的Option项:
语法解释:

$("#select_id").append("<option value='Value'>Text</option>");  //为Select追加一个Option(下拉项)
$("#select_id").prepend("<option value='0'>请选择</option>");  //为Select插入一个Option(第一个位置)
$("#select_id option:last").remove();  //删除Select中索引值最大Option(最后一个)
$("#select_id option[index='0']").remove();  //删除Select中索引值为0的Option(第一个)
$("#select_id option[value='3']").remove();  //删除Select中Value='3'的Option
$("#select_id option").remove();  //删除Select中Text='4'的Option

 

jquery radio取值,checkbox取值,select取值,radio选中,checkbox选中,select选中,及其相关
获 取一组radio被选中项的值
var item = $(‘input[name=items][checked]’).val();
获 取select被选中项的文本
var item = $(“select[name=items] option[selected]”).text();
select下拉框的第二个元素为当前选中值
$(‘#select_id’)[0].selectedIndex = 1;
radio单选组的第二个元素为当前选中值
$(‘input[name=items]’).get(1).checked = true;
获取值:
文本框,文本区域:$(“#txt”).attr(“value”);
多选框 checkbox:$(“#checkbox_id”).attr(“value”);
单选组radio:   $(“input[type=radio][checked]”).val();
下拉框select: $(‘#sel’).val();
控制表单元素:
文本框,文本区域:$(“#txt”).attr(“value”,”);//清空内容
$(“#txt”).attr(“value”,’11’);//填充内容
多选框checkbox: $(“#chk1”).attr(“checked”,”);//不打勾
$(“#chk2”).attr(“checked”,true);//打勾
if($(“#chk1”).attr(‘checked’)==undefined) //判断是否已经打勾
单选组 radio:    $(“input[type=radio]”).attr(“checked”,’2′);//设置value=2的项目为当前选中项
下拉框 select:   $(“#sel”).attr(“value”,’-sel3′);//设置value=-sel3的项目为当前选中项
$(“<option value=’1′>1111</option><option value=’2′>2222</option>”).appendTo(“#sel”)//添加下拉框的option
$(“#sel”).empty();//清空下拉框

js判断时间是否为今天日期(判断日期与当前日期相差多少天) - 邱小健 - 博客园

mikel阅读(1440)

来源: js判断时间是否为今天日期(判断日期与当前日期相差多少天) – 邱小健 – 博客园

下面是我的代码:

// 判断时间为今天
judgeTime(data){
    var date = data.toString();
    var year = date.substring(0, 4);
    var month = date.substring(4, 6);
    var day = date.substring(6, 8);
    var d1 = new Date(year + '/' + month + '/' + day);
    var dd = new Date();
    var y = dd.getFullYear();
    var m = dd.getMonth() + 1;
    var d = dd.getDate();
    var d2 = new Date(y + '/' + m + '/' + d);
    var iday = parseInt(d2 - d1) / 1000 / 60 / 60 / 24;
    return iday;
  },

var start_time="2018-07-31T14:00"   //这是一般的时间格式,下面是使用replace方法进行转换为数字格
//式

if(judgeTime(start_time.replace(/-|T|:/g, ''))==0){
    console.log('日期为今天')
}

如果数字为负数,-1为明天   负多少就是差多少    正数相反的道理

在Linux上部署.NetCore项目 · Microbubu's Lab

mikel阅读(1127)

来源: 在Linux上部署.NetCore项目 · Microbubu’s Lab

Linux管理工具宝塔面板的使用

安装

具体方法可以参考宝塔官网,下面仅总结一下要点:

  1. yum安装宝塔面板(6.8版本):
1
yum install -y wget && wget -O install.sh http://download.bt.cn/install/install_6.0.sh && bash install.sh
  1. 升级宝塔面板:
1
yum install -y wget && wget -O install.sh http://download.bt.cn/install/install_6.0.sh && bash install.sh

使用

默认端口是8888,登陆之前可以通过终端更改面板的用户名和密码:

bt后端设置

之后便可以使用浏览器直接访问面板了,建议登录之后首先更改默认端口以及登录入口。

宝塔面板提供很多功能,包括服务器的监控、网站的一键搭建、FTP服务器的一键搭建、数据库及文件管理,另外还可以运行计划任务,也可以通过面板管理部分软件及运行环境,比如可以安装apache服务器软件、ngnix服务器软件以及php运行环境等,是一款很不错的Linux管理面板,并且该软件还有Windows Server版本。

.NetCore SDK的安装

由于宝塔面板中没有集成.NetCore SDK,所以我们只能通过yum在终端手动安装。

各个不同Linux发行版的安装方法大同小异,在这个官方页面均有详细介绍,这里我用的是CentOS,所以使用yum安装。

  1. 根据官方的说法,在安装DotNetCore环境之前需要注册Microsoft Key以及Product Repository,并且还需要安装一些依赖,这一步每台机器只需要运行一次,具体命令如下:
1
sudo rpm -Uvh https://packages.microsoft.com/config/rhel/7/packages-microsoft-prod.rpm
  1. 安装dotnet-sdk-2.2:
1
2
3
~$ sudo yum update

~$ sudo yum install dotnet-sdk-2.2
  1. 测试是否安装成功:
1
2
3
4
5
// 直接看版本:
~$ dotnet --version

// 使用yum查看info
~$ yum info dotnet-sdk-2.2

本地新建.NetCore WebApi项目

虽然在服务器上安装了.NetCore SDK,也可以用命令行直接在服务器上新建项目,但是鉴于Visual Studio强大的功能,我们选择在本地构建应用,然后部署到服务器即可。下面就编写一个简易的WebApi,注意新建项目的时候不要选择“为HTTPS配置”,因为服务器上的ngnix并没有启用https。

新建一个UsersController,代码编写如下:

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
[Route("api/[controller]")]
[ApiController]
public class UsersController : ControllerBase
{
    [HttpGet]
    public ActionResult<IEnumerable<SysUser>> Get()
    {
        return SysUser.Users;
    }
}

public class SysUser
{
    public int Id { get; set; }

    public string Name { get; set; }

    public string Password { get; set; }

    public string Description { get; set; }

    public static List<SysUser> Users = new List<SysUser>()
    {
        new SysUser()
        {
            Id=1,
            Name="张三",
            Password="Not Set",
            Description="我是张三"
        },
        new SysUser()
        {
            Id=2,
            Name="李四",
            Password="Not Set",
            Description="我是李四"
        }
    };
}

正如你所见,我们的WebApi只能使用Http的Get方法获取地址api/users的数据,后台会返回一个SysUser数组,这是我们预期的结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[
    {
        "id":1,
        "name":"张三",
        "password":"Not Set",
        "description":"我是张三"
    },
    {
        "id":2,
        "name":"李四",
        "password":"Not Set",
        "description":"我是李四"
    }
]

接着先将服务部署到本地文件系统上面,之后打成压缩包然后通过宝塔面板上传至服务器并解压。

在Linux上面运行项目

在服务器上进入部署文件所在目录,然后使用dotnet命令启动服务:

启动.NetCore应用

但是现在我们还不能直接访问到我们新部署的WebApi服务,需要使用ngnix设置反向代理,将特定的端口代理到http://localhost:5000,这一步可以通过宝塔面板来完成,步骤如下:

  1. 在宝塔面板上新建一个网站,设置为静态网站即可,如果当前宝塔面板已经有多个项目,注意设置好端口即可,这里我设置成了81端口。
  2. 在刚才新建的网站中设置反向代理,目标URL填写http://localhost:5000即可,发送域名留空。

现在可以直接在浏览器中使用url访问刚才部署的url服务,如下图:

查看结果

但是现在还有个问题,就是当我们关闭xShell等ssh工具的时候服务进程也会停止运行,下面就说说解决方法。

解决关闭Shell之后进程停止的问题

这个问题可以通过yum安装screen解决:

1
~$ yum install screen

screen的原理大概是新建一个终端进程,然后其他运行的任务可以附加到新建的这个进程运行,这样退出shell的时候我们的webapi服务便不会终止运行了。

下面是几个常用的screen命令:

  1. 新建一个screen(这里取名为box):
1
~$ screen -S box
  1. 查看当前存在的screen列表(这个命令会输出存在的所有screen实例的进程pid以及名称):
1
~$ screen -ls
  1. 关闭一个screen(关闭其对应的进程即可,以pid=1086为例):
1
~$ kill 1086

NetCore EasyNetQ 高级使用 RabbitMq_坚定的走-CSDN博客

mikel阅读(1496)

来源: NetCore EasyNetQ 高级使用 RabbitMq_坚定的走-CSDN博客

一、消息队列

消息队列作为分布式系统中的重要组件,常用的有MSMQ,RabbitMq,Kafa,ActiveMQ,RocketMQ。至于各种消息队列的优缺点比较,在这里就不做扩展了,网上资源很多。

更多内容可参考 消息队列及常见消息队列介绍。我在这里选用的是RabbitMq。

官网地址:http://www.rabbitmq.com

安装和配置:Windows下RabbitMq安装及配置

二、RabbitMq简单介绍

RabbitMQ是一款基于AMQP(高级消息队列协议),由Erlang开发的开源消息队列组件。是一款优秀的消息队列组件,他由两部分组成:服务端和客户端,客户端支持多种语言的驱动,如:.Net、JAVA、   Erlang等。在RabbitMq中首先要弄清楚的概念是 交换机、队列、绑定。基本的消息通讯步骤就是首先定义ExChange,然后定义队列,然后绑定交换机和队列。

需要明确的一点儿是,发布者在发送消息是,并不是把消息直接发送到队列中,而是发送到Exchang,然后由交互机根据定义的消息匹配规则,在将消息发送到队列中。

Exchange有四种消息消息分发规则:direct,topic,fanout,header。headers 匹配 AMQP 消息的 header 而不是路由键,此外 headers 交换器和 direct 交换器完全一致,但性能差很多,目前几乎用不到了。

详细的概念介绍推荐查看:消息队列之RabbitMq

三、EasyNetQ使用

Easynetq是一个简单易用的Rabbitmq Net客户端。同时支持 NetFramework和NetCore。GitHub地址。它是针对RabbitMq Net客户端的进一步封装。关于EasyNetQ的简单使用推荐教程:EasyNetQ的介绍

本文主要介绍基于EasyNeq的高级API的使用。EasyNetQ的作者在核心的IBus接口中尽量避免暴露AMQP中的交换机、队列、绑定这些概念,使用者即使不去了解这些概念,也能完成消息的发送接收。这相当简洁,但某些情况下,基于应用场景的需要,我们需要自定义交换机、队列、绑定这些信息,EasyNetQ允许你这么做,这些都是通过IAdvanceBus接口实现。

3.1 项目装备

这里为了演示,首先新建一个项目,包括一个发布者,两个接收者,一个公共的类库

安装EasyNetQ: NuGet>Install-Package EasyNetQ

3.2 简单封装

在Common项目里面是针对Easynetq的使用封装,主要目录如下

 

在RabbitMq文件夹下,是针对消息发送接收的简单封装。

首先来看下RabbitMqManage,主要的发送和订阅操作都在这个类中。其中ISend接口定义了发送消息的规范,SendMessageManage是ISend的实现。IMessageConsume接口定义订阅规范。

MesArg 和PushMsg分别是订阅和发送需用到的参数类。RabbitMQManage是暴露在外的操作类。

首先看发送的代码

  1. public enum SendEnum
  2. {
  3. 订阅模式 = 1,
  4. 推送模式 = 2,
  5. 主题路由模式 = 3
  6. }
  7. public class PushMsg
  8. {
  9. /// <summary>
  10. /// 发送的数据
  11. /// </summary>
  12. public object sendMsg { get; set; }
  13. /// <summary>
  14. /// 消息推送的模式
  15. /// 现在支持:订阅模式,推送模式,主题路由模式
  16. /// </summary>
  17. public SendEnum sendEnum { get; set; }
  18. /// <summary>
  19. /// 管道名称
  20. /// </summary>
  21. public string exchangeName { get; set; }
  22. /// <summary>
  23. /// 路由名称
  24. /// </summary>
  25. public string routeName { get; set; }
  26. }
  27. internal interface ISend
  28. {
  29. Task SendMsgAsync(PushMsg pushMsg, IBus bus);
  30. void SendMsg(PushMsg pushMsg, IBus bus);
  31. }
  32. internal class SendMessageMange : ISend
  33. {
  34. public async Task SendMsgAsync(PushMsg pushMsg, IBus bus)
  35. {
  36. //一对一推送
  37. var message = new Message<object>(pushMsg.sendMsg);
  38. IExchange ex = null;
  39. //判断推送模式
  40. if (pushMsg.sendEnum == SendEnum.推送模式)
  41. {
  42. ex = bus.Advanced.ExchangeDeclare(pushMsg.exchangeName, ExchangeType.Direct);
  43. }
  44. if (pushMsg.sendEnum == SendEnum.订阅模式)
  45. {
  46. //广播订阅模式
  47. ex = bus.Advanced.ExchangeDeclare(pushMsg.exchangeName, ExchangeType.Fanout);
  48. }
  49. if (pushMsg.sendEnum == SendEnum.主题路由模式)
  50. {
  51. //主题路由模式
  52. ex = bus.Advanced.ExchangeDeclare(pushMsg.exchangeName, ExchangeType.Topic);
  53. }
  54. await bus.Advanced.PublishAsync(ex, pushMsg.routeName.ToSafeString(“”), false, message)
  55. .ContinueWith(task =>
  56. {
  57. if (!task.IsCompleted && task.IsFaulted)//消息投递失败
  58. {
  59. //记录投递失败的消息信息
  60. }
  61. });
  62. }
  63. public void SendMsg(PushMsg pushMsg, IBus bus)
  64. {
  65. //一对一推送
  66. var message = new Message<object>(pushMsg.sendMsg);
  67. IExchange ex = null;
  68. //判断推送模式
  69. if (pushMsg.sendEnum == SendEnum.推送模式)
  70. {
  71. ex = bus.Advanced.ExchangeDeclare(pushMsg.exchangeName, ExchangeType.Direct);
  72. }
  73. if (pushMsg.sendEnum == SendEnum.订阅模式)
  74. {
  75. //广播订阅模式
  76. ex = bus.Advanced.ExchangeDeclare(pushMsg.exchangeName, ExchangeType.Fanout);
  77. }
  78. if (pushMsg.sendEnum == SendEnum.主题路由模式)
  79. {
  80. //主题路由模式
  81. ex = bus.Advanced.ExchangeDeclare(pushMsg.exchangeName, ExchangeType.Topic);
  82. }
  83. bus.Advanced.Publish(ex, pushMsg.routeName.ToSafeString(“”), false, message);
  84. }
  85. }

在EasyNetQ中对于异步发送消息的时候,消息是否送达Broker只需要查看异步发送方法最终执行成功还是失败,成功就表示消息送达,如果失败可以将失败后的消息存入数据库中,然后用后台线程轮询

数据库表,将失败后的消息进行重新 发送。这种方式还可以进一步变成消息表,就是先将要发送的消息存入消息表中,然后后台线程轮询消息表来进行消息发送。一般这种方式被广泛用于分布式事务中,

将本地数据库操作和消息表写入放入同一个本地事务中,来保证消息发送和本地数据操作的同步成功,因为我的系统中,分布式事务的涉及很少,所以就没这样去做,只是简单的在异步发送的时候监控下

是否发送失败,然后针对失败的消息做一个重新发送的机制。这里,推荐大佬的NetCore分布式事务解决方案 CAP GitHub地址

 接着看一下消息订阅接收涉及的代码

  1. public class MesArgs
  2. {
  3. /// <summary>
  4. /// 消息推送的模式
  5. /// 现在支持:订阅模式,推送模式,主题路由模式
  6. /// </summary>
  7. public SendEnum sendEnum { get; set; }
  8. /// <summary>
  9. /// 管道名称
  10. /// </summary>
  11. public string exchangeName { get; set; }
  12. /// <summary>
  13. /// 对列名称
  14. /// </summary>
  15. public string rabbitQueeName { get; set; }
  16. /// <summary>
  17. /// 路由名称
  18. /// </summary>
  19. public string routeName { get; set; }
  20. }
  21. public interface IMessageConsume
  22. {
  23. void Consume(string message);
  24. }

在订阅中我定义了一个接口,最终业务代码中,所有的消息订阅类,都需要继续此接口

最后,我们来看下对外使用的操作类

  1. public class RabbitMQManage
  2. {
  3. private volatile static IBus bus = null;
  4. private static readonly object lockHelper = new object();
  5. /// <summary>
  6. /// 创建服务总线
  7. /// </summary>
  8. /// <param name=”config”></param>
  9. /// <returns></returns>
  10. public static IBus CreateEventBus()
  11. {
  12. //获取RabbitMq的连接地址
  13. //SystemJsonConfigManage 是我简单封装的一个json操作类,用于针对json文件的读写操作
  14. var config = SystemJsonConfigManage.GetInstance().AppSettings[“MeessageService”];
  15. if (string.IsNullOrEmpty(config))
  16. throw new Exception(“消息地址未配置”);
  17. if (bus == null && !string.IsNullOrEmpty(config))
  18. {
  19. lock (lockHelper)
  20. {
  21. if (bus == null)
  22. bus = RabbitHutch.CreateBus(config);
  23. }
  24. }
  25. return bus;
  26. }
  27. /// <summary>
  28. /// 释放服务总线
  29. /// </summary>
  30. public static void DisposeBus()
  31. {
  32. bus?.Dispose();
  33. }
  34. /// <summary>
  35. /// 消息同步投递
  36. /// </summary>
  37. /// <param name=”pushMsg”></param>
  38. /// <returns></returns>
  39. public static bool PushMessage(PushMsg pushMsg)
  40. {
  41. bool b = true;
  42. try
  43. {
  44. if (bus == null)
  45. CreateEventBus();
  46. new SendMessageMange().SendMsg(pushMsg, bus);
  47. b = true;
  48. }
  49. catch (Exception ex)
  50. {
  51. b = false;
  52. }
  53. return b;
  54. }
  55. /// <summary>
  56. /// 消息异步投递
  57. /// </summary>
  58. /// <param name=”pushMsg”></param>
  59. public static async Task PushMessageAsync(PushMsg pushMsg)
  60. {
  61. try
  62. {
  63. if (bus == null)
  64. CreateEventBus();
  65. await new SendMessageMange().SendMsgAsync(pushMsg, bus);
  66. }
  67. catch (Exception ex)
  68. {
  69. throw ex;
  70. }
  71. }
  72. /// <summary>
  73. /// 消息订阅
  74. /// </summary>
  75. public static void Subscribe<TConsum>(MesArgs args)
  76. where TConsum : IMessageConsume,new()
  77. {
  78. if (bus == null)
  79. CreateEventBus();
  80. if (string.IsNullOrEmpty(args.exchangeName))
  81. return;
  82. Expression<Action<TConsum>> methodCall;
  83. IExchange ex = null;
  84. //判断推送模式
  85. if (args.sendEnum == SendEnum.推送模式)
  86. {
  87. ex = bus.Advanced.ExchangeDeclare(args.exchangeName, ExchangeType.Direct);
  88. }
  89. if (args.sendEnum == SendEnum.订阅模式)
  90. {
  91. //广播订阅模式
  92. ex = bus.Advanced.ExchangeDeclare(args.exchangeName, ExchangeType.Fanout);
  93. }
  94. if (args.sendEnum == SendEnum.主题路由模式)
  95. {
  96. //主题路由模式
  97. ex = bus.Advanced.ExchangeDeclare(args.exchangeName, ExchangeType.Topic);
  98. }
  99. IQueue qu;
  100. if (string.IsNullOrEmpty(args.rabbitQueeName))
  101. {
  102. qu = bus.Advanced.QueueDeclare();
  103. }
  104. else
  105. qu = bus.Advanced.QueueDeclare(args.rabbitQueeName);
  106. bus.Advanced.Bind(ex, qu, args.routeName.ToSafeString(“”));
  107. bus.Advanced.Consume(qu, (body, properties, info) => Task.Factory.StartNew(() =>
  108. {
  109. try
  110. {
  111. lock (lockHelper)
  112. {
  113. var message = Encoding.UTF8.GetString(body);
  114. //处理消息
  115. methodCall = job => job.Consume(message);
  116. methodCall.Compile()(new TConsum());
  117. }
  118. }
  119. catch (Exception e)
  120. {
  121. throw e;
  122. }
  123. }));
  124. }
  125. }

这里面主要封装了消息的发送和订阅,以及IBus单例的创建。在后续的消息发送和订阅主要就通过此处来实现。我们看到一开始的类目结构中还有一个RaExMessageHandleJob类,这个类就是一个后台

循环任务,用来监测数据库中是否保存了发送失败的消息,如果有,则将消息取出,尝试重新发送。在此就不做多的介绍,大家可以根据自己的实际需求来实现。

3.3 发布者

现在来看一下消息发布者的代码

主要的发送代码都在Send类中,其中appsettings.json里面配置了Rabbitmq的连接地址,TestDto只是一个为了方便演示的参数类。

下面看一下Program里面的代码

很简单的一个发送消息调用。

然后来看一下Send类中的代码

  1. public class Send
  2. {
  3. /// <summary>
  4. /// 发送消息
  5. /// </summary>
  6. public static void SendMessage()
  7. {
  8. //需要注意一点儿,如果发送的时候,在该管道下找不到相匹配的队列框架将默认丢弃该消息
  9. //推送模式
  10. //推送模式下,需指定管道名称和路由键值名称
  11. //消息只会被发送到和指定路由键值完全匹配的队列中
  12. var directdto = new PushMsg()
  13. {
  14. sendMsg = new TestDto()
  15. {
  16. Var1 = “这是推送模式”
  17. },
  18. exchangeName = “message.directdemo”,
  19. routeName= “routekey”,
  20. sendEnum =SendEnum.推送模式
  21. };
  22. //同步发送 ,返回true或fasle true 发送成功,消息已存储到Rabbitmq中,false表示发送失败
  23. var b= RabbitMQManage.PushMessage(directdto);
  24. //异步发送,如果失败,失败的消息会被写入数据库,会有后台线程轮询数据库进行重新发送
  25. //RabbitMQManage.PushMessageAsync(directlist);
  26. //订阅模式
  27. //订阅模式只需要指定管道名称
  28. //消息会被发送到该管道下的所有队列中
  29. var fanoutdto = new PushMsg()
  30. {
  31. sendMsg = new TestDto()
  32. {
  33. Var1 = “这是订阅模式”
  34. },
  35. exchangeName = “message.fanoutdemo”,
  36. sendEnum = SendEnum.订阅模式
  37. };
  38. //同步发送
  39. var fb = RabbitMQManage.PushMessage(fanoutdto);
  40. //异步发送
  41. //RabbitMQManage.PushMessageAsync(fanoutdto);
  42. //主题路由模式
  43. //路由模式下需指定 管道名称和路由值
  44. //消息会被发送到该管道下,和路由值匹配的队列中去
  45. var routedto = new PushMsg()
  46. {
  47. sendMsg = new TestDto()
  48. {
  49. Var1 = “这是主题路由模式1”,
  50. },
  51. exchangeName = “message.topicdemo”,
  52. routeName=“a.log”,
  53. sendEnum=SendEnum.主题路由模式
  54. };
  55. var routedto2 = new PushMsg()
  56. {
  57. sendMsg = new TestDto()
  58. {
  59. Var1 = “这是主题路由模式2”,
  60. },
  61. exchangeName = “message.topicdemo”,
  62. routeName = “a.log.a.b”,
  63. sendEnum = SendEnum.主题路由模式
  64. };
  65. //同步发送
  66. var rb = RabbitMQManage.PushMessage(routedto);
  67. var rb2 = RabbitMQManage.PushMessage(routedto2);
  68. //异步发送
  69. //RabbitMQManage.PushMessageAsync(routedto);
  70. }
  71. }

3.4 消费者

首先来看下消费者端的目录结构

 

其中appsettings.json中配置Rabbitmq的连接信息,Program中只是简单调用消息订阅

主要的消息订阅代码都在MessageManage文件夹下,MessageManService用于定义消息订阅类型

  1. public class MessageManService
  2. {
  3. public static void Subsribe()
  4. {
  5. Task.Run(() =>
  6. {
  7. //概念 一个管道下面可以绑定多个队列。
  8. //发送消息 是指将消息发送到管道中,然后由rabbitmq根据发送规则在将消息具体的转发到对应到管道下面的队列中
  9. //消费消息 是指消费者(即服务)从管道下面的队列中获取消息
  10. //同一个队列 可以有多个消费者(即不同的服务,都可以连接到同一个队列去获取消息)
  11. //但注意 当一个队列有多个消费者的时候,消息会被依次分发到不同的消费者中。比如第一条消息给第一个消费者,第二条消息给第二个消费者(框架内部有一个公平分发的机制)
  12. //推送模式时 需指定管道名称和路由值
  13. //队列名称可自己指定
  14. //注意 ,管道名称和路由名称一定要和发送方的管道名称和路由名称一致
  15. //无论这个管道下面挂靠有多少个队列,只有路由名称和此处指定的路由名称完全一致的队列,才会收到这条消息。
  16. var dirarg = new MesArgs()
  17. {
  18. sendEnum = SendEnum.推送模式,
  19. exchangeName = “message.directdemo”,
  20. rabbitQueeName = “meesage.directmessagequene”,
  21. routeName = “routekey”
  22. };
  23. RabbitMQManage.Subscribe<DirectMessageConsume>(dirarg);
  24. //订阅模式时需指定管道名称,并且管道名称要和发送方管道名称一致
  25. //队列名称可自己指定
  26. //所有这个管道下面的队列,都将收到该条消息
  27. var fanoutrg = new MesArgs()
  28. {
  29. sendEnum = SendEnum.订阅模式,
  30. exchangeName = “message.fanoutdemo”,
  31. rabbitQueeName = “meesage.fanoutmessagequene”
  32. };
  33. RabbitMQManage.Subscribe<FanoutMessageConsume>(fanoutrg);
  34. //路由模式时需指定管道名称,路由关键字并且管道名称,路由关键字要和发送方的一致
  35. //队列名称可自己指定
  36. //消息将被发送到管道下面的能匹配路由关键字的队列中
  37. //也就是说 路由模式时,有多少队列能收到消息,取决于该队列的路由关键字是否匹配,只要匹配就能收到消息
  38. //符号“#”匹配一个或多个词,符号“*”匹配不多不少一个词
  39. var topicrg = new MesArgs()
  40. {
  41. sendEnum = SendEnum.主题路由模式,
  42. exchangeName = “message.topicdemo”,
  43. rabbitQueeName = “message.topicmessagequene”,
  44. routeName = “#.log.#”
  45. };
  46. RabbitMQManage.Subscribe<TopicMessageConsume>(topicrg);
  47. });
  48. }
  49. }

Consume文件夹下主要定义了消息的业务处理

  1. //推送模式过来的消息
  2. public class DirectMessageConsume : IMessageConsume
  3. {
  4. //消息的处理方法中最好不要进行try catch操作
  5. //如果发送异常,EasyNetQ会自动将消息放入错误队列中
  6. //如果在Consume方法体中捕获了异常并且没有抛出,会默认消息处理成功
  7. //消息的幂等性需业务方自行处理,也就是说同一条消息可能会接收到两次
  8. //(比如说第一次正在处理消息的时候服务挂掉,服务重启后这条消息又会重新推送过来)
  9. public void Consume(string message)
  10. {
  11. var dto = JsonConvert.DeserializeObject<TestDto>(message);
  12. Console.WriteLine(dto.Var1 + “;” + dto.Var2 + “;” + dto.Var3);
  13. }
  14. }
  15. //广播模式过来的消息
  16. public class FanoutMessageConsume : IMessageConsume
  17. {
  18. //消息的处理方法中最好不要进行try catch操作
  19. //如果发送异常,EasyNetQ会自动将消息放入错误队列中
  20. //如果在Consume方法体中捕获了异常并且没有抛出,会默认消息处理成功
  21. //消息的幂等性需业务方自行处理,也就是说同一条消息可能会接收到两次
  22. //(比如说第一次正在处理消息的时候服务挂掉,服务重启后这条消息又会重新推送过来)
  23. public void Consume(string message)
  24. {
  25. var dto = JsonConvert.DeserializeObject<TestDto>(message);
  26. Console.WriteLine(dto.Var1 + “;” + dto.Var2 + “;” + dto.Var3);
  27. }
  28. }
  29. //主题路由模式过来的消息
  30. public class TopicMessageConsume : IMessageConsume
  31. {
  32. //消息的处理方法中最好不要进行try catch操作
  33. //如果发送异常,EasyNetQ会自动将消息放入错误队列中
  34. //如果在Consume方法体中捕获了异常并且没有抛出,会默认消息处理成功
  35. //消息的幂等性需业务方自行处理,也就是说同一条消息可能会接收到两次
  36. //(比如说第一次正在处理消息的时候服务挂掉,服务重启后这条消息又会重新推送过来)
  37. public void Consume(string message)
  38. {
  39. var dto = JsonConvert.DeserializeObject<TestDto>(message);
  40. Console.WriteLine(dto.Var1 + “;” + dto.Var2 + “;” + dto.Var3);
  41. }
  42. }

可以看到,所有的类都集成自我们定义的接口IMessageConsume。

四、总结

在EasyNetQ中如果需要消费者确认功能,则需要在Rabbitmq的连接配置中设置publisherConfirms=true,这将会开启自动确认。在使用高级api定义交换机和队列时可以自己定义多种参数,比如消息是否持久化,消息最大长度等等,具体大家可以去看官方文档,上面有详细介绍。Easynetq会自动去捕获消费异常的消息并将其放入到错误队列中,而且官方提供了重新发送错误队列中消息的方法,当然你也可以自己去监视错误列队,对异常消息进行处理。EasyNetQ里面作者针对消息的发布确认和消费确认都做了封装。在EasyNetQ中发布消息的时候如果选用的同步发送,只要没有抛出异常,我们就可以认为任务消息已经正确到达Broker,而异步发送的话需要我们自己去监视Task是否成功 。如果开启了自动确认,并不需要我们在消息处理的方法体中手动返回ack信息,只要消息被 正确处理就会自动ack。虽然RabbitMq中也有事务消息,但由于性能比较差,并不推荐使用。其实,只要我们能明确消息是否发布成功和消费成功,就将会很容易在这个基础上扩展出分布式事务的处理。

NoSQL初探之人人都爱Redis:(3)使用Redis作为消息队列服务场景应用案例 - EdisonZhou - 博客园

mikel阅读(929)

来源: NoSQL初探之人人都爱Redis:(3)使用Redis作为消息队列服务场景应用案例 – EdisonZhou – 博客园

一、消息队列场景简介

消息”是在两台计算机间传送的数据单位。消息可以非常简单,例如只包含文本字符串;也可以更复杂,可能包含嵌入对象。消息被发送到队列中,“消息队列”是在消息的传输过程中保存消息的容器

在目前广泛的Web应用中,都会出现一种场景:在某一个时刻,网站会迎来一个用户请求的高峰期(比如:淘宝的双十一购物狂欢节,12306的春运抢票节等),一般的设计中,用户的请求都会被直接写入数据库或文件中,在高并发的情形下会对数据库服务器或文件服务器造成巨大的压力,同时呢,也使响应延迟加剧。这也说明了,为什么我们当时那么地抱怨和吐槽这些网站的响应速度了。当时2011年的京东图书促销,曾一直出现在购物车中点击“购买”按钮后一直是“Service is too busy”,其实就是因为当时的并发访问量过大,超过了系统的最大负载能力。当然,后边,刘强东临时购买了不少服务器进行扩展以求增强处理并发请求的能力,还请了信息部的人员“喝茶”,现在京东已经是超大型的网上商城了,我也有同学在京东成都研究院工作了。

使用消息队列

从京东当年的“Service is too busy”不难看出,高并发的用户请求是网站成长过程中必不可少的过程,也是一个必须要解决的难题。在众多的实践当中,除了增加服务器数量配置服务器集群实现伸缩性架构设计之外,异步操作也被广泛采用。而异步操作中最核心的就是使用消息队列,通过消息队列,将短时间高并发产生的事务消息存储在消息队列中,从而削平高峰期的并发事务,改善网站系统的性能。在京东之类的电子商务网站促销活动中,合理地使用消息队列,可以有效地抵御促销活动刚开始就开始大量涌入的订单对系统造成的冲击

记得我在实习期间,成都市XXXX局的一个价格信息采集发布系统项目中有一个采集任务发布的模块,其中每个任务都是一个事务,这个事务中需要向数据库中不断地插入行,每个任务发布时都要往表中插入几百行甚至几千行的任务数据(比如价格采集日报,往往需要发布2-3年的任务数据,每一天都是一个任务,所以大约有2,3千行任务期号数据,还要发给很多个区县的监测中心,因此数据库写操作量很大,更别说同时发布的并发操作),由于业务逻辑的处理比较复杂和往数据库的写操作量交大,所以在没有采用消息队列时点击“发布”按钮后往往需要等待1分钟左右的时间才提示“发布成功”,用户体验极不友好。

这时,我们就可以使用消息队列的思想来重构这个发布模块,在用户点击“发布”按钮后,系统只需要把往数据库插入的这个事务信息插入到指定的任务发布消息队列里边去(入队操作,这里一般有一台独立的消息队列服务器来单独存储和处理),然后系统就可以立即对用户的这个发布请求进行响应(比如给出一个发布成功的操作提示,这里暂不考虑消息队列服务操作失败的情形,如果失败了,可以考虑采用给用户发送邮件、短信或站内消息,让其重新进行发布操作)。

队列结构

最后,消息队列服务器中有一个进程单独对消息队列进行处理,首先判断消息队列中是否有待处理的消息,如果有,则将其取出(出队操作,坚持“先进先出”的顺序,保证事务的准确性)进行相应地处理(比如这里是进行保存数据的操作,将数据插入到数据库服务器中的指定数据库里边,实质还是文件的IO操作)。就这样,通过消息队列将高并发用户请求进行异步操作,然后一一对消息队列进行出队的同步操作,也避免了并发控制的难题。

说到这里,大家可能会想到这尼玛不就是生产者消费者模式么?对的,么么嗒,消息队列就是生产者消费者模式的典型场景。简单地说,客户端不同用户发送的操作请求就是生产者,他们将要处理的事务存储到消息队列中,然后消息队列服务器的某个进程不停地将要处理的单个事务从消息队列中一个一个地取出来进行相应地处理,这就是消费者消费的过程。

下面我们将以异常日志为案例,介绍在.Net中如何采用消息队列的思想解决并发问题。当然,消息队列只是解决并发问题的其中一种方式,在实际中往往需要结合多种不同的技术方式来共同解决,比如负载均衡、反向代理、集群等方案。这里,虽然以异常日志为案例,但是“麻雀虽小五脏俱全”,日志写入文件的高并发操作也同样适用于数据库的高并发,所以,研究这个案例是具有实际意义的。

二、使用预置类型实现异常日志队列

在日常的Web应用中,异常日志的记录是一个十分重要的要点。因为,人无完人,系统也一样,难免会在什么时候出一个测试阶段未能完全测试到的异常。这时候,不能将异常信息直接显示给客户,那样既不友好也不安全。所以,一般都采用将异常信息记录到日志文件中(比如某个txt文件,数据库中某个表等),然后技术支持人员通过查看异常日志,分析异常原因,改进BUG重新发布,保障系统正常运行。

在用户的各种操作中,如果出现异常的时间一致,那么记录异常日志的操作就会成为并发操作,而记录异常日志又属于文件的IO操作(其实数据库的读写归根结底也是对文件即对磁盘进行的IO操作),因此很有可能带来并发控制的一系列问题。在以往的编码实践中,我们可以通过给不同的IO请求进行加锁(C#中的lock),等第一个请求完成写入后释放锁,第二个请求再获得锁,进行IO操作,然后释放掉,一直到第N个请求释放后结束。这种方式,虽然解决了并发操作带来的问题,但是通过加锁延迟了用户响应请求的时间(比如第一个正在IO写入操作时,后面的均处于等待状态),并且加锁也会给服务器带来一定的性能负担,造成服务器性能的下降。

基于以上原因,我们采用消息队列的思想将异常日志的记录操作改为队列版,这里我们先不采用Redis,直接使用.Net为我们提供的预置类型-Queue。接下来,就让我们动手开刀,写起来。

(1)新建一个ASP.NET MVC 4项目,选择“基本”类型,视图引擎选择“Razor”。

(2)既然是异常日志记录,首先得有异常。这时,我们脑海中想到了那个经典的异常:DividedByZeroException。于是,在Controllers文件夹中新建一个Controller,取名为Home(这里因为Global文件中的默认路由就指向了Home控制器中的Index这个Action),在HomeController中修改Index这个Action的代码如下:

复制代码
        public ActionResult Index()
        {
            int a = 10;
            int b = 0;
            int c = a / b; //会抛一个DividedByZero的异常

            return View();
        }
复制代码

(3)在ASP.NET MVC项目中,我们需要在Global.asax中的Application_Start这个事件中修改全局过滤器(主要是App_Start中的FilterConfig类的RegisterGlobalFilters这个方法),让系统支持对异常的全局处理操作(我们这里主要是对异常进行记录到指定文件中)。PS:Application_Start是整个Web应用的起始事件,主要进行一些配置(如过滤器配置、日志器配置、路由配置等等)的初始化操作,当然这些配置也只会进行一次。

复制代码
    public class FilterConfig
    {
        public static void RegisterGlobalFilters(GlobalFilterCollection filters)
        {
            // MyExceptionFilterAttribute继承自HandleError,主要作用是将异常信息写入日志文件中
            filters.Add(new MyExceptionFilterAttribute());
            // 默认的异常记录类
            filters.Add(new HandleErrorAttribute());
        }
    }
复制代码

通过改写过滤器配置,我们向全局过滤器中注册了一个异常处理的过滤器配置,那么这个MyExceptionFilterAttribute类又是如何编写的呢?

复制代码
    public class MyExceptionFilterAttribute : HandleErrorAttribute
    {
        //版本1:使用预置队列类型存储异常对象
        public static Queue<Exception> ExceptionQueue = new Queue<Exception>();

        public override void OnException(ExceptionContext filterContext)
        {
            //将异常信息入队
            ExceptionQueue.Enqueue(filterContext.Exception);
            //跳转到自定义错误页
            filterContext.HttpContext.Response.Redirect("~/Common/CommonError.html");

            base.OnException(filterContext);
        }
    }
复制代码

通过使该类继承HandlerErrorAttribute并使其覆写OnException这个事件,代表在异常发生时可以进行的操作。而我们在这儿主要通过一个异常队列将获取的异常写入队列,然后跳转到自定义错误页:~/Common/CommonError.html,这个错误页很简单,就是简单的显示“系统发生错误,5秒后自动跳转到首页”

 View Code

(4)走到这里,生产者消费者模式中生产者的任务已经完成了,接下来消费者就需要开始消费了。也就是说,消息队列已经建好了,我们什么时候从队列中去任务,在哪里执行?怎么样执行?通过上面的介绍,我们知道,在专门的消息队列服务器中有一个进程在始终不停地监视消息队列,如果有需要待办的任务信息,则会立即从队列中取出来执行相应的操作,直到队列为空为止。于是,思路有了,我们马上来实现以下。这个消息监视的操作也是一个全局操作,在系统启动时就会一直运行,于是它也应该写在Application_Start这个全局起始事件里边,于是按照标准的配置写法,我们在Application_Start中添加了如下代码:MessageQueueConfig.RegisterExceptionLogQueue();

复制代码
        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();

            WebApiConfig.Register(GlobalConfiguration.Configuration);
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            BundleConfig.RegisterBundles(BundleTable.Bundles);

            //自定义事件注册
            MessageQueueConfig.RegisterExceptionLogQueue();
        }
复制代码

那么,这个MessageQueueConfig.RegisterExceptionLogQueue()又是怎么写的呢?

复制代码
  public class MessageQueueConfig
    {
        public static void RegisterExceptionLogQueue()
        {
            string logFilePath = HttpContext.Current.Server.MapPath("/App_Data/");
            //通过线程池开启线程,不停地从队列中获取异常信息并将其写入日志文件
            ThreadPool.QueueUserWorkItem(o =>
            {
                while (true)
                {
                    try
                    {
                        if (MyExceptionFilterAttribute.ExceptionQueue.Count > 0)
                        {
                            Exception ex = MyExceptionFilterAttribute.ExceptionQueue.Dequeue(); //从队列中出队,获取异常对象
                            if (ex != null)
                            {
                                //构建完整的日志文件名
                                string logFileName = logFilePath + DateTime.Now.ToString("yyyy-MM-dd") + ".txt";
                                //获得异常堆栈信息
                                string exceptionMsg = ex.ToString();
                                //将异常信息写入日志文件中
                                File.AppendAllText(logFileName, exceptionMsg, Encoding.Default);
                            }
                        }
                        else
                        {
                            Thread.Sleep(1000); //为避免CPU空转,在队列为空时休息1秒
                        }
                    }
                    catch (Exception ex)
                    {
                        MyExceptionFilterAttribute.ExceptionQueue.Enqueue(ex);
                    }
                }
            }, logFilePath);
        }
    }
复制代码

现在,让我们来看看这段代码:

①首先定义Log文件存放的文件夹目录,这里我们一般放到App_Data里边,因为放到这里边外网是无法访问到的,可以防止下载操作;

②其次通过线程池ThreadPool开启一个线程,不停地监听消息队列里边的待办事项个数,如果个数>0,则进行出队(FIFO,先入队的先出队)操作。这里主要是取出具体的异常实例对象,并将异常的具体堆栈信息追加写入到指定命名格式的文件中。

PS:许多应用程序创建的线程都要在休眠状态中消耗大量时间,以等待事件发生。其他线程可能进入休眠状态,只被定期唤醒以轮询更改或更新状态信息。线程池通过为应用程序提供一个由系统管理的辅助线程池使您可以更为有效地使用线程。关于线程池的更多信息请访问:http://msdn.microsoft.com/zh-cn/library/system.threading.threadpool(v=VS.90).aspx

③如果该线程检测到消息队列中无待办事项,则使用Thread.Sleep使线程“休息”一会,避免了CPU空转(从理论上来说,CPU资源是很珍贵的,应该尽量提高CPU的利用率)。

(5)最后,我们来看看效果如何?

①首先,高大上的VS捕捉到了异常-DividedByZeroException:

②按照我们的全局异常处理过滤器,会将此异常记入队列中,并返回HTTP 302重定向跳转到自定义错误页面:

③最后,打开App_Data文件夹,查看日志文件:

到这里时,我们已经借助消息队列的思想完成了一个自定义的异常日志队列服务。但也许有朋友会说,这个跟Redis有关系么?异常日志不都是用Log4Net么?不要着急,后边我们就会使用Redis+Log4Net来重构这个异常日志队列服务,不要走开,我们不得插播广告哦,么么嗒!

三、使用Redis重构异常日志队列

(1)第一步,开启Redis的服务,这里我们使用命令开启Redis服务(之前已经将Redis注册到了Windows系统服务中了嘛,么么嗒):net start redis-instance,当然,也可以通过在Windows服务列表中开启。

(2)第二步,在刚刚的版本1的Demo中新建一个文件夹,命名为Lib,将ServiceStack.Redis的dll和Log4Net的dll都拷贝进去。然后,在引用中添加对Lib文件夹中所有dll的引用。

(3)第三步,重写MyExceptionFilterAttribute这个全局异常信息过滤器。这里使用到了Redis的客户端连接池,每次连接时都是从池中取,不需要每次都创建,节省了时间和资源,提高了资源利用率。对于,多台Redis服务器组成的集群而言,这里需要指定多个形如 IP地址:端口号 的字符串数组。

复制代码
    public class MyExceptionFilterAttribute : HandleErrorAttribute
    {
        //版本2:使用Redis的客户端管理器(对象池)
        public static IRedisClientsManager redisClientManager = new PooledRedisClientManager(new string[] 
        {
            //如果是Redis集群则配置多个{IP地址:端口号}即可
            //例如: "10.0.0.1:6379","10.0.0.2:6379","10.0.0.3:6379"
            "127.0.0.1:6379"
        });
        //从池中获取Redis客户端实例
        public static IRedisClient redisClient = redisClientManager.GetClient();

        public override void OnException(ExceptionContext filterContext)
        {
            //将异常信息入队
            redisClient.EnqueueItemOnList("ExceptionLog", filterContext.Exception.ToString());
            //跳转到自定义错误页
            filterContext.HttpContext.Response.Redirect("~/Common/CommonError.html");

            base.OnException(filterContext);
        }
    }
复制代码

(4)第四步,首先在Web.config中加入Log4Net的详细配置。

复制代码
<configSections>
    <!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 -->
    <section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=4.4.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
    <!-- Log4Net配置声明 -->
    <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler,log4net"/>
  </configSections>
  <!-- Log4Net具体配置 -->
  <log4net>
    <!-- OFF, FATAL, ERROR, WARN, INFO, DEBUG, ALL -->
    <!-- Set root logger level to ERROR and its appenders -->
    <root>
      <level value="ALL"/>
      <appender-ref ref="SysAppender"/>
    </root>
    <!-- Print only messages of level DEBUG or above in the packages -->
    <logger name="WebLogger">
      <level value="DEBUG"/>
    </logger>
    <appender name="SysAppender" type="log4net.Appender.RollingFileAppender,log4net" >
      <param name="File" value="App_Data/" />
      <param name="AppendToFile" value="true" />
      <param name="RollingStyle" value="Date" />
      <param name="DatePattern" value="&quot;Logs_&quot;yyyyMMdd&quot;.txt&quot;" />
      <param name="StaticLogFileName" value="false" />
      <layout type="log4net.Layout.PatternLayout,log4net">
        <!--<param name="ConversionPattern" value="%d [%t] %-5p %c - %m%n" />-->
        <param name="ConversionPattern" value="记录时间:%date %n线程ID: [%thread] %n日志级别:%-5level %n出错类:%logger property: [%property{NDC}] - %n错误描述:%message%newline %n" />
        <param name="Header" value="-------------------------------------------------------header-----------------------------------------------------------&#13;&#10;" />
        <param name="Footer" value="-------------------------------------------------------footer-----------------------------------------------------------&#13;&#10;" />
      </layout>
    </appender>
    <appender name="consoleApp" type="log4net.Appender.ConsoleAppender,log4net">
      <layout type="log4net.Layout.PatternLayout,log4net">
        <param name="ConversionPattern" value="%d [%t] %-5p %c - %m%n" />
      </layout>
    </appender>
  </log4net>
复制代码

PS:Log4Net是用来记录日志的一个常用组件(Log4J的移植版本),可以将程序运行过程中的信息输出到一些地方(文件、数据库、EventLog等)。由于Log4Net不是本篇博文介绍的重点,所以对Log4Net不熟悉的朋友,请在博客园首页搜索:Log4Net,浏览其详细的介绍。

其次,在App_Start文件夹中添加一个类,取名为LogConfig,定义一个静态方法:RegisterLog4NetConfigure,具体代码只有一行,实现了Log4Net配置的初始化操作。

复制代码
    public class LogConfig
    {
        public static void RegisterLog4NetConfigure()
        {
            //获取Log4Net配置信息(配置信息定义在Web.config文件中)
            log4net.Config.XmlConfigurator.Configure();
        }
    }
复制代码

最后,在Global.asax中的Application_Start方法中添加一行代码,注册Log4Net的配置:

复制代码
        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();

            WebApiConfig.Register(GlobalConfiguration.Configuration);
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            BundleConfig.RegisterBundles(BundleTable.Bundles);


            //自定义事件注册
            MessageQueueConfig.RegisterExceptionLogQueue();
            LogConfig.RegisterLog4NetConfigure();
        }
复制代码

(5)第五步,改写MessageQueueConfig中的RegisterExceptionLogQueue方法。这里就不再需要从预置类型Queue中取任务了,而是Redis中取出任务出队进行相应处理。这里,我们使用了Log4Net进行异常日志的记录工作。PS:注意在代码顶部添加对log4net的引用:using log4net;

复制代码
      public static void RegisterExceptionLogQueue()
        {
            //通过线程池开启线程,不停地从队列中获取异常信息并将其写入日志文件
            ThreadPool.QueueUserWorkItem(o =>
            {
                while (true)
                {
                    try
                    {
                        if (MyExceptionFilterAttribute.redisClient.GetListCount("ExceptionLog") > 0)
                        {
                            //从队列中出队,获取异常对象
                            string errorMsg = MyExceptionFilterAttribute.redisClient.DequeueItemFromList("ExceptionLog");
                            if (!string.IsNullOrEmpty(errorMsg))
                            {
                                //使用Log4Net写入异常日志
                                ILog logger = LogManager.GetLogger("Log");
                                logger.Error(errorMsg);
                            }
                        }
                        else
                        {
                            Thread.Sleep(1000); //为避免CPU空转,在队列为空时休息1秒
                        }
                    }
                    catch (Exception ex)
                    {
                        MyExceptionFilterAttribute.redisClient.EnqueueItemOnList("ExceptionLog", ex.ToString());
                    }
                }
            });
        }
复制代码

(6)最后一步,调试验证是否能正常写入App_Data文件的日志中,发现写入的异常日志如下,格式好看,信息详细,圆满完成了我们的目的。

四、小结

使用消息队列将调用异步化,可以改善网站系统的性能:消息队列具有很好的削峰作用,即通过异步处理,将短时间高并发产生的事务消息存储在消息队列中,从而削平高峰期的并发事务。在电商网站的促销活动中,合理使用消息队列,可以有效地抵御促销活动刚开始大量涌入的订单对系统造成的冲击。本文使用消息队列的思想,借助Redis+Log4Net完成了一个超简单的异常日志队列的应用案例,可以有效地解决在多线程操作中对日志文件的并发操作带来的一些问题。同样地,借助消息队列的思想,我们也可以完成对数据库的高并发的消息队列方案。所以,麻雀虽小五脏俱全,理解好了这个案例,相信对我们这些菜鸟码农是有所裨益的。同样,也请大牛们一笑而过,多多指教菜鸟们一步一步地提高,谢谢了!后边,我们会探索一下Redis的集群、主从复制,以及在VMWare中建立几台虚拟机来构建主从结构,并使用Redis记录网站中重要的Session会话对象,或者是电商项目中常见的商品类目信息等。但是,本人资质尚浅,并且都是一些初探性质的学习,如有错误和不当,还请各位园友多多指教!

参考文献

(1)传智播客.Net学院王承伟,数据优化技术之Redis公开课,http://bbs.itcast.cn/thread-26525-1-1.html

(2)Sanfilippo/贾隆译,《几点建议,让Redis在你的系统中发挥更大作用》,http://database.51cto.com/art/201107/276333.htm

(3)NoSQLFan,《Redis作者谈Redis应用场景》,http://blog.noSQLfan.com/html/2235.html

(4)善心如水,《C#中使用Log4Net记录日志》,http://www.cnblogs.com/wangsaiming/archive/2013/01/11/2856253.html

(5)逆心,《ServiceStack.Redis之IRedisClient》,http://www.cnblogs.com/kissdodog/p/3572084.html

(6)李智慧,《大型网站技术架构-核心原理与案例分析》,http://item.jd.com/11322972.html

附件下载

(1)版本1:使用预置类型的异常日志队列Demo,http://pan.baidu.com/s/1nt5G7Fj

(2)版本2:使用Redis+Log4Net的异常日志队列Demo,http://pan.baidu.com/s/1i3gMnnJ

22、The Advanced API 高级API - 困兽斗 - 博客园

mikel阅读(912)

来源: 22、The Advanced API 高级API – 困兽斗 – 博客园

EasyNetQ的使命是为RabbitMQ消息传递提供最简单的API。核心IBus接口有意避免暴露AMQP概念:如交换器、绑定、队列。相反,EasyNetQ实现一个默认基于消息的class type的“交换器+绑定+队列”拓扑结构。

有些场景下,需要能配置自定义的“交换器+绑定+队列”拓扑。EasyNetQ的The Advanced API 就可以提供这些功能。这个高级API对AMQP标准有很好的理解。

高级API是通过IAdvancedBus接口提供的,你可以通过IBus的Advanced属性获得一个IAdvancedBus接口的实例。

var advancedBus = RabbitHutch.CreateBus("host=localhost").Advanced;

 

一、如何声明交换器

要声明一个交换器(在RabbitMQ上),你可以使用EasyNetQ.IAdvancedBus接口的ExchangeDeclare方法,方法原型:

复制代码
IExchange ExchangeDeclare(
    string name, 
    string type, 
    bool passive = false, 
    bool durable = true, 
    bool autoDelete = false, 
    bool @internal = false, 
    string alternateExchange = null, 
    bool delayed = false);
复制代码

形参的含义如下:

复制代码
name:                欲创建的交换器名The name of the exchange you want to create
type:                欲创建交换器类型,必须是AMQP标准里定义的类型,你可以通过ExchangeType类的静态属性安全地指定它。
passive:            指定为true时,如果该名字的交换器之前不存在,不会创建它,而是抛出异常。(默认 false)
durable:            交换器是否可持久。(默认 true)
autoDelete:            当最后一个队列解绑定后,该交换器是否自动删除。(默认 false)
internal:            指定为true时,该交换器不能直接被发布者使用,而只能被其他普通交换器绑定使用。(默认 false)
alternateExchange:    替代交换器名。如果无法路由消息,就将消息路由到该交换器。
delayed:            指定为true时,声明一个x-delayed-type交换器,用于路由延迟消息。
复制代码

小例子:

复制代码
//创建一个直接交换器
var exchange = advancedBus.ExchangeDeclare("my_exchange", ExchangeType.Direct);

//创建一个主题交换器
var exchange = advancedBus.ExchangeDeclare("my_exchange", ExchangeType.Topic);

//创建一个扇出交换器
var exchange = advancedBus.ExchangeDeclare("my_exchange", ExchangeType.Fanout);
复制代码

获取RabbitMQ默认的交换器:

var exchange = Exchange.GetDefault();

 

 

二、如何声明队列

要声明一个消息队列(在RabbitMQ上),你可以使用EasyNetQ.IAdvancedBus接口的QueueDeclare方法,方法原型:

复制代码
IQueue QueueDeclare(
    string name, 
    bool passive = false, 
    bool durable = true, 
    bool exclusive = false, 
    bool autoDelete = false,
    int? perQueueMessageTtl  = null, 
    int? expires = null,
    byte? maxPriority = null,
    string deadLetterExchange = null, 
    string deadLetterRoutingKey = null,
    int? maxLength = null,
    int? maxLengthBytes = null);
复制代码

形参含义如下:

复制代码
name:                      队列名
passive:                   如果该队列之前不存在,不创建它,而是抛出异常。(默认 false)
durable:                   队列是否可持久。(默认 true)
exclusive:                 是否当前连接专用。(默认 false)
autoDelete:                是否自动删除队列,一旦所有消费者断开连接。(默认 false)
perQueueMessageTtl:        在被丢弃之前,消息应该在队列中保留多长时间(毫秒)。(默认 null,即不设置)
expires:                   在自动删除队列之前,该队列应该保持未使用状态多长时间(毫秒)。(默认 null,即不设置)
maxPriority:               指定队列应该支持的最大消息优先级。
deadLetterExchange:        指定在被RabbitMQ服务器自动删除之前,交换的名称是否保持未占用状态。
deadLetterRoutingKey:      如果设置了,将使用指定的路由键路由消息,如果没有设置,消息将使用它们最初发布的同一路由键进行路由。
maxLength:                 队列中能够存放的ready消息的最大数量。 一旦超限,为了给新来消息腾位置,队首消息将被丢弃或者成为死信。
maxLengthBytes:            队列最大字节数。 一旦超限,为了给新来消息腾位置,队首消息将被丢弃或者成为死信。
复制代码

请注意RabbitMQ对待上面两个maxLength的行为,它们并不像人们想象那样。有人可能以为超限后RabbitMQ会拒绝接收(生产者)更多的消息,然而RabbitMQ文档指出一旦超限,队首消息将被丢弃或者成为死信,要为新来的消息腾地方。

 

小例子:

// 声明一个持久化队列
var queue = advancedBus.QueueDeclare("my_queue");

// declare a queue with message TTL of 10 seconds:
var queue = advancedBus.QueueDeclare("my_queue", perQueueMessageTtl:10000);

 

要声明一个“未命名”的独占队列,(实际上由RabbitMQ为之产生一个队列名),请调用QueueDeclare() 无参重载方法:

var queue = advancedBus.QueueDeclare();

请注意,EasyNetQ的自动消费者重连接逻辑不能用于“独占队列”。

 

 

三、绑定

你可以像这样把一个队列绑定到一个交换器:

var queue = advancedBus.QueueDeclare("my.queue");    //队列
var exchange = advancedBus.ExchangeDeclare("my.exchange", ExchangeType.Topic);    //主题交换器
var binding = advancedBus.Bind(exchange, queue, "A.*");//绑定,指定路由键

 

要指定一个队列和一个交换之间的多个绑定,只需多次调用Bind方法:

var queue = advancedBus.QueueDeclare("my.queue");   //声明队列
var exchange = advancedBus.ExchangeDeclare("my.exchange", ExchangeType.Topic);  //声明交换器

advancedBus.Bind(exchange, queue, "A.B");   //绑定, 主题设置为 A.B
advancedBus.Bind(exchange, queue, "A.C");   //绑定,主题设置为 A.C

 

你也可以把一个交换器绑定到另一个交换器上,穿成串

复制代码
var sourceExchange = advancedBus.ExchangeDeclare("my.exchange.1", ExchangeType.Topic);       //源交换器
var destinationExchange = advancedBus.ExchangeDeclare("my.exchange.2", ExchangeType.Topic);  //目标交换器
var queue = advancedBus.QueueDeclare("my.queue");        //声明队列

advancedBus.Bind(sourceExchange, destinationExchange, "A.*");   //把源交换器绑定到目标交换器
advancedBus.Bind(destinationExchange, queue, "A.C");            //把目标交换器绑定到队列
复制代码

注意上面穿成串后,目标交换器收到A主题和 *(任一个字母)的主题消息;而队列只能收到A和C的主题消息。

 

 

四、发布

高级发布方法允许指定你要把消息发布到哪个交换器上,它还允许访问消息的AMQP标准的basic属性。

高级API要求将你的消息封装到Message类对象中

var myMessage = new MyMessage {Text = "Hello from the publisher"};
var message = new Message<MyMessage>(myMessage);

Message类使你可以访问AMQP的basic属性,例如:

message.Properties.AppId = "my_app_id";
message.Properties.ReplyTo = "my_reply_queue";

最后你只要调用Publish方法发布你的消息,在下例我们发布到默认交换器

bus.Publish(Exchange.GetDefault(), queueName, false, false, message);

一个重载Publish方法允许你绕过EasyNetQ的消息序列化,直接创建你自己的字节数组作为消息。

var properties = new MessageProperties();
var body = Encoding.UTF8.GetBytes("Hello World!");
bus.Publish(Exchange.GetDefault(), queueName, false, false, properties, body);

 

 

五、消费

使用IAdvancedBus接口的Consume方法,就可以消费队列中的消息。

IDisposable Consume<T>(IQueue queue, Func<IMessage<T>, MessageReceivedInfo, Task> onMessage) where T : class;

onMessage 委托是你要提供的消息处理方法。

正如上面的“发布”那一节所描述的,IMessage可以让你访问消息和它的MessageProperties。而这里MessageRecivedInfo提供了关于消息被消费的上下文的额外信息:

复制代码
public class MessageReceivedInfo
{
    public string ConsumerTag { get; set; }
    public ulong DeliverTag { get; set; }
    public bool Redelivered { get; set; }
    public string Exchange { get; set; }
    public string RoutingKey { get; set; }         
}
复制代码

onMessage委托返回一个Task,该任务允许你编写非阻塞的异步处理程序。

该消费方法返回一个IDisposable接口实例,调用该实例的Dispose方法,可以撤销该消费者。

如果你仅仅需要同步处理消息,你可以调用同步的Consume重载方法:

IDisposable Consume<T>(IQueue queue, Action<IMessage<T>, MessageReceivedInfo> onMessage) where T : class;

 

如果要绕过EasyNetQ的消息序列化,调用下面的Consume重载方法,提供一个字节数组(作为消息):

void Consume(IQueue queue, Func<Byte[], MessageProperties, MessageReceivedInfo, Task> onMessage);

在下面示例中,我们正在消费队列“myqueue”中的原始字节数组(即消息):

复制代码
var queue = advancedBus.QueueDeclare("my_queue");   //声明队列
advancedBus.Consume(queue, (body, properties, info) => Task.Factory.StartNew(() =>
    {
        var message = Encoding.UTF8.GetString(body);
        Console.WriteLine("Got message: '{0}'", message);
    }));
复制代码

你可以调用另一个重载的Consume方法,让单个消费者可选地注册多个处理委托:

IDisposable Consume(IQueue queue, Action<IHandlerRegistration> addHandlers);

IHandlerRegistration 接口如下所示:

复制代码
public interface IHandlerRegistration
{
    /// <summary>
    /// 添加一个异步处理委托Add an asynchronous handler
    /// </summary>
    /// <typeparam name="T">消息类型The message type</typeparam>
    /// <param name="handler">处理委托The handler</param>
    /// <returns></returns>
    IHandlerRegistration Add<T>(Func<IMessage<T>, MessageReceivedInfo, Task> handler)
        where T : class;

    /// <summary>
    /// 添加一个同步处理委托Add a synchronous handler
    /// </summary>
    /// <typeparam name="T">消息类型The message type</typeparam>
    /// <param name="handler">处理委托The handler</param>
    /// <returns></returns>
    IHandlerRegistration Add<T>(Action<IMessage<T>, MessageReceivedInfo> handler)
        where T : class;

    /// <summary>
    /// 如果设置为true,如果没有适合的处理委托,将会抛出异常。
    /// 设置为false,返回一个无操作(什么也不做)的委托。(默认 true)
    /// Set to true if the handler collection should throw an EasyNetQException when no
    /// matching handler is found, or false if it should return a noop handler.
    /// Default is true.
    /// </summary>
    bool ThrowOnNoMatchingHandler { get; set; }
}
复制代码

在下面例子中,我们注册了两个不同的处理委托:一个处理MyMessage类型消息,另一个处理MyOtherMessage类型消息:

复制代码
bus.Advanced.Consume(queue, x => x
        .Add<MyMessage>((message, info) => 
            { 
                Console.WriteLine("Got MyMessage {0}", message.Body.Text);
                countdownEvent.Signal();
            })
        .Add<MyOtherMessage>((message, info) =>
            {
                Console.WriteLine("Got MyOtherMessage {0}", message.Body.Text);
                countdownEvent.Signal();
            })
    );
复制代码

更多信息请参阅这篇博客文章:

http://mikehadlow.blogspot.co.uk/2013/11/easynetq-multiple-handlers-per-consumer.html

 

 

6、从队列获取单条消息

要从队列中获得单条消息,请使用IAdvancedBus.Get() 方法:

IBasicGetResult<T> Get<T>(IQueue queue) where T : class;

AMQP文档说:“该方法使用同步对话直接访问队列中的消息,该对话是为特定类型的应用程序设计的,而同步功能比性能更重要。”

不要在循环中调用Get方法访问消息队列。在一般场景中,我想你一定只喜欢使用Consume方法。

IBasicGetResult接口如下所示:

复制代码
/// <summary>
/// AdvancedBus.Get 方法获取的结果
/// </summary>
/// <typeparam name="T"></typeparam>
public interface IBasicGetResult<T> where T : class
{
    /// <summary>
    /// 消息是否可用。True if a message is availabe, false if not.
    /// </summary>
    bool MessageAvailable { get; }

    /// <summary>
    /// The message retreived from the queue. 
    /// This property will throw a MessageNotAvailableException if no message
    /// was available. You should check the MessageAvailable property before
    /// attempting to access it.你应该先检查MessageAvailable属性值,再读取该属性值。
    /// </summary>
    IMessage<T> Message { get; }
}
复制代码

注意:在读取Message属性前,你应该总是先检查MessageAvailable属性值是否为true才行(避免抛出异常),如下例所示:

复制代码
var queue = advancedBus.QueueDeclare("get_test");    //声明队列
advancedBus.Publish(Exchange.GetDefault(), "get_test", false, false,
    new Message<MyMessage>(new MyMessage{ Text = "Oh! Hello!" }));    //发布消息

var getResult = advancedBus.Get<MyMessage>(queue);   //获取单条消息

if (getResult.MessageAvailable)   //如果消息可用
{
    Console.Out.WriteLine("Got message: {0}", getResult.Message.Body.Text);
}
else
{
    Console.Out.WriteLine("Failed to get message!");
}
复制代码

要访问二进制消息,请使用非泛型的Get方法:

IBasicGetResult Get(IQueue queue);

非泛型的IBasicGetResult接口定义如下:

复制代码
public interface IBasicGetResult
{
    byte[] Body { get; }
    MessageProperties Properties { get; }
    MessageReceivedInfo Info { get; }
}
复制代码

 

 

7、消息类型必须匹配

EasyNetQ 高级API要求订阅者只接收泛型类型参数指定的类型的消息。在上例中,只接收类型MyMessage类型的消息。

但是,EasyNetQ不担保你发布错误类型的消息给订阅者。比如:我可以很容易地设置一个 “交换器-绑定-队列”拓扑来发布NotMyMessage类型的消息,而用上面的处理程序接收它。

如果接收到错误类型的消息,EasyNetQ会抛出EasyNetQInvalidMessageTypeException 异常,如下:

EasyNetQ.EasyNetQInvalidMessageTypeException: Message type is incorrect. Expected 'EasyNetQ_Tests_MyMessage:EasyNetQ_Tests', but was 'EasyNetQ_Tests_MyOtherMessage:EasyNetQ_Tests'
   at EasyNetQ.RabbitAdvancedBus.CheckMessageType[TMessage](MessageProperties properties) in D:\Source\EasyNetQ\Source\EasyNetQ\RabbitAdvancedBus.cs:line 217
   at EasyNetQ.RabbitAdvancedBus.<>c__DisplayClass1`1.<Subscribe>b__0(Byte[] body, MessageProperties properties, MessageReceivedInfo messageRecievedInfo) in D:\Source\EasyNetQ\Source\EasyNetQ\RabbitAdvancedBus.cs:line 131
   at EasyNetQ.RabbitAdvancedBus.<>c__DisplayClass6.<Subscribe>b__5(String consumerTag, UInt64 deliveryTag, Boolean redelivered, String exchange, String routingKey, IBasicProperties properties, Byte[] body) in D:\Source\EasyNetQ\Source\EasyNetQ\RabbitAdvancedBus.cs:line 176
   at EasyNetQ.QueueingConsumerFactory.HandleMessageDelivery(BasicDeliverEventArgs basicDeliverEventArgs) in D:\Source\EasyNetQ\Source\EasyNetQ\QueueingConsumerFactory.cs:line 85

 

8、事件

当通过RabbitHutch方法实例化一个IBus接口实例时,您可以指定一个AdvancedBusEventHandlers委托。

这个类包含一个事件处理委托属性,用于在 IAdvancedBus中的每个事件,提供了在bus实例化之前指定事件处理程序的方法。

不需要使用它,因为一旦创建了bus,仍然可以添加事件处理程序。

但是,你想要抓到 RabbitAdvancedBus.的首次已连接事件,你必须使创建 AdvancedBusEventHandlers 委托,注册已连接事件Connected

这是因为bus在其构造函数返回前只尝试连接一次。如果连接成功,会触发RabbitAdvancedBus.OnConnected事件。

var bus = RabbitHutch.CreateBus("host=localhost", new AdvancedBusEventHandlers(connected: (s, e) =>
{
      var advancedBus = (IAdvancedBus)s;
      Console.WriteLine(advancedBus.IsConnected); // This will print true.
}));

 

1、EasyNetQ手册翻译目录 - 困兽斗 - 博客园

mikel阅读(1191)

来源: 1、EasyNetQ手册翻译目录 – 困兽斗 – 博客园

EasyNetQ是一个简单的用于RabbitMQ .NET Client 的API。首先要安装RabbitMQ

  1. Install Erlang: http://www.erlang.org/download.html
  2. Install the RabbitMQ server: http://www.rabbitmq.com/download.html
  3. You’ll probably want to enable the RabbitMQ management API too: http://www.rabbitmq.com/management.html

现在您应该能够登录RabbitMQ管理Web界面 URL:

http://localhost:15672/

有个小例子你可以下载后测试测试:

https://github.com/mikehadlow/EasyNetQTest

 

下面开始手册目录

 

2、Introduction介绍

3、A Note on Versioning版本控制 —— 4、Installing EasyNetQ如何安装

5、Connecting to RabbitMQ连接到RabbitMQ服务器

6、Connecting with SSL 在EasyNetQ上使用SSL连接

7、Logging日志

8、Publish发布

9、Subscribe订阅

10、Request Response 请求响应

11、Send Receive 发送接收

12、Topic Based Routing 主题路由

13、Controlling Queue names 控制队列名称

14、Polymorphic Publish and Subscribe 多态发布和订阅

15、Versioning Messages 版本化消息

16、Publisher Confirms 发布者确认

17、Scheduling Events with Future Publish 用Future Publish发布日程事件

17B、Support for Delayed Messages Plugin 对延迟消息插件的支持

18、Auto Subscriber 自动订阅

19、非泛型发布&订阅扩展方法

20、Error Conditions 错误情况

21、Re Submitting Error Messages With EasyNetQ.Hosepipe 使用Hosepipe重新提交错误消息

22、The Advanced API 高级API

23、Cluster Support 集群支持

24、Wiring up EasyNetQ with TopShelf and Windsor 让EasyNetQ、TopShelf 和Windsor 一起使用

25、Replacing EasyNetQ Components 替换EasyNetQ组件

26、Using Alternative DI Containers 替换DI容器

图解RAID 0, RAID 1, RAID 5, RAID 10 - 曾先森在努力 - 博客园

mikel阅读(944)

来源: 图解RAID 0, RAID 1, RAID 5, RAID 10 – 曾先森在努力 – 博客园

   RAID(Redundant Array of Independent Disk 独立冗余磁盘阵列)技术是加州大学伯克利分校1987年提出,最初是为了组合小的廉价磁盘来代替大的昂贵磁盘,同时希望磁盘失效时不会使对数据的访问受损 失而开发出一定水平的数据保护技术。RAID就是一种由多块廉价磁盘构成的冗余阵列,在操作系统下是作为一个独立的大型存储设备出现。RAID可以充分发 挥出多块硬盘的优势,可以提升硬盘速度,增大容量,提供容错功能够确保数据安全性,易于管理的优点,在任何一块硬盘出现问题的情况下都可以继续工作,不会 受到损坏硬盘的影响。

RAID 为 Redundant Array of Indepent Disks (独立磁盘冗余阵列) 的缩写,最常用的四种RAID为 RAID 0、RAID 1、RAID 5、RAID 10,下面以图解的方式解释这四种RAID的特点和区别。

 

在后面的图示中,用到以下标识:

  • A,B,C,D,E和F – 表示数据块
  • p1,p2,p3 – 表示奇偶校验信息块

 

RAID 0

RAID 0的特点:

  • 最少需要两块磁盘
  • 数据条带式分布
  • 没有冗余,性能最佳(不存储镜像、校验信息)
  • 不能应用于对数据安全性要求高的场合

 

RAID 1

以下为RAID 1的特点:

  • 最少需要2块磁盘
  • 提供数据块冗余
  • 性能好

 

RAID 5

RAID 5特点:

  • 最少3块磁盘
  • 数据条带形式分布
  • 以奇偶校验作冗余
  • 适合多读少写的情景,是性能与数据冗余最佳的折中方案

 

RAID 10

 

RAID 10(又叫RAID 1+0)特点:

  • 最少需要4块磁盘
  • 先按RAID 0分成两组,再分别对两组按RAID 1方式镜像
  • 兼顾冗余(提供镜像存储)和性能(数据条带形分布)
  • 在实际应用中较为常用

RAID 0即Data Stripping(数据分条技术)。整个逻辑盘的数据是被分条(stripped)分布在多个物理磁盘上,可以并行读/写,提供最快的速度,但没有冗余能力。要求至少两个磁盘。我们通过RAID 0可以获得更大的单个逻辑盘的容量,且通过对多个磁盘的同时读取获得更高的存取速度。RAID 0首先考虑的是磁盘的速度和容量,忽略了安全,只要其中一个磁盘出了问题,那么整个阵列的数据都会不保了。

问:RAID0至少几块盘?
答:RAID0最少要两块硬盘才能实现。

RAID 1
RAID 1,又称镜像方式,也就是数据的冗余。在整个镜像过程中,只有一半的磁盘容量是有效的(另一半磁盘容量用来存放同样的数据)。同RAID 0相比,RAID 1首先考虑的是安全性,容量减半、速度不变。

问:RAID1至少几块盘?
答:RAID1最少要两块硬盘才能实现。

RAID 0+1(RAID 10)
为了达到既高速又安全,出现了RAID 10(或者叫RAID 0+1),可以把RAID 10简单地理解成由多个磁盘组成的RAID 0阵列再进行镜像。

问:RAID0+1至少几块硬盘才能实现?
答:RAID0+1至少需要4块盘。

RAID 3和RAID 5
RAID 3和RAID 5都是校验方式。RAID 3的工作方式是用一块磁盘存放校验数据。由于任何数据的改变都要修改相应的数据校验信息,存放数据的磁盘有好几个且并行工作,而存放校验数据的磁盘只有一个,这就带来了校验数据存放时的瓶颈。RAID 5的工作方式是将各个磁盘生成的数据校验切成块,分别存放到组成阵列的各个磁盘中去,这样就缓解了校验数据存放时所产生的瓶颈问题,但是分割数据及控制存放都要付出速度上的代价。

问:RAID5需要几块硬盘?为什么损失一个盘的容量?
答:至少3块。
RAID5把数据和相对应的奇偶校验信息存储到组成RAID5的各个磁盘上,并且奇偶校验信息和相对应的数据分别存储于不同的磁盘上,其中任意N-1块磁盘上都存储完整的数据,也就是说有相当于一块磁盘容量的空间用于存储奇偶校验信息。因此当RAID5的一个磁盘发生损坏后,不会影响数据的完整性,从而保证了数据安全。当损坏的磁盘被替换后,RAID还会自动利用剩下奇偶校验信息去重建此磁盘上的数据,来保持RAID5的高可靠性。

 

努力奋斗的小墨鱼 —- http://www.cnblogs.com/WayneZeng/

RAID阵列基础知识_FlyWine的博客-CSDN博客_raid备份基础知识

mikel阅读(1022)

来源: RAID阵列基础知识_FlyWine的博客-CSDN博客_raid备份基础知识

RAID阵列基础知识

独立硬盘冗余阵列 (RAID, Redundant Array of Independent Disks),旧称廉价磁盘冗余阵列(Redundant Array of Inexpensive Disks),简称磁盘阵列

RAID的种类

这里我们只介绍比较常用的RAID类型,详细可查看维基百科

RAID-0 (等量模式,stripe): 性能最佳

  • 这种模式如果使用相同型号与容量的磁盘来组成时,效果最佳。这种模式的RAID会将磁盘先切出等量的区块(举例来说:4KB),然后当一个文件要写入RAID时,该文件会依据块的大小切割好,之后再依序放到各个磁盘里面去。由于每个磁盘会交错存放数据,因此当你的数据要写入RAID时,数据会被等量放置在各个磁盘上面。

    例如:你有两块硬盘组成RAID-0,当你有100MB的数据要写入时,每个磁盘会被分配到50MB的存储量。RAID-0的示意图:

    这里写图片描述

  • RAID-0需要自行负责数据损毁的风险,由上图我们知道文件是被切割成为适合每块磁盘分区区块的大小,然后再依序放置到各个磁盘中,所以如果一个磁盘坏了,那么数据将缺少一块,此时这个文件就损毁了。由于每个文件都是这样存放,因此RAID-0只要任何一块磁盘损毁,在RAID-0上面的所有数据都会丢失而无法读取。

RAID-1 (镜像模式,mirror): 完整备份

  • 这种模式也是需要相同的磁盘容量的,最好是一模一样的磁盘。如果是不同容量的磁盘组成RAID-1时,那么总容量将以最小的那一块磁盘为主!这种模式主要是让同一份数据完整保存在两块磁盘上面。举例来说,如果有一个100MB的文件,且我仅有两块磁盘组成RAID-1时,那么这两块磁盘将会同步写入100MB到他们的存储空间去。因此,整体RAID的容量就几乎少了50%.

这里写图片描述

  • 由于两块磁盘内的数据一模一样,所以任何一块硬盘损毁时,你地数据还是可以完整保留下来。所以我们可以说,RAID-1最大的优点大概就是在于数据的备份。不过由于磁盘容量有一半用在备份,因此总容量会是全部磁盘容量的一半而已。RAID-1的写入性能不佳,不过读取的性能则是还可以。这是因为数据有两份在不同的磁盘上面,如果多个进程在读取同一条数据时,RAID会自行取得最佳的读取平衡。

RAID 0 + 1

  • RAID-0的性能佳但是数据不安全,RAID-1的数据安全但是性能不佳,那么能不能将这两者整合起来设置RAID呢?那就是RAID 0 + 1或RAID 1 + 0。所谓的RAID 0 + 1就是先让两块硬盘组成RAID-0,并且这样的设计有两组;然后将这两组RAID-0再组成一组RAID-1,这就是RAID 0 + 1,反过来说,RAID 1 + 0 就是先组成RAID-1再组成RAID-0的意思。
  • 下图中,Disk 0 + Disk 1组成第一组RAID 0,Disk 2 + Disk 3组成第二组RAID 0,然后这两组再整合成一组RAID-1。如果我有100MB的数据要写,则由于RAID 1的关系,两组RAID-0 都会写入100MB,但是由于RAID-0的关系,因此每块磁盘仅会写入50MB而已。如此一来,无论哪一组的RAID-0的磁盘损毁,只要另外一组RAID-0还存在,那么就能够通过RAID-1的机制来恢复数据。

这里写图片描述

  • 由于具有RAID-0的优点,所以性能得以提升,由于具有RAID-1的优点,所以数据得以备份。但是也由于RAID-1的缺点,所以总容量会少一半用来作为备份。

Raid 1 + 0

  • 下图Disk 0 + Disk 1先组成第一组RAID-1,Disk 2 + Disk 3再组成一组RAID-1,然后这两组再整合成为一组RAID-0。如果我有100MB数据要写,首先会将数据分割,那么有50MB的数据会写入第一组RAID-1中,另外50MB写入第二组RAID-1中,由于RAID-1的特点,Disk 0 和 Disk 1都会写入同样的50MB数据,另外一组RAID-1也是同样。因此,的那个我饿能其中一块硬盘坏掉是没有关系的,但是如果第一组RAID-1(disk 0 + Disk 1)两块硬盘全部损坏,那么数据将会丢失。再比如Disk 0 和Disk 2两块硬盘损坏,由于RAID-1所以数据还是可以恢复的。

这里写图片描述

Spare Disk: 预备磁盘的功能

  • 磁盘阵列的磁盘损坏时,就得将要坏的磁盘拔除,然后换上一块新的磁盘。换成新磁盘并且顺利启动磁盘阵列后,磁盘阵列就会开始重建原本坏掉的那块磁盘数据到新的磁盘中去,然后你磁盘阵列上的数据就复原了。这就是磁盘阵列的优点,不过我们还是要手动插拔磁盘,此时通常得要关机才能这么做。
  • 为了让系统可以实时地在坏掉硬盘时主动重建,因此就需要预备磁盘(spare disk)的辅助。所谓的spare disk就是一块或者多块没有包含原本磁盘阵列等级中的磁盘,这块磁盘平时并不会被磁盘阵列所使用,当磁盘阵列有任何一块磁盘损坏时,这块spare disk会被主动拉进磁盘阵列中,并将坏掉的那块磁盘移除磁盘阵列,然后重建数据系统。

硬阵列和软阵列

  • 硬件磁盘阵列(hardware RAID)
    硬磁盘阵列就是通过磁盘阵列卡来完成阵列的。磁盘阵列卡上面有一块专门的芯片处理RAID任务,因此在性能方面会比较好。并且目前中高等的磁盘阵列卡都支持热插拔,即在不关机的情况下更换损坏的硬盘。
  • 软件磁盘阵列(software RAID)
    软件磁盘阵列主要是通过软件来仿真硬件磁盘阵列,因此会损耗较多的系统资源,比如说CPU的运算于I/O总线的资源等,不过我们现在的计算机已经速度很快了,因此这些限制现可以忽略不计了。

宝塔面板(BT) – Docker管理器安装OneIndex并实现反-代 - 大鸟博客

mikel阅读(1480)

来源: 宝塔面板(BT) – Docker管理器安装OneIndex并实现反-代 – 大鸟博客

大鸟前面也说了不少关于宝塔面板docker的教程,每次前言说很多,我也觉得啰嗦,今天大鸟这篇文章说一下如何用docker管理安装OneIndex并实现反代。

宝塔面板(BT) – Docker管理器安装OneIndex并实现反-代

不占用服务器空间,不走服务器流量,直接列出 OneDrive 目录,文件直链下载。

整个项目基础宝塔面板6.9.3,Docker管理器2.0,宝塔面板自己安装,前提还需要一台VPS,这些自己去折腾吧。

一:Docker管理器2.0

我们在宝塔面板的软件商店里面安装Docker管理器2.0,只需要安装一个nginx即可,不需要完整的lnmp或者lamp环境。

宝塔面板(BT) – Docker管理器安装OneIndex并实现反-代

github:https://github.com/donwa/oneindex

获取镜像:yinaoxiong/oneindex

这里我们只需要一个镜像就可以了。

二:docker安装

在宝塔面板的软件商店里面打开Docker管理器2.0,切换到镜像管理,并点击获取镜像,在官方库中输入yinaoxiong/oneindex并点击获取镜像。

2.1获取镜像

宝塔面板(BT) – Docker管理器安装OneIndex并实现反-代

获取镜像后,我们可以在镜像列表中看到:

宝塔面板(BT) – Docker管理器安装OneIndex并实现反-代

2.2创建容器

容器列表→创建容器,目录映射,执行命令都不用填写了,端口映射如下是映射80端口到服务器的某一端口,大鸟这里是映射到了8083端口:

宝塔面板(BT) – Docker管理器安装OneIndex并实现反-代

之后提交之后,就可以测试:<IP:8083>(自定义的任意端口号)来访问验证是否已经成功。

三:创建反-代

接下来我们来需要反向代理来实现访问域名直接进入OneIndex,新建网站,如图:

宝塔面板(BT) – Docker管理器安装OneIndex并实现反-代

注意PHP版本我们选择纯静态,可以在你OneIndex设置中去除掉网址中的/?/而不需要配置伪静态。

创建好之后我们进入网站设置,切换至反向代理面板,如图:

宝塔面板(BT) – Docker管理器安装OneIndex并实现反-代

 

  • 代理名称:任意
  • 目标URL:http://ip:8083(自定义的任意端口号)
  • 发送域名:留空或填写进行访问的域名

 

点击提交即可完成反向代理,之后就可以用域名访问oneindex了。

四:系统安装

大鸟用的8083端口,我们来访问开始安装oneindex,因为已经反代了域名,所以可以用域名访问了安装了:

宝塔面板(BT) – Docker管理器安装OneIndex并实现反-代

下一步,需要填入client_id和client_secret,点击蓝色的按钮自动跳转!

宝塔面板(BT) – Docker管理器安装OneIndex并实现反-代

然后我们需要登录,登录之后会自动获取你的应用机密,我们复制到oneindex中,之后点击【知道了,返回到快速启动】

宝塔面板(BT) – Docker管理器安装OneIndex并实现反-代

点击【知道了,返回到快速启动】之后,会再次自动获取应用ID

宝塔面板(BT) – Docker管理器安装OneIndex并实现反-代

app id复制到应用id中,之后下一步即可,我们需要绑定账号:

宝塔面板(BT) – Docker管理器安装OneIndex并实现反-代

到这里的时候出现【无法访问此网站 找不到 ju.tn 的服务器 IP 地址】的错误,我们这样解决:

  1. youdomain.com/?install&code=
  2. https://bt.woweiwang.org/?install&code=M3453553-67a4c-c435345-dc948-115435355

这样我们就可以安装成功了,如图:

宝塔面板(BT) – Docker管理器安装OneIndex并实现反-代

我们登录后台看看:

宝塔面板(BT) – Docker管理器安装OneIndex并实现反-代

五:访问

虽然安装完成,但是访问的时候还是会有点问题的,我们用域名访问还是会出现安装的界面,如何修改呢。

此处为隐藏的内容!
发表评论并刷新,方可查看

这样我们就可以直接用域名打开oneindex网盘了:

 

宝塔面板(BT) – Docker管理器安装OneIndex并实现反-代

六:小结

这样就通过docker部署了oneindex,并且通过反-代实现了域名访问,如果你要开启SSL用宝塔面板的自带的申请即可。

宝塔面板安装部署测试的项目还是很方便的,搜索福利用百度,搜索学术有谷歌!这话不知道是谁说的……