限速设计及实现(微服务架构)

[图片上传失败...(image-c9b470-1662870789547)]

开过车或者坐过车的都对限速是深恶痛绝吧,不过为了大家安全,忍一忍吧。系统究竟是如何判断车主是否超速那,本文主要介绍这个系统到底是如何设计的。本文基于微服务进行设计。

关于微服务,在微服务初探(0)微服务-node.js中对微服务有简单的介绍。通过这篇文章,可以进一步了解微服务。

注:本文内容参考《Dapr For .NET developer》,专有名词保留英文原文,不做翻译。

Dapr

The Distributed Application Runtime (Dapr) provides APIs that simplify microservice connectivity. Whether your communication pattern is service to service invocation or pub/sub messaging, Dapr helps you write resilient and secured microservices.

Building blocks抽象了分布式应用能力的实现,service只需要和Building blocks进行交互即可。
[图片上传失败...(image-5830d9-1662870789547)]

Dapr的现在和安装可参考Install the Dapr CLI

限速设计

[图片上传失败...(image-149a63-1662870789547)]
这里面fine是罚单,罚款的意思。

VehicleState包括了车牌号,进入限速区域(Cameral)时间,退出限速区域(Cameral)时间。

public record struct VehicleState
{
    public string LicenseNumber { get; init; }
    public DateTime EntryTimestamp { get; init; }
    public DateTime? ExitTimestamp { get; init; }

    public VehicleState(string licenseNumber, DateTime entryTimestamp, DateTime? exitTimestamp = null)
    {
        this.LicenseNumber = licenseNumber;
        this.EntryTimestamp = entryTimestamp;
        this.ExitTimestamp = exitTimestamp;
    }
}

据此就可以计算出平均时速,然后再与规定的时速进行比较,如果超过了,不好意思,您将收到一封邮件,也可以用短信等方式发送给车主。而车主的邮件信息通过VehicleRegistrationService来获取。

车辆信息的结构体定义如下:

public record struct VehicleInfo(string VehicleId, string Brand, string Model, string OwnerName, string OwnerEmail);

代码实现

windows平台下需要将start-maildev.ps1修改为,修改参考maildev/maildev

docker run -d -p 4000:1080 -p 4025:1025 --name dtc-maildev maildev/maildev

-d: 后台运行容器,并返回容器ID;

(1) CameralSimulation.cs

模拟进入和退出代码:

// simulate entry
DateTime entryTimestamp = DateTime.Now;
var vehicleRegistered = new VehicleRegistered
{
    Lane = _camNumber,
    LicenseNumber = GenerateRandomLicenseNumber(),
    Timestamp = entryTimestamp
};
await _trafficControlService.SendVehicleEntryAsync(vehicleRegistered);
Console.WriteLine($"Simulated ENTRY of vehicle with license-number {vehicleRegistered.LicenseNumber} in lane {vehicleRegistered.Lane}");

// simulate exit
TimeSpan exitDelay = TimeSpan.FromSeconds(_rnd.Next(_minExitDelayInS, _maxExitDelayInS) + _rnd.NextDouble());
Task.Delay(exitDelay).Wait();
vehicleRegistered.Timestamp = DateTime.Now;
vehicleRegistered.Lane = _rnd.Next(1, 4);
await _trafficControlService.SendVehicleExitAsync(vehicleRegistered);
Console.WriteLine($"Simulated EXIT of vehicle with license-number {vehicleRegistered.LicenseNumber} in lane {vehicleRegistered.Lane}");

跟踪 _trafficControlService.SendVehicleEntryAsync

 public async Task SendVehicleEntryAsync(VehicleRegistered vehicleRegistered)
 {
     var eventJson = JsonSerializer.Serialize(vehicleRegistered);
     var message = new MqttApplicationMessageBuilder()
         .WithTopic("trafficcontrol/entrycam")
         .WithPayload(Encoding.UTF8.GetBytes(eventJson))
         .WithAtMostOnceQoS()
         .Build();
     await _client.PublishAsync(message, CancellationToken.None);
 }

使用Input Binding来发布消息。可追踪到消息的订阅者是trafficcontrolservice。
(2)TrafficController.cs

[HttpPost("entrycam")]
 public async Task<ActionResult> VehicleEntryAsync(VehicleRegistered msg)
 {
     try
     {
         // log entry
         _logger.LogInformation($"ENTRY detected in lane {msg.Lane} at {msg.Timestamp.ToString("hh:mm:ss")} " +
             $"of vehicle with license-number {msg.LicenseNumber}.");

         // store vehicle state
         var vehicleState = new VehicleState(msg.LicenseNumber, msg.Timestamp, null);
         await _vehicleStateRepository.SaveVehicleStateAsync(vehicleState);

         return Ok();
     }
     catch (Exception ex)
     {
         _logger.LogError(ex, "Error occurred while processing ENTRY");
         return StatusCode(500);
     }
 }

跟踪_vehicleStateRepository.SaveVehicleStateAsync(vehicleState)

public async Task SaveVehicleStateAsync(VehicleState vehicleState)
{
    await _daprClient.SaveStateAsync<VehicleState>(
        DAPR_STORE_NAME, vehicleState.LicenseNumber, vehicleState);
}

使用state management building block来保存VehicleState。

同理可知退出时会调用trafficcontrolservice的VehicleExitAsync:

 [HttpPost("exitcam")]
    public async Task<ActionResult> VehicleExitAsync(VehicleRegistered msg, [FromServices] DaprClient daprClient)
    {
        try
        {
            // get vehicle state
            var state = await _vehicleStateRepository.GetVehicleStateAsync(msg.LicenseNumber);
            if (state == default(VehicleState))
            {
                return NotFound();
            }

            // log exit
            _logger.LogInformation($"EXIT detected in lane {msg.Lane} at {msg.Timestamp.ToString("hh:mm:ss")} " +
                $"of vehicle with license-number {msg.LicenseNumber}.");

            // update state
            var exitState = state.Value with { ExitTimestamp = msg.Timestamp };
            await _vehicleStateRepository.SaveVehicleStateAsync(exitState);

            // handle possible speeding violation
            int violation = _speedingViolationCalculator.DetermineSpeedingViolationInKmh(exitState.EntryTimestamp, exitState.ExitTimestamp.Value);
            if (violation > 0)
            {
                _logger.LogInformation($"Speeding violation detected ({violation} KMh) of vehicle" +
                    $"with license-number {state.Value.LicenseNumber}.");

                var speedingViolation = new SpeedingViolation
                {
                    VehicleId = msg.LicenseNumber,
                    RoadId = _roadId,
                    ViolationInKmh = violation,
                    Timestamp = msg.Timestamp
                };

                // publish speedingviolation (Dapr publish / subscribe)
                await daprClient.PublishEventAsync("pubsub", "speedingviolations", speedingViolation);
            }

            return Ok();
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error occurred while processing EXIT");
            return StatusCode(500);
        }
    }

如果存在超速的话会调用:

await daprClient.PublishEventAsync("pubsub", "speedingviolations", speedingViolation)

根据配置文件

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: pubsub
  namespace: dapr-trafficcontrol
spec:
  type: pubsub.rabbitmq
  version: v1
  metadata:
  - name: host
    value: "amqp://localhost:5672"
  - name: durable
    value: "false"
  - name: deletedWhenUnused
    value: "false"
  - name: autoAck
    value: "false"
  - name: reconnectWait
    value: "0"
  - name: concurrency
    value: parallel
scopes:
  - trafficcontrolservice
  - finecollectionservice

可追踪到消息的订阅者是finecollectionservice。
(3)FineCollectionService

    [Topic("pubsub", "speedingviolations")]
    [Route("collectfine")]
    [HttpPost()]
    public async Task<ActionResult> CollectFine(SpeedingViolation speedingViolation, [FromServices] DaprClient daprClient)
    {
        decimal fine = _fineCalculator.CalculateFine(_fineCalculatorLicenseKey!, speedingViolation.ViolationInKmh);

        // get owner info (Dapr service invocation)
        var vehicleInfo = _vehicleRegistrationService.GetVehicleInfo(speedingViolation.VehicleId).Result;

        // log fine
        string fineString = fine == 0 ? "tbd by the prosecutor" : $"{fine} Euro";
        _logger.LogInformation($"Sent speeding ticket to {vehicleInfo.OwnerName}. " +
            $"Road: {speedingViolation.RoadId}, Licensenumber: {speedingViolation.VehicleId}, " +
            $"Vehicle: {vehicleInfo.Brand} {vehicleInfo.Model}, " +
            $"Violation: {speedingViolation.ViolationInKmh} Km/h, Fine: {fineString}, " +
            $"On: {speedingViolation.Timestamp.ToString("dd-MM-yyyy")} " +
            $"at {speedingViolation.Timestamp.ToString("hh:mm:ss")}.");

        // send fine by email (Dapr output binding)
        var body = EmailUtils.CreateEmailBody(speedingViolation, vehicleInfo, fineString);
        var metadata = new Dictionary<string, string>
        {
            ["emailFrom"] = "noreply@cfca.gov",
            ["emailTo"] = vehicleInfo.OwnerEmail,
            ["subject"] = $"Speeding violation on the {speedingViolation.RoadId}"
        };
        await daprClient.InvokeBindingAsync("sendmail", "create", body, metadata);

        return Ok();
    }

其中vehicleInfo 的值是通过service invocation调用VehicleRegistrationService获取的。

最后通过Output Binding来发送邮件:

await daprClient.InvokeBindingAsync("sendmail", "create", body, metadata);

运行

  1. start-all.ps1
  2. dapr run --app-id vehicleregistrationservice --app-port 6002 --dapr-http-port 3602 --dapr-grpc-port 60002 --config ../dapr/config/config.yaml --components-path ../dapr/components dotnet run
  3. dapr run --app-id finecollectionservice --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 --config ../dapr/config/config.yaml --components-path ../dapr/components dotnet run
  4. dapr run --app-id trafficcontrolservice --app-port 6000 --dapr-http-port 3600 --dapr-grpc-port 60000 --config ../dapr/config/config.yaml --components-path ../dapr/components dotnet run
  5. cd Simulation && dotnet run

最终结果:
[图片上传失败...(image-c915c-1662870789547)]

车辆的进入退出信息都会实时捕获到,如果有超速信息,会通过邮件发送。
[图片上传失败...(image-878039-1662870789547)]

写在最后

可视化版本

cd .\VisualSimulation\ && dotnet run

[图片上传失败...(image-cee7e8-1662870789547)]

视频可参见: https://live.csdn.net/v/238245

rabbitmq管理界面

默认账号密码都是guest。
[图片上传失败...(image-db9747-1662870789547)]

公众号

更多内容,欢迎关注我的微信公众号: 半夏之夜的无情剑客。
[图片上传失败...(image-a6573f-1662870789547)]

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,583评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,669评论 2 374
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,684评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,682评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,533评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,396评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,814评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,458评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,745评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,789评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,565评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,410评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,828评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,050评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,342评论 1 253
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,793评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,010评论 2 337

推荐阅读更多精彩内容