65.9K
CodeProject 正在变化。 阅读更多。
Home

一个简单的键值存储微服务

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.14/5 (7投票s)

2021年6月26日

CPOL

3分钟阅读

viewsIcon

27731

downloadIcon

160

我有一个非常具体的用例,需要一个微服务来管理一个简单的内存数据存储,我称之为“bucket(桶)”。

引言

我有一个非常具体的用例,需要一个微服务来管理一个简单的内存数据存储,我称之为“bucket(桶)”。令我惊讶的是,我简单地在Google上搜索一个现有的简单解决方案却一无所获,但这很正常,因为简单的事情会变得复杂,而简单的事情最终会被遗忘。

我的具体用例是,我需要跟踪谁在使用资源以及使用了多长时间。用户可以表明他们正在使用资源,以及他们何时完成资源的使用,并且用户可以添加关于资源使用的注释。因此,其他用户可以看到谁在使用资源,他们已经使用了多长时间,以及关于他们使用的任何注释。鉴于此,我的需求更多的是关于我不需要什么,而不是我需要什么。

我需要什么

  1. 将一个键设置为一个值的能力。

我不需要什么

  1. 我甚至不需要管理不同桶的能力,但如果不实现这个功能似乎有点愚蠢,所以它被实现了。
  2. 我不在乎服务器重启时内存是否丢失。这仅用于在不同客户端之间共享的信息数据。从技术上讲,其中一个前端客户端(因为客户端将拥有完整的数据)甚至可以为其他所有人恢复数据。
  3. 我不需要复杂的数据结构,只需要键值对,其中值是一些值类型,而不是结构。
  4. 我甚至不在乎一个用户覆盖另一个用户 - 在实际使用中,这种情况不会发生,即使发生了,我也不在乎 - 谁最后更新一个键,谁就赢了。

Redis

"Redis 是一个开源(BSD 许可)的,内存中的数据结构存储,用作数据库、缓存和消息代理。Redis 提供诸如字符串、哈希、列表、集合、排序集合与范围查询、位图、超日志、地理空间索引和流等数据结构。 Redis 具有内置的复制、Lua 脚本、LRU 驱逐、事务和不同级别的磁盘持久化,并通过 Redis Sentinel 提供高可用性,并通过 Redis Cluster 提供自动分区。"

哇。绝对是我需要的过度。也就是说,除非您的需求确实符合这里的要求,否则请不要使用它来替换像 Redis 之类的东西。

API

浏览到 https://:7672/swagger/index.html,我们看到

从 Swagger 文档中可以看出(某种程度上),我们有以下端点

  • 按名称获取桶的内容。
  • 列出当前内存中的所有桶,按其名称。
  • 获取桶对象,其中将包括桶的数据和桶的元数据,元数据只是桶的名称。
  • 删除一个桶。
  • 将桶的数据设置为 POST 正文中的键值对。
  • 使用指定的键值更新桶的数据。

实现

该实现 (C# ASP.NET Core 3.1) 是 123 行控制器代码。非常简单。 这是它的全部内容。

using System.Collections.Generic;
using System.Linq;

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;

using MemoryBucket.Classes;

namespace MemoryBucket.Controllers
{
  [ApiController]
  [Route("[controller]")]
  public class MemoryBucketController : ControllerBase
  {
    private static Buckets buckets = new Buckets();
    private static object locker = new object();

    public MemoryBucketController()
    {
    }

    /// <summary>
    /// Returns the contents of an existing bucket.
    /// </summary>
    [HttpGet]
    public object Get([FromQuery, BindRequired] string bucketName)
    {
      lock (locker)
      {
        Assertion.That(buckets.TryGetValue(bucketName, out Bucket bucket), 
            $"No bucket with name {bucketName} exists.");

        return bucket.Data;
      }
    }

    /// <summary>
    /// Lists all buckets.
    /// </summary>
    [HttpGet("List")]
    public object GetBucket()
    {
      lock (locker)
      {
        return buckets.Select(b => b.Key);
      }
    }

    /// <summary>
    /// Returns the bucket itself.
    /// </summary>
    [HttpGet("Bucket")]
    public object GetBucket([FromQuery, BindRequired] string bucketName)
    {
      lock (locker)
      {
        Assertion.That(buckets.TryGetValue(bucketName, out Bucket bucket), 
           $"No bucket with name {bucketName} exists.");

        return bucket;
      }
    }

    /// <summary>
    /// Deletes an existing bucket.
    /// </summary>
    [HttpDelete("{bucketName}")]
    public void Delete(string bucketName)
    {
      lock (locker)
      {
        Assertion.That(buckets.TryGetValue(bucketName, out Bucket bucket), 
           $"No bucket with name {bucketName} exists.");

        buckets.Remove(bucketName);
      }
    }

    /// <summary>
    /// Creates or gets an existing bucket and replaces its data.
    /// </summary>
    [HttpPost("Set")]
    public object Post(
      [FromQuery, BindRequired] string bucketName, 
      [FromBody] Dictionary<string, object> data)
    {
      lock (locker)
      {
        var bucket = CreateOrGetBucket(bucketName);
        bucket.Data = data;

        return data;
      }
    }

    /// <summary>
    /// Creates or gets an existing bucket and updates the specified key with the 
    /// specified value.
    /// </summary>
    [HttpPost("Update")]
    public object Post(
    [FromQuery, BindRequired] string bucketName,
    [FromQuery, BindRequired] string key,
    [FromQuery, BindRequired] string value)
    {
      lock (locker)
      {
        var bucket = CreateOrGetBucket(bucketName);

        var data = bucket.Data;
        data[key] = value;

        return data;
      }
    }

    private Bucket CreateOrGetBucket(string bucketName)
    {
      if (!buckets.TryGetValue(bucketName, out Bucket bucket))
      {
        bucket = new Bucket();
        bucket.Name = bucketName;
        buckets[bucketName] = bucket;
      }

      return bucket;
    }
  }
}

辅助类

尽量不要感到敬畏。 我真的认为这不需要解释。

Bucket 类

using System.Collections.Generic;

namespace MemoryBucket.Classes
{
  public class Bucket
  {
    public string Name { get; set; }
    public Dictionary<string, object> Data { get; set; } = new Dictionary<string, object>();
  }
}

Buckets 类

using System.Collections.Generic;

namespace MemoryBucket.Classes
{
  public class Buckets : Dictionary<string, Bucket>
  {
  }
}

测试

这里一些Postman测试就足够了。

创建一个桶

curl --location --request POST 'https://:7672/memoryBucket/Set?bucketName=Soup' \
--header 'Content-Type: application/json' \
--data-raw '{
"Name":"Marc",
"Resource": "Garlic",
"Note": "For the soup"
}'

我们看到的响应是

{
  "Name": "Marc",
  "Resource": "Garlic",
  "Note": "For the soup"
}

更新一个桶

Kate正在接手做汤

curl --location 
--request POST 'https://:7672/memoryBucket/Update?bucketName=Soup&key=Name&value=Kate'

我们看到的响应是

{
  "Name": "Kate",
  "Resource": "Garlic",
  "Note": "For the soup"
}

列出桶

我正在添加另一个桶

curl --location --request POST 'https://:7672/memoryBucket/Set?bucketName=Salad' \
--header 'Content-Type: application/json' \
--data-raw '{
"Name":"Laurie",
"Resource": "Lettuce"
}'

当我列出桶时

curl --location --request GET 'https://:7672/memoryBucket/List'

我看到

[
  "Salad",
  "Soup"
]

获取桶本身

curl --location --request GET 'https://:7672/memoryBucket/Bucket?bucketName=Soup'

我看到

{
  "name": "Soup",
  "data": {
    "Name": "Marc",
    "Resource": "Garlic",
    "Note": "For the soup"
  }
}

删除桶

晚餐准备好了

curl --location --request DELETE '<a href="https://:7672/memoryBucket/Soup">
https://:7672/memoryBucket/Soup</a>'
curl --location --request DELETE '<a href="https://:7672/memoryBucket/Salad">
https://:7672/memoryBucket/Salad</a>'
curl --location --request GET 'https://:7672/memoryBucket/List'

没有更多的桶了

[]

结论

简单,对吧? 这让我想知道为什么我们不再做简单的事情了。

历史

  • 2021年6月26日:初始版本
© . All rights reserved.