项目作者: notmarkopadjen

项目描述 :
Distributed in-memory cache for dot net core
高级语言: C#
项目地址: git://github.com/notmarkopadjen/imperfect-dollop.git
创建时间: 2019-08-22T19:20:14Z
项目社区:https://github.com/notmarkopadjen/imperfect-dollop

开源协议:Other

下载


project icon

Imperfect Dollop

This is a .net library that helps user create distributed in-memory cache.

The library targets .net core 2.2

Can be found on NuGet.org:

What this library is not

  • Standalone server
  • ACID compliant

What this library is

  • Really fast
  • Data source connection fault tolerant
  • Able to acquire data from sibling nodes

Idea

There are requirements of caching commonly accessed data. Usualy we do this by storing the data to some caching server (like Redis) and read it from all the nodes. For most use cases this may be the best approach, and I strongly recommend using that if it fits all of your requirements.

From time to time this is not enough for one of the folowing reasons:

  • Connection to caching server is slow, unreliable and / or expensive
  • System needs to continue working while cache server is down (interruption or maintenance)
  • Caching server does not support complex data operations that are required
  • We really don’t want to have it over there; we want to have it in our memory

Implementation

We solve this problem by using in-memory cache.

If just being used by itself (Paden.ImperfectDollop.Repository<T, TId> implemented or one of inherited classes Paden.ImperfectDollop.DictionaryRepository<T, TId> or Paden.ImperfectDollop.ConcurrentDictionaryRepository) it will provide in-memory caching functionality.

If broker is presented (Paden.ImperfectDollop.IBroker) it will hook up and provided synchronization features as well.

Repository<T, TId> has some tweaking options in order to make it configurable for many use cases:

  • TimeSpan? ExpiryInterval - default is 2 minutes
  • bool IsReadOnly - flag indended to be set on slave nodes
  • IFallbackStrategy FallbackStrategy - isolates fallback decision making actions; provided is OneRetryThenRPCFallbackStrategy which is default

It also contains abstract methods which need to be implemented if Repository<T, TId> is inherited. If provided distionary repositories are implemented, this is not required.

Sample project

sample architecture

Provided is the project Paden.SimpleREST, with can be ran by booting up docker-compose.yml.
This compose file includes:

  1. Five instances of Simple REST web application
  2. MariaDb as relational database
  3. RabbitMQ as a broker option
  4. Redis as a broker option

Repository is utilizing provided ConcurrentDictionaryRepository<T, TId>:

  1. using Dapper.Contrib.Extensions;
  2. using Microsoft.Extensions.Logging;
  3. using Microsoft.Extensions.Options;
  4. using MySql.Data.MySqlClient;
  5. using Paden.ImperfectDollop;
  6. using System;
  7. using System.Collections.Generic;
  8. using System.Data;
  9. namespace Paden.SimpleREST.Data
  10. {
  11. public class StudentRepository : ConcurrentDictionaryRepository<Student, int>
  12. {
  13. private readonly string connectionString;
  14. public StudentRepository(IOptions<Settings> settings, ILogger<StudentRepository> logger = null, IBroker broker = null) : base(logger, broker)
  15. {
  16. connectionString = settings.Value.Database;
  17. ExecuteStatement($"CREATE DATABASE IF NOT EXISTS `{Student.PreferedDatabase}`");
  18. connectionString = $"{connectionString};Database={Student.PreferedDatabase}";
  19. ExecuteStatement(Student.CreateStatement);
  20. }
  21. protected override void CreateInSource(Student entity)
  22. {
  23. WithConnection(db => db.Insert(entity));
  24. }
  25. protected override void DeleteInSource(int id)
  26. {
  27. WithConnection(db => db.Delete(new Student { Id = id }));
  28. }
  29. protected override IEnumerable<Student> GetAllFromSource()
  30. {
  31. return WithConnection(db => db.GetAll<Student>());
  32. }
  33. protected override void UpdateInSource(Student entity)
  34. {
  35. WithConnection(db => db.Update(entity));
  36. }
  37. public T WithConnection<T>(Func<IDbConnection, T> function)
  38. {
  39. using (IDbConnection db = new MySqlConnection(connectionString))
  40. {
  41. db.Open();
  42. return function(db);
  43. }
  44. }
  45. public int ExecuteStatement(string sql)
  46. {
  47. return WithConnection(db => new MySqlCommand(sql, db as MySqlConnection).ExecuteNonQuery());
  48. }
  49. }
  50. }

for entity:

  1. using Paden.ImperfectDollop;
  2. namespace Paden.SimpleREST.Data
  3. {
  4. public class Student : Entity<int>
  5. {
  6. public const string PreferedDatabase = "University";
  7. public const string ReCreateStatement = @"
  8. DROP TABLE IF EXISTS `Students`;
  9. " + CreateStatement;
  10. public const string CreateStatement = @"
  11. CREATE TABLE IF NOT EXISTS `Students` (
  12. `id` int(11) NOT NULL AUTO_INCREMENT,
  13. `name` varchar(255) CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL,
  14. `version` bigint(255) UNSIGNED NULL DEFAULT NULL,
  15. PRIMARY KEY (`id`) USING BTREE
  16. ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = latin1 COLLATE = latin1_swedish_ci ROW_FORMAT = Dynamic;
  17. ";
  18. public string Name { get; set; }
  19. }
  20. }

When this implemented, it needs to be added to IoC container:

  1. // Option 1 - RabbitMQBroker
  2. services.AddSingleton<IBroker, RabbitMQBroker>(sp =>
  3. {
  4. var settings = sp.GetService<IOptions<Settings>>();
  5. return new RabbitMQBroker(settings.Value.RabbitMQ, sp.GetService<ILogger<RabbitMQBroker>>());
  6. });
  7. // Option 2 - RedisBroker
  8. //services.AddSingleton<IBroker, RedisBroker>(sp =>
  9. //{
  10. // var settings = sp.GetService<IOptions<Settings>>();
  11. // return new RedisBroker(settings.Value.Redis, sp.GetService<ILogger<RedisBroker>>());
  12. //});
  13. services.AddSingleton<StudentRepository>();

and it is ready for being used in controller:

  1. using Microsoft.AspNetCore.Mvc;
  2. using Paden.SimpleREST.Data;
  3. using System.Collections.Generic;
  4. namespace Paden.SimpleREST.Controllers
  5. {
  6. [Route("api/[controller]")]
  7. [ApiController]
  8. public class StudentsController : ControllerBase
  9. {
  10. private readonly StudentRepository studentRepository;
  11. public StudentsController(StudentRepository studentRepository)
  12. {
  13. this.studentRepository = studentRepository;
  14. }
  15. [HttpGet]
  16. public IEnumerable<Student> Get()
  17. {
  18. return studentRepository.GetAll();
  19. }
  20. [HttpGet("{id}")]
  21. public Student Get(int id)
  22. {
  23. return studentRepository.Get(id);
  24. }
  25. [HttpPost]
  26. public void Post([FromBody] Student value)
  27. {
  28. studentRepository.Create(value);
  29. }
  30. [HttpPut("{id}")]
  31. public void Put(int id, [FromBody] Student value)
  32. {
  33. value.Id = id;
  34. studentRepository.Update(value);
  35. }
  36. [HttpDelete("{id}")]
  37. public void Delete(int id)
  38. {
  39. studentRepository.Delete(id);
  40. }
  41. [HttpGet("info")]
  42. public RepositoryInfo GetInfo()
  43. {
  44. return new RepositoryInfo
  45. {
  46. EntitiesCount = studentRepository.ItemCount,
  47. LastSourceRead = studentRepository.LastSourceRead,
  48. SourceConnectionAliveSince = studentRepository.SourceConnectionAliveSince
  49. };
  50. }
  51. }
  52. }

Optionally, you can register Prometheus endpoint by booting up prometheus-net:

  1. public void Configure(IApplicationBuilder app, IHostingEnvironment env)
  2. {
  3. // ...
  4. app.UseMetricServer();
  5. // ...
  6. }

and adding repository metrics:

  1. services.AddSingleton(sp => new RepositoryMetrics<StudentRepository, Student, int>(sp.GetService<StudentRepository>()));
  2. services.BuildServiceProvider().GetService<RepositoryMetrics<StudentRepository, Student, int>>();

which then produces result like this:

  1. # HELP repositories_studentrepository_source_connection_age_seconds Time in seconds since current source connection has been established
  2. # TYPE repositories_studentrepository_source_connection_age_seconds gauge
  3. repositories_studentrepository_source_connection_age_seconds 19.2369868
  4. # HELP repositories_studentrepository_data_age_seconds Time in seconds since last read from source
  5. # TYPE repositories_studentrepository_data_age_seconds gauge
  6. repositories_studentrepository_data_age_seconds 19.2369857
  7. # HELP repositories_studentrepository_entities_count Number of entities loaded in repository
  8. # TYPE repositories_studentrepository_entities_count gauge
  9. repositories_studentrepository_entities_count 2
  10. # HELP process_private_memory_bytes Process private memory size
  11. # TYPE process_private_memory_bytes gauge
  12. process_private_memory_bytes 0
  13. # HELP dotnet_collection_count_total GC collection count
  14. # TYPE dotnet_collection_count_total counter
  15. dotnet_collection_count_total{generation="1"} 0
  16. dotnet_collection_count_total{generation="0"} 0
  17. dotnet_collection_count_total{generation="2"} 0
  18. # HELP process_num_threads Total number of threads
  19. # TYPE process_num_threads gauge
  20. process_num_threads 26
  21. # HELP process_cpu_seconds_total Total user and system CPU time spent in seconds.
  22. # TYPE process_cpu_seconds_total counter
  23. process_cpu_seconds_total 3.87
  24. # HELP dotnet_total_memory_bytes Total known allocated memory
  25. # TYPE dotnet_total_memory_bytes gauge
  26. dotnet_total_memory_bytes 11666328
  27. # HELP process_start_time_seconds Start time of the process since unix epoch in seconds.
  28. # TYPE process_start_time_seconds gauge
  29. process_start_time_seconds 1566706242.23
  30. # HELP process_virtual_memory_bytes Virtual memory size in bytes.
  31. # TYPE process_virtual_memory_bytes gauge
  32. process_virtual_memory_bytes 12289839104
  33. # HELP process_working_set_bytes Process working set
  34. # TYPE process_working_set_bytes gauge
  35. process_working_set_bytes 98308096
  36. # HELP process_open_handles Number of open handles
  37. # TYPE process_open_handles gauge
  38. process_open_handles 210

on endpoint http://localhost:5081/metrics.

Known limitations and advises

Applications using this library can be ran in master-master or master-slave mode. First one, although possible, is not recommended because it may lead to data inconsistency. You should be aware of this. This library is not ACID and doesn’t handle data conflicts. It has simple version checking, but on heavy load it is not enough.

More common usage is master-slave, where, for example, we have web admin application which can change data, and many nodes (eg microservices) which utilize this data. This library was made for scenarios like this, and it handles them very well. In order to ensure slave nodes are not changing data, you can set IsReadOnly property on repository on slave nodes.

For synchronization purposes, you may provide any Paden.ImperfectDollop.IBroker. The ones provided are RabbitMQ and Redis, but you are welcome to implement additional ones by using provided ones as an example.

RabbitMQ broker

RabbitMQ is a message bus, so it is made for scenarios like this one. Entity change events are being propagated over fanout exchange to client-reading queues. RPC (remote procedure call) is execute over private channels.

Should be used if possible.

Redis broker

Redis is key-value pair caching server. It provides queueing and stacking functionality, but it is pull-only. So, RPC listeners have to maintain their thread and read from Redis queue occasionally.

Should be used if RabbitMQ is not possible, Redis server is already in place, or fits your use case better.

Plans for next releases

  • Entity Framework Core provider