Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
"redis:6379"
],
"name": "L1L2RedisCache",
"postCreateCommand": "dotnet dev-certs https",
"remoteUser": "root",
"service": "devcontainer",
"shutdownAction": "stopCompose",
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
uses: actions/setup-dotnet@v4
with:
dotnet-version: 9
- name: Build
run: dotnet build -c Release
- name: Pack
run: dotnet pack -c Release
- name: Publish
run: dotnet nuget push "**/*.nupkg" -k ${{ secrets.NUGET_KEY }} -n -s https://api.nuget.org/v3/index.json --skip-duplicate
20 changes: 20 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"version": "0.2.0",
"configurations":
[
{
"name": "MessagingRedisCache Lab",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceFolder}/labs/MessagingRedisCache/bin/Debug/net9.0/MessagingRedisCache.Lab.dll",
"args": [],
"cwd": "${workspaceFolder}/labs/MessagingRedisCache",
"stopAtEntry": false,
"env":
{
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
]
}
12 changes: 12 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<Project>
<PropertyGroup>
<AnalysisLevel>latest-all</AnalysisLevel>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<RepositoryType>git</RepositoryType>
<RepositoryUrl>https://github.com/null-d3v/L1L2RedisCache.git</RepositoryUrl>
</PropertyGroup>
</Project>
22 changes: 22 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<Project>
<PropertyGroup>
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="BenchmarkDotNet" Version="0.14.0" />
<PackageVersion Include="coverlet.collector" Version="6.0.2" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.0" />
<PackageVersion Include="Microsoft.Extensions.Caching.Hybrid" Version="9.0.0-preview.9.24556.5" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.0" />
<PackageVersion Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="9.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="9.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageVersion Include="MSTest.TestAdapter" Version="3.6.4" />
<PackageVersion Include="MSTest.TestFramework" Version="3.6.4" />
<PackageVersion Include="NSubstitute" Version="5.3.0" />
</ItemGroup>
</Project>
86 changes: 69 additions & 17 deletions L1L2RedisCache.sln
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,25 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26124.0
MinimumVisualStudioVersion = 15.0.26124.0
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "L1L2RedisCache", "src\L1L2RedisCache.csproj", "{71A8453B-1A5E-49ED-A9A2-17B7DC9A7407}"
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{FEEBBE29-BAE7-4407-95D5-EB1F56F6BDA3}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{79FE29CD-A4E5-46BB-9FC8-5EC921CFE5F3}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "L1L2RedisCache", "src\L1L2RedisCache\L1L2RedisCache.csproj", "{9C09D590-3186-4E27-8103-66C2D6CAFDF2}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "L1L2RedisCache.Tests.Unit", "tests\Unit\L1L2RedisCache.Tests.Unit.csproj", "{8791FCF7-078D-44A5-AC59-C7C2CE469D3F}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MessagingRedisCache", "src\MessagingRedisCache\MessagingRedisCache.csproj", "{71A8453B-1A5E-49ED-A9A2-17B7DC9A7407}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "L1L2RedisCache.Tests.System", "tests\System\L1L2RedisCache.Tests.System.csproj", "{6A825E82-5BF4-43A0-BA08-9CB000FB232A}"
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{E6F22576-963C-4CE3-A653-FBC7DEA5F10A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "L1L2RedisCache.Tests.System", "tests\MessagingRedisCache\System\MessagingRedisCache.Tests.System.csproj", "{DF934775-FF05-462D-8514-497F68BB8A45}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "L1L2RedisCache.Tests.Unit", "tests\MessagingRedisCache\Unit\MessagingRedisCache.Tests.Unit.csproj", "{8791FCF7-078D-44A5-AC59-C7C2CE469D3F}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "L1L2RedisCache", "L1L2RedisCache", "{E4234EE7-DAAE-40F5-A24F-14C9C2B5F9BD}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "L1L2RedisCache.Tests.System", "tests\L1L2RedisCache\System\L1L2RedisCache.Tests.System.csproj", "{2BA929A4-78E0-4C29-A676-1F5339C99245}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "labs", "labs", "{3E40FF50-EE26-49F6-81B8-94DE4CC9BDF2}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MessagingRedisCache.Lab", "labs\MessagingRedisCache\MessagingRedisCache.Lab.csproj", "{961D1C90-AD89-4F6F-BBB4-923D9F7C4F3A}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down Expand Up @@ -48,20 +60,60 @@ Global
{8791FCF7-078D-44A5-AC59-C7C2CE469D3F}.Release|x64.Build.0 = Release|Any CPU
{8791FCF7-078D-44A5-AC59-C7C2CE469D3F}.Release|x86.ActiveCfg = Release|Any CPU
{8791FCF7-078D-44A5-AC59-C7C2CE469D3F}.Release|x86.Build.0 = Release|Any CPU
{6A825E82-5BF4-43A0-BA08-9CB000FB232A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6A825E82-5BF4-43A0-BA08-9CB000FB232A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6A825E82-5BF4-43A0-BA08-9CB000FB232A}.Debug|x64.ActiveCfg = Debug|Any CPU
{6A825E82-5BF4-43A0-BA08-9CB000FB232A}.Debug|x64.Build.0 = Debug|Any CPU
{6A825E82-5BF4-43A0-BA08-9CB000FB232A}.Debug|x86.ActiveCfg = Debug|Any CPU
{6A825E82-5BF4-43A0-BA08-9CB000FB232A}.Debug|x86.Build.0 = Debug|Any CPU
{6A825E82-5BF4-43A0-BA08-9CB000FB232A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6A825E82-5BF4-43A0-BA08-9CB000FB232A}.Release|Any CPU.Build.0 = Release|Any CPU
{6A825E82-5BF4-43A0-BA08-9CB000FB232A}.Release|x64.ActiveCfg = Release|Any CPU
{6A825E82-5BF4-43A0-BA08-9CB000FB232A}.Release|x64.Build.0 = Release|Any CPU
{6A825E82-5BF4-43A0-BA08-9CB000FB232A}.Release|x86.ActiveCfg = Release|Any CPU
{6A825E82-5BF4-43A0-BA08-9CB000FB232A}.Release|x86.Build.0 = Release|Any CPU
{DF934775-FF05-462D-8514-497F68BB8A45}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DF934775-FF05-462D-8514-497F68BB8A45}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DF934775-FF05-462D-8514-497F68BB8A45}.Debug|x64.ActiveCfg = Debug|Any CPU
{DF934775-FF05-462D-8514-497F68BB8A45}.Debug|x64.Build.0 = Debug|Any CPU
{DF934775-FF05-462D-8514-497F68BB8A45}.Debug|x86.ActiveCfg = Debug|Any CPU
{DF934775-FF05-462D-8514-497F68BB8A45}.Debug|x86.Build.0 = Debug|Any CPU
{DF934775-FF05-462D-8514-497F68BB8A45}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DF934775-FF05-462D-8514-497F68BB8A45}.Release|Any CPU.Build.0 = Release|Any CPU
{DF934775-FF05-462D-8514-497F68BB8A45}.Release|x64.ActiveCfg = Release|Any CPU
{DF934775-FF05-462D-8514-497F68BB8A45}.Release|x64.Build.0 = Release|Any CPU
{DF934775-FF05-462D-8514-497F68BB8A45}.Release|x86.ActiveCfg = Release|Any CPU
{DF934775-FF05-462D-8514-497F68BB8A45}.Release|x86.Build.0 = Release|Any CPU
{9C09D590-3186-4E27-8103-66C2D6CAFDF2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9C09D590-3186-4E27-8103-66C2D6CAFDF2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9C09D590-3186-4E27-8103-66C2D6CAFDF2}.Debug|x64.ActiveCfg = Debug|Any CPU
{9C09D590-3186-4E27-8103-66C2D6CAFDF2}.Debug|x64.Build.0 = Debug|Any CPU
{9C09D590-3186-4E27-8103-66C2D6CAFDF2}.Debug|x86.ActiveCfg = Debug|Any CPU
{9C09D590-3186-4E27-8103-66C2D6CAFDF2}.Debug|x86.Build.0 = Debug|Any CPU
{9C09D590-3186-4E27-8103-66C2D6CAFDF2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9C09D590-3186-4E27-8103-66C2D6CAFDF2}.Release|Any CPU.Build.0 = Release|Any CPU
{9C09D590-3186-4E27-8103-66C2D6CAFDF2}.Release|x64.ActiveCfg = Release|Any CPU
{9C09D590-3186-4E27-8103-66C2D6CAFDF2}.Release|x64.Build.0 = Release|Any CPU
{9C09D590-3186-4E27-8103-66C2D6CAFDF2}.Release|x86.ActiveCfg = Release|Any CPU
{9C09D590-3186-4E27-8103-66C2D6CAFDF2}.Release|x86.Build.0 = Release|Any CPU
{2BA929A4-78E0-4C29-A676-1F5339C99245}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2BA929A4-78E0-4C29-A676-1F5339C99245}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2BA929A4-78E0-4C29-A676-1F5339C99245}.Debug|x64.ActiveCfg = Debug|Any CPU
{2BA929A4-78E0-4C29-A676-1F5339C99245}.Debug|x64.Build.0 = Debug|Any CPU
{2BA929A4-78E0-4C29-A676-1F5339C99245}.Debug|x86.ActiveCfg = Debug|Any CPU
{2BA929A4-78E0-4C29-A676-1F5339C99245}.Debug|x86.Build.0 = Debug|Any CPU
{2BA929A4-78E0-4C29-A676-1F5339C99245}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2BA929A4-78E0-4C29-A676-1F5339C99245}.Release|Any CPU.Build.0 = Release|Any CPU
{2BA929A4-78E0-4C29-A676-1F5339C99245}.Release|x64.ActiveCfg = Release|Any CPU
{2BA929A4-78E0-4C29-A676-1F5339C99245}.Release|x64.Build.0 = Release|Any CPU
{2BA929A4-78E0-4C29-A676-1F5339C99245}.Release|x86.ActiveCfg = Release|Any CPU
{2BA929A4-78E0-4C29-A676-1F5339C99245}.Release|x86.Build.0 = Release|Any CPU
{961D1C90-AD89-4F6F-BBB4-923D9F7C4F3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{961D1C90-AD89-4F6F-BBB4-923D9F7C4F3A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{961D1C90-AD89-4F6F-BBB4-923D9F7C4F3A}.Debug|x64.ActiveCfg = Debug|Any CPU
{961D1C90-AD89-4F6F-BBB4-923D9F7C4F3A}.Debug|x64.Build.0 = Debug|Any CPU
{961D1C90-AD89-4F6F-BBB4-923D9F7C4F3A}.Debug|x86.ActiveCfg = Debug|Any CPU
{961D1C90-AD89-4F6F-BBB4-923D9F7C4F3A}.Debug|x86.Build.0 = Debug|Any CPU
{961D1C90-AD89-4F6F-BBB4-923D9F7C4F3A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{961D1C90-AD89-4F6F-BBB4-923D9F7C4F3A}.Release|Any CPU.Build.0 = Release|Any CPU
{961D1C90-AD89-4F6F-BBB4-923D9F7C4F3A}.Release|x64.ActiveCfg = Release|Any CPU
{961D1C90-AD89-4F6F-BBB4-923D9F7C4F3A}.Release|x64.Build.0 = Release|Any CPU
{961D1C90-AD89-4F6F-BBB4-923D9F7C4F3A}.Release|x86.ActiveCfg = Release|Any CPU
{961D1C90-AD89-4F6F-BBB4-923D9F7C4F3A}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{6A825E82-5BF4-43A0-BA08-9CB000FB232A} = {79FE29CD-A4E5-46BB-9FC8-5EC921CFE5F3}
{DF934775-FF05-462D-8514-497F68BB8A45} = {E6F22576-963C-4CE3-A653-FBC7DEA5F10A}
{9C09D590-3186-4E27-8103-66C2D6CAFDF2} = {FEEBBE29-BAE7-4407-95D5-EB1F56F6BDA3}
{E4234EE7-DAAE-40F5-A24F-14C9C2B5F9BD} = {E6F22576-963C-4CE3-A653-FBC7DEA5F10A}
{2BA929A4-78E0-4C29-A676-1F5339C99245} = {E4234EE7-DAAE-40F5-A24F-14C9C2B5F9BD}
{961D1C90-AD89-4F6F-BBB4-923D9F7C4F3A} = {3E40FF50-EE26-49F6-81B8-94DE4CC9BDF2}
EndGlobalSection
EndGlobal
42 changes: 4 additions & 38 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,41 +1,7 @@
# L1L2RedisCache

`L1L2RedisCache` is an implementation of [`IDistributedCache`](https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Caching.Abstractions/src/IDistributedCache.cs) with a strong focus on performance. It leverages [`IMemoryCache`](https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Caching.Abstractions/src/IMemoryCache.cs) as a level 1 cache and [`RedisCache`](https://github.com/dotnet/aspnetcore/blob/main/src/Caching/StackExchangeRedis/src/RedisCache.cs) as a level 2 cache, with level 1 evictions being managed via [Redis pub/sub](https://redis.io/topics/pubsub).

`L1L2RedisCache` is heavily inspired by development insights provided over the past several years by [StackOverflow](https://stackoverflow.com/). It attempts to simplify those concepts into a highly accessible `IDistributedCache` implementation that is more performant.

I expect to gracefully decomission this project when [`StackExchange.Redis`](https://github.com/StackExchange/StackExchange.Redis) has [client-side caching](https://redis.io/docs/latest/develop/use/client-side-caching/) support.

## Configuration

It is intended that L1L12RedisCache be used as an `IDistributedCache` implementation.

`L1L2RedisCache` can be registered during startup with the following `IServiceCollection` extension method:
# MessagingRedisCache

```
services.AddL1L2RedisCache(options =>
{
options.Configuration = "localhost";
options.InstanceName = "Namespace:Prefix:";
});
```
[`MessagingRedisCache`](/src/MessagingRedisCache/README.md) is an implementation of [`IDistributedCache`](https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Caching.Abstractions/src/IDistributedCache.cs) using [`RedisCache`](https://github.com/dotnet/aspnetcore/blob/main/src/Caching/StackExchangeRedis/src/RedisCache.cs) as a base implementation. `MessagingRedisCache` will utilize [Redis pub/sub](https://redis.io/topics/pubsub) to ensure that memory cache entries can be synchronized in a distributed system. This makes it a viable backing store for [`HybridCache`](https://learn.microsoft.com/en-us/aspnet/core/performance/caching/hybrid), where it can evict `IMemoryCache` entries in distributed systems.

`L1L2RedisCache` options are an extension of the standard `RedisCache` [`RedisCacheOptions`](https://github.com/dotnet/aspnetcore/blob/main/src/Caching/StackExchangeRedis/src/RedisCacheOptions.cs). The following additional customizations are supported:

### MessagingType

The type of messaging system to use for L1 memory cache eviction.

| MessagingType | Description | Suggestion |
| - | - | - |
| `Default` | Use standard `L1L2RedisCache` [pub/sub](https://redis.io/topics/pubsub) messages for L1 memory cache eviction. | Default behavior. The Redis server requires no additional configuration. |
| `KeyeventNotifications` | Use [keyevent notifications](https://redis.io/topics/notifications) for L1 memory eviction instead of standard `L1L2RedisCache` [pub/sub](https://redis.io/topics/pubsub) messages. The Redis server must have keyevent notifications enabled. | This is only advisable if the Redis server is already using [keyevent notifications](https://redis.io/topics/notifications) with at least a `ghE` configuration and the majority of keys in the server are managed by `L1L2RedisCache`. |
| `KeyspaceNotifications` | Use [keyspace notifications](https://redis.io/topics/notifications) for L1 memory eviction instead of standard `L1L2RedisCache` [pub/sub](https://redis.io/topics/pubsub) messages. The Redis server must have keyspace notifications enabled. | This is only advisable if the Redis server is already using [keyevent notifications](https://redis.io/topics/notifications) with at least a `ghK` configuration and the majority of keys in the server are managed by `L1L2RedisCache`. |

## Performance

L1L2RedisCache will generally outperform `RedisCache`, especially in cases of high volume or large cache entries. As entries are opportunistically pulled from memory instead of Redis, costs of latency, network, and Redis operations are avoided. Respective performance gains will rely heavily on the impact of afforementioned factors.

## Considerations
# L1L2RedisCache

Due to the complex nature of a distributed L1 memory cache, cache entries with sliding expirations are only stored in L2 (Redis). These entries will show no performance improvement over the standard `RedisCache`, but incur no performance penalty.
[`L1L2RedisCache`](/src/L1L2RedisCache/README.md) is an implementation of [`IDistributedCache`](https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Caching.Abstractions/src/IDistributedCache.cs), leveraging [`IMemoryCache`](https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Caching.Abstractions/src/IMemoryCache.cs) as a level 1 cache and [`MessagingRedisCache`](/src/MessagingRedisCache/README.md) as a level 2 cache. Level 1 evictions are managed via `MessagingRedisCache`'s [Redis pub/sub](https://redis.io/topics/pubsub).
17 changes: 17 additions & 0 deletions labs/MessagingRedisCache/MessagingRedisCache.Lab.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<NoWarn>EXTEXP0018;</NoWarn>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\MessagingRedisCache\MessagingRedisCache.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Caching.Hybrid" />
<PackageReference Include="Microsoft.Extensions.Configuration" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
</ItemGroup>
</Project>
34 changes: 34 additions & 0 deletions labs/MessagingRedisCache/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using Microsoft.Extensions.Caching.Hybrid;
using Microsoft.Extensions.DependencyInjection;

var services = new ServiceCollection();
services.AddHybridCache();
services.AddMessagingRedisCache(options =>
{
options.Configuration = "redis";
options.InstanceName = "MessagingRedisCache:Test:";
});
var serviceProvider = services.BuildServiceProvider();

var hybridCache = serviceProvider
.GetRequiredService<HybridCache>();

var key = Guid.NewGuid().ToString();
var value = Guid.NewGuid().ToString();

await hybridCache
.SetAsync(
key,
value,
new HybridCacheEntryOptions
{
Expiration = TimeSpan.FromHours(1),
})
.ConfigureAwait(false);

var testValue = await hybridCache
.GetOrCreateAsync(
key,
cancellationToken =>
ValueTask.FromResult<string?>(null))
.ConfigureAwait(false);
Loading
Loading