.Net高并发解决思路

首先在windows上安装好Redis,RabbitMQ

这里写图片描述


Redis-cli使用示例

这里写图片描述

这里写图片描述


ModelContext.cs代码:


    public class ModelContext : DbContext
    {
        //您的上下文已配置为从您的应用程序的配置文件(App.config 或 Web.config)
        //连接字符串。
        public ModelContext()
            : base("name=default")
        {
        }

        public virtual DbSet<Person> Person { get; set; }
    }

    public class Person
    {
        public int Id { get; set; }
        public string Id2 { get; set; }
        public string Name { get; set; }
    }

在 Package Manager Console 下运行命令 Enable-Migrations
 这个命令将在项目下创建文件夹 Migrations

The Configuration class 这个类允许你去配置如何迁移,对于本文将使用默认的配置(在本文中因为只有一个 Context, Enable-Migrations 将自动对 context type 作出适配);
An InitialCreate migration (本文为201702220232375_20170222.cs)这个迁移之所以存在是因为我们之前用 Code First 创建了数据库, 在启用迁移前,scaffolded migration 里的代码表示在数据库中已经创建的对象,本文中即为表 Person(列 Id 和 Name). 文件名包含一个 timestamp 以便排序(如果之前数据库没有被创建,那么 InitialCreate migration 将不会被创建,相反,当我们第一次调用 Add-Migration 的时候所有表都将归集到一个新的 migration 中)

多个实体锁定同一数据库

当使用 EF6 之前的版本时,只会有一个 Code First Model 被用来生成/管理数据库的 Schema, 这将导致每个数据库只会有一张 __MigrationsHistory 表,从而无法辨别实体与模型的对应关系。

从 EF6 开始,Configuration 类将会包含一个 ContextKey 属性,它将作为每一个 Code First Model 的唯一标识符, __MigrationsHistory 表中一个相应地的列允许来自多个模型(multiple models)的实体共享表(entries),默认情况下这个属性被设置成 context 的完全限定名。

定制化迁移

在 Package Manager Console 中运行命令 Add-Migration XXXXXXXXX
生成的迁移如下

 public partial class _20170222 : DbMigration
    {
        public override void Up()
        {
            CreateTable(
                "dbo.People",
                c => new
                    {
                        Id = c.Int(nullable: false, identity: true),
                        Id2 = c.String(),
                        Name = c.String(),
                    })
                .PrimaryKey(t => t.Id);

        }

        public override void Down()
        {
            DropTable("dbo.People");
        }
    }

Configuration.cs代码:

internal sealed class Configuration : DbMigrationsConfiguration<EF.ModelContext>
    {
        public Configuration()
        {
            AutomaticMigrationsEnabled = false;
        }

        protected override void Seed(EF.ModelContext context)
        {

        }
    }

我们对迁移做些更改:

以下是本项目无关的其他示例:

namespace MigrationsDemo.Migrations
{
    using System;
    using System.Data.Entity.Migrations;

    public partial class AddPostClass : DbMigration
    {
        public override void Up()
        {
            CreateTable(
                "dbo.Posts",
                c => new
                    {
                        PostId = c.Int(nullable: false, identity: true),
                        Title = c.String(maxLength: 200),
                        Content = c.String(),
                        BlogId = c.Int(nullable: false),
                    })
                .PrimaryKey(t => t.PostId)
                .ForeignKey("dbo.Blogs", t => t.BlogId, cascadeDelete: true)
                .Index(t => t.BlogId)
                .Index(p => p.Title, unique: true);

            AddColumn("dbo.Blogs", "Rating", c => c.Int(nullable: false, defaultValue: 3));
        }

        public override void Down()
        {
            DropIndex("dbo.Posts", new[] { "Title" });
            DropForeignKey("dbo.Posts", "BlogId", "dbo.Blogs");
            DropIndex("dbo.Posts", new[] { "BlogId" });
            DropColumn("dbo.Blogs", "Rating");
            DropTable("dbo.Posts");
        }
    }
}

在 Package Manager Console 中运行命令 Update-Database –Verbose


消费者端,用来把消息队列里的数据写入数据库

MqHelper.cs代码:

public class MqHelper
    {
        private static IConnection _connection;

        /// <summary>
        /// 获取连接对象
        /// </summary>
        /// <returns></returns>
        public static IConnection GetConnection()
        {
            if (_connection != null) return _connection;
            _connection = GetNewConnection();
            return _connection;
        }

        public static IConnection GetNewConnection()
        {

            //从工厂中拿到实例 本地host、用户admin
            var factory = new ConnectionFactory()
            {
                HostName = "localhost",
                UserName = "guest",
                Password = "guest",

            };

            _connection = factory.CreateConnection();
            return _connection;

        }
    }

Program.cs代码:

  internal class Program
    {
        private static void Main(string[] args)
        {
            using (var channel = MqHelper.GetConnection().CreateModel())
            {
                //参数有 queue名字 是否持久化 独占的queue(仅供此连接) 不使用时是否自动删除 其他参数
                channel.QueueDeclare("NET", true, false, false, null);

                //我们要告诉服务器从队列里推送消息,因为消息是异步发送的,所以我们需要提供一个回调事件EventingBasicConsumer,用于处理接收到的消息。
                //这就是 EventingBasicConsumer.Received 事件处理程序做的事。
                var consumber = new EventingBasicConsumer(channel);

                //QoS = quality-of-service, 顾名思义,服务的质量。
                //代码第一个参数是可接收消息的大小的,但是似乎在客户端2.8.6版本中它必须为0,即使:不受限制。
                //如果不输0,程序会在运行到这一行的时候报错,说还没有实现不为0的情况。
                //第二个参数是处理消息最大的数量。举个例子,如果输入1,那如果接收一个消息,但是没有应答,则客户端不会收到下一个消息,消息只会在队列中阻塞。
                //如果输入3,那么可以最多有3个消息不应答,如果到达了3个,则发送端发给这个接收方得消息只会在队列中,而接收方不会有接收到消息的事件产生。
                //总结说,就是在下一次发送应答消息前,客户端可以收到的消息最大数量。
                //第三个参数则设置了是不是针对整个Connection的,因为一个Connection可以有多个Channel,如果是false则说明只是针对于这个Channel的。

                //Fair dispatch 公平分发
                //通过 BasicQos 方法设置prefetchCount = 1。这样RabbitMQ就会使得每个Consumer在同一个时间点最多处理一个Message。换句话说,在接收到该Consumer的ack前,他它不会将新的Message分发给它。 设置方法如下:
                channel.BasicQos(0, 1, false);

                consumber.Received += (sender, e) =>
                {
                    try
                    {
                        var user = JsonConvert.DeserializeObject<User>(Encoding.UTF8.GetString(e.Body));

                        //Redis INCR命令用于将键的整数值递增1。如果键不存在,则在执行操作之前将其设置为0。 如果键包含错误类型的值或包含无法表示为整数的字符串,则会返回错误。此操作限于64位有符号整数。
                        var flag = RedisHelper.GetRedisClient().Incr(user.Id.ToString());
                        if (flag == 1)
                        {
                            //用户的第一次请求,为有效请求
                            //下面开始入库,这里使用List做为模拟
                            Console.WriteLine(string.Format("{0}标识为{1} {2}", user.Id, flag, user.Name));
                            var dbContext = new ModelContext();
                            dbContext.Person.Add(new Person() {Id2 = user.Id.ToString(), Name = user.Name});
                            Task ts = dbContext.SaveChangesAsync();
                            ts.Wait();
                            //添加入库标识
                            RedisHelper.GetRedisClient().Incr(string.Format("{0}入库", user.Id.ToString()));

                            Console.WriteLine("入库成功");
                        }

                        //用户的N次请求,为无效请求
                        channel.BasicAck(e.DeliveryTag, false); // 回发ACK 参数 tag 是否多个     //对message进行确认  
                    }
                    catch (Exception ex)
                    {
                        File.AppendAllText(string.Format("{0}/bin/log.txt", System.AppDomain.CurrentDomain.BaseDirectory), ex.Message);
                    }
                };
                Console.WriteLine("开始工作");


                // 如果 channel.BasicConsume 中参数 noAck 设置为 false,必须加上消息确认语句
                // Message acknowledgment(消息确认机制作用)
                //打开应答机制  
                //no_ack 的用途:确保 message 被 consumer “成功”处理了。
                //这里“成功”的意思是,(在设置了 no_ack=false 的情况下)只要 consumer 手动应答了 Basic.Ack ,就算其“成功”处理了。

                //情况一:no_ack=true (此时为自动应答)
                //在这种情况下,consumer 会在接收到 Basic.Deliver + Content-Header + Content-Body 之后,立即回复 Ack 。
                //而这个 Ack 是 TCP 协议中的 Ack 。此 Ack 的回复不关心 consumer 是否对接收到的数据进行了处理,当然也不关心处理数据所需要的耗时。
                //情况二:no_ack=false (此时为手动应答)
                //在这种情况下,要求 consumer 在处理完接收到的 Basic.Deliver + Content-Header + Content-Body 之后才回复 Ack 。
                //而这个 Ack 是 AMQP 协议中的 Basic.Ack 。此 Ack 的回复是和业务处理相关的,所以具体的回复时间应该要取决于业务处理的耗时。
                channel.BasicConsume("NET", false, consumber);
                Console.ReadKey();
            }
        }
    }

模拟并发的MVC网站,写入队列

HomeController.cs代码

 public class HomeController : Controller
    {
        // GET: Controller
        public ActionResult Index()
        {
            return View();
        }

        /// <summary>
        /// 抢单接口
        /// </summary>
        /// <param name="user"></param>
        /// <returns></returns>
        [HttpPost]
        public ActionResult GrabSingle(User user)
        {
            //使用后台任务
            //BackgroundJob.Enqueue(() => MqPublish.AddQueue(user));
            MqPublish.AddQueue(user);

            //MqPublish.AddQueue(user);
            return Json(new { Status = "OK" });
        }

        /// <summary>
        /// 获取数量
        /// </summary>
        /// <returns></returns>
        [HttpGet]
        public async Task<ActionResult> GetCount()
        {
            using (var dbcontext = new ModelContext())
            {
                return Json(new { Count = await dbcontext.Person.CountAsync() }, JsonRequestBehavior.AllowGet);
            }

        }
    }

MqPublish.cs类

 /// <summary>
    /// 发布者
    /// </summary>
    public class MqPublish
    {
        public const string QueueName = "NET";
        public static IList<User> UserList = new List<User>();


        /// <summary>
        /// 添加到队列
        /// </summary>
        public static void AddQueue(User user)
        {
            //创建一个channel
            using (var channel = MqHelper.GetNewConnection().CreateModel())
            {
                //json序列化
                var bytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(user));

                //channel.basicPublish(exchangeName, routingKey, null, messageBodyBytes);
                //参数说明:
                //要发出的交换机名字 
                //路由关键字 
                //是否强制(设置为true时,找不到收的人时可以通过returnListener返回) 
                //是否立即(其实rabbitmq不支持) 
                //其他属性 
                //消息主体
                channel.BasicPublish(String.Empty, QueueName, null, bytes); 
            }
        }
    }

RedisHelper.cs代码:

 /// <summary>
    /// Redis帮助类
    /// </summary>
    public class RedisHelper
    {

        public static RedisClient GetRedisClient()
        {

            return new RedisClient("127.0.0.1", 6379,"123456");

        }
    }

Index.cshtml内容:


@{
    Layout = null;
}

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Index</title>
    <link href="//cdn.bootcss.com/bootstrap/4.0.0-alpha.6/css/bootstrap.min.css" rel="stylesheet">
    <script src="//cdn.bootcss.com/jquery/3.1.1/jquery.min.js"></script>
    <script>
        var uarray = [
            { Id: 1, Name: "小明" }, { Id: 2, Name: "张三" }, { Id: 3, Name: "李四" }, { Id: 4, Name: "王五" },
            { Id: 5, Name: "赵六" }, { Id: 6, Name: "钱八" }, { Id: 7, Name: "小红" }, { Id: 8, Name: "小紫" },
            { Id: 9, Name: "小蓝" }, { Id: 10, Name: "老王" }
        ];

        function btnClick() {
            //构造并发模拟
            var flag;
            for (var i = 0; i < 1000; i++) {
                flag = parseInt(Math.random() * (9 - 0 + 1) + 0);
                $.ajax({
                    type: "post",
                    url: "/Home/GrabSingle",
                    data: uarray[flag]
                });
            }
        }


        function GetCount() {
            $.ajax({
                type: "get",
                url: "/Home/GetCount",
                dataType: "json",
                success: function (data) {
                    alert(data.Count + "个");
                }
            });
        }
    </script>
</head>
<body>
    <h2>.Net高并发解决思路</h2>
    <hr />
    <button class="btn btn-primary" onclick="btnClick()">点击抢单</button>
    <button class="btn btn-primary" onclick="GetCount()">查看入库数量</button>
</body>
</html>

Startup.cs代码:

using System;
using System.Threading.Tasks;
using Hangfire;
using Hangfire.MemoryStorage;
using Microsoft.Owin;
using NetHigh.RabbitMq;
using Owin;

[assembly: OwinStartup(typeof(NetHigh.Startup))]
namespace NetHigh
{
    public partial class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            GlobalConfiguration.Configuration.UseMemoryStorage();
            app.UseHangfireServer();
            app.UseHangfireDashboard("/hangfire");


            //添加三个后台任务也就是三个consumer
            //BackgroundJob.Enqueue(() => MqConsumber.ConsumeQueue());
            //BackgroundJob.Enqueue(() => MqConsumber.ConsumeQueue());
            //BackgroundJob.Enqueue(() => MqConsumber.ConsumeQueue());
        }
    }
}

Web.config

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    <section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
  </configSections>
  <system.web>
    <compilation debug="true" targetFramework="4.5" />
    <httpRuntime targetFramework="4.5" />
  </system.web>

  <entityFramework>
    <defaultConnectionFactory type="System.Data.Entity.Infrastructure.LocalDbConnectionFactory, EntityFramework">
      <parameters>
        <parameter value="mssqllocaldb" />
      </parameters>
    </defaultConnectionFactory>
    <providers>
      <provider invariantName="System.Data.SqlClient" type="System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework.SqlServer" />
    </providers>
  </entityFramework>
  <connectionStrings>
    <add name="default" connectionString="Server=.;User Id=sa;PassWord=123456;DataBase=Person" providerName="System.Data.SqlClient" />
  </connectionStrings>
</configuration>

运行结果如图:

这里写图片描述


这里写图片描述


运行消费者端(控制台运用程序)
这里写图片描述


这里写图片描述

展开阅读全文

没有更多推荐了,返回首页