├── README.md
├── bmalec_redisconf18_presentation_final.pptx
├── TestHarness
├── packages.config
├── App.config
├── Properties
│ └── AssemblyInfo.cs
├── TestHarness.csproj
└── Program.cs
├── RedisMultilevelCache
├── packages.config
├── ISerializationProvider.cs
├── DefaultSerializationProvider.cs
├── Properties
│ └── AssemblyInfo.cs
├── DataSyncMessage.cs
├── RedisMultilevelCache.csproj
├── HashSlotCalculator.cs
└── MultilevelCacheProvider.cs
├── RedisMultilevelCache.sln
└── .gitignore
/README.md:
--------------------------------------------------------------------------------
1 | # RedisMultilevelCache
2 | Example implementation for the RedisConf18 session
3 |
--------------------------------------------------------------------------------
/bmalec_redisconf18_presentation_final.pptx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bmalec/RedisMultilevelCache/HEAD/bmalec_redisconf18_presentation_final.pptx
--------------------------------------------------------------------------------
/TestHarness/packages.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/RedisMultilevelCache/packages.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/TestHarness/App.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/RedisMultilevelCache/ISerializationProvider.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 |
3 |
4 | namespace RedisMultilevelCache
5 | {
6 | ///
7 | /// This interface defines the methods needed to replace the RedisMultilevelCache's default serialization provider
8 | ///
9 | ///
10 | /// Generic type parameters are used here to better accomodate serializers like Newtonsoft.Json,
11 | /// which need to know the type they're deserializing to
12 | ///
13 | public interface ISerializationProvider
14 | {
15 | void Serialize(Stream serializationStream, T data);
16 | T Deserialize(Stream serializationStream);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/RedisMultilevelCache/DefaultSerializationProvider.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using System.Runtime.Serialization.Formatters.Binary;
3 |
4 | namespace RedisMultilevelCache
5 | {
6 | ///
7 | /// Default serialization provider implementation
8 | ///
9 | ///
10 | /// This implementation requires the [Serializeable] attribute on all all classes stored in the cache
11 | ///
12 | internal class DefaultSerializationProvider : ISerializationProvider
13 | {
14 | public T Deserialize(Stream serializationStream)
15 | {
16 | var formatter = new BinaryFormatter();
17 | return (T) formatter.Deserialize(serializationStream);
18 | }
19 |
20 | public void Serialize(Stream serializationStream, T data)
21 | {
22 | var formatter = new BinaryFormatter();
23 | formatter.Serialize(serializationStream, data);
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/TestHarness/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.CompilerServices;
3 | using System.Runtime.InteropServices;
4 |
5 | // General Information about an assembly is controlled through the following
6 | // set of attributes. Change these attribute values to modify the information
7 | // associated with an assembly.
8 | [assembly: AssemblyTitle("TestHarness")]
9 | [assembly: AssemblyDescription("")]
10 | [assembly: AssemblyConfiguration("")]
11 | [assembly: AssemblyCompany("")]
12 | [assembly: AssemblyProduct("TestHarness")]
13 | [assembly: AssemblyCopyright("Copyright © 2018")]
14 | [assembly: AssemblyTrademark("")]
15 | [assembly: AssemblyCulture("")]
16 |
17 | // Setting ComVisible to false makes the types in this assembly not visible
18 | // to COM components. If you need to access a type in this assembly from
19 | // COM, set the ComVisible attribute to true on that type.
20 | [assembly: ComVisible(false)]
21 |
22 | // The following GUID is for the ID of the typelib if this project is exposed to COM
23 | [assembly: Guid("9141f124-2a57-4e2a-a2e4-c1349aec5bd7")]
24 |
25 | // Version information for an assembly consists of the following four values:
26 | //
27 | // Major Version
28 | // Minor Version
29 | // Build Number
30 | // Revision
31 | //
32 | // You can specify all the values or you can default the Build and Revision Numbers
33 | // by using the '*' as shown below:
34 | // [assembly: AssemblyVersion("1.0.*")]
35 | [assembly: AssemblyVersion("1.0.0.0")]
36 | [assembly: AssemblyFileVersion("1.0.0.0")]
37 |
--------------------------------------------------------------------------------
/RedisMultilevelCache/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.CompilerServices;
3 | using System.Runtime.InteropServices;
4 |
5 | // General Information about an assembly is controlled through the following
6 | // set of attributes. Change these attribute values to modify the information
7 | // associated with an assembly.
8 | [assembly: AssemblyTitle("RedisMultilevelCache")]
9 | [assembly: AssemblyDescription("")]
10 | [assembly: AssemblyConfiguration("")]
11 | [assembly: AssemblyCompany("")]
12 | [assembly: AssemblyProduct("RedisMultilevelCache")]
13 | [assembly: AssemblyCopyright("Copyright © 2018")]
14 | [assembly: AssemblyTrademark("")]
15 | [assembly: AssemblyCulture("")]
16 |
17 | // Setting ComVisible to false makes the types in this assembly not visible
18 | // to COM components. If you need to access a type in this assembly from
19 | // COM, set the ComVisible attribute to true on that type.
20 | [assembly: ComVisible(false)]
21 |
22 | // The following GUID is for the ID of the typelib if this project is exposed to COM
23 | [assembly: Guid("ab7ce4de-347a-468a-8b35-6df42999f48b")]
24 |
25 | // Version information for an assembly consists of the following four values:
26 | //
27 | // Major Version
28 | // Minor Version
29 | // Build Number
30 | // Revision
31 | //
32 | // You can specify all the values or you can default the Build and Revision Numbers
33 | // by using the '*' as shown below:
34 | // [assembly: AssemblyVersion("1.0.*")]
35 | [assembly: AssemblyVersion("1.0.0.0")]
36 | [assembly: AssemblyFileVersion("1.0.0.0")]
37 |
--------------------------------------------------------------------------------
/RedisMultilevelCache.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio 15
4 | VisualStudioVersion = 15.0.27428.2037
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestHarness", "TestHarness\TestHarness.csproj", "{9141F124-2A57-4E2A-A2E4-C1349AEC5BD7}"
7 | EndProject
8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RedisMultilevelCache", "RedisMultilevelCache\RedisMultilevelCache.csproj", "{AB7CE4DE-347A-468A-8B35-6DF42999F48B}"
9 | EndProject
10 | Global
11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
12 | Debug|Any CPU = Debug|Any CPU
13 | Release|Any CPU = Release|Any CPU
14 | EndGlobalSection
15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
16 | {9141F124-2A57-4E2A-A2E4-C1349AEC5BD7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
17 | {9141F124-2A57-4E2A-A2E4-C1349AEC5BD7}.Debug|Any CPU.Build.0 = Debug|Any CPU
18 | {9141F124-2A57-4E2A-A2E4-C1349AEC5BD7}.Release|Any CPU.ActiveCfg = Release|Any CPU
19 | {9141F124-2A57-4E2A-A2E4-C1349AEC5BD7}.Release|Any CPU.Build.0 = Release|Any CPU
20 | {AB7CE4DE-347A-468A-8B35-6DF42999F48B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
21 | {AB7CE4DE-347A-468A-8B35-6DF42999F48B}.Debug|Any CPU.Build.0 = Debug|Any CPU
22 | {AB7CE4DE-347A-468A-8B35-6DF42999F48B}.Release|Any CPU.ActiveCfg = Release|Any CPU
23 | {AB7CE4DE-347A-468A-8B35-6DF42999F48B}.Release|Any CPU.Build.0 = Release|Any CPU
24 | EndGlobalSection
25 | GlobalSection(SolutionProperties) = preSolution
26 | HideSolutionNode = FALSE
27 | EndGlobalSection
28 | GlobalSection(ExtensibilityGlobals) = postSolution
29 | SolutionGuid = {AB1EB4E6-507F-4CF9-BFCD-ABBD18CB167E}
30 | EndGlobalSection
31 | EndGlobal
32 |
--------------------------------------------------------------------------------
/RedisMultilevelCache/DataSyncMessage.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 |
4 | namespace RedisMultilevelCache
5 | {
6 | ///
7 | /// Represents a notification to evict keys with a specific hashslot
8 | ///
9 | internal class DataSyncMessage
10 | {
11 | // Unique ID of the cache instance that initiated the data change
12 | public Guid SenderInstanceId { get; private set; }
13 | // Hashslot of the keys to evict
14 | public ushort KeyHashSlot { get; private set; }
15 |
16 |
17 | ///
18 | /// Private constructor to insure users call the static Create() method to create message objects
19 | ///
20 | private DataSyncMessage(Guid senderInstanceId, ushort keyHashSlot)
21 | {
22 | SenderInstanceId = senderInstanceId;
23 | KeyHashSlot = keyHashSlot;
24 | }
25 |
26 | ///
27 | /// Create a populated DataSyncMessage object
28 | ///
29 | /// ID of the cache instance making the data change
30 | /// Hashslot of the keys being evicted
31 | ///
32 | public static DataSyncMessage Create(Guid senderInstanceId, ushort keyHashSlot)
33 | {
34 | return new DataSyncMessage(senderInstanceId, keyHashSlot);
35 | }
36 |
37 |
38 | ///
39 | /// Serialize the DataSyncMessage object to a byte array
40 | ///
41 | /// Byte array representing a DataSyncMessage instance
42 | public byte[] Serialize()
43 | {
44 | var messageBytes = new byte[18];
45 |
46 | Buffer.BlockCopy(SenderInstanceId.ToByteArray(), 0, messageBytes, 0, 16);
47 |
48 | // final two bytes are the key CRC in big-endian format
49 |
50 | messageBytes[16] = (byte)(KeyHashSlot >> 8);
51 | messageBytes[17] = (byte)(KeyHashSlot & 0x00FF);
52 |
53 | return messageBytes;
54 | }
55 |
56 | ///
57 | /// Create a DataSyncMessage object from a byte array
58 | ///
59 | /// Byte array representing the DataSyncMessage object
60 | ///
61 | public static DataSyncMessage Deserialize(byte[] messageBytes)
62 | {
63 | if (messageBytes == null) throw new ArgumentNullException(nameof(messageBytes));
64 | if (messageBytes.Length != 18) throw new ArgumentException("Invalid message length");
65 |
66 | var guidBytes = new byte[16];
67 | Buffer.BlockCopy(messageBytes, 0, guidBytes, 0, guidBytes.Length);
68 |
69 | var senderInstanceId = new Guid(guidBytes);
70 | ushort keyHashSlot = (ushort)((((ushort) messageBytes[16]) << 8) + messageBytes[17]);
71 |
72 | return new DataSyncMessage(senderInstanceId, keyHashSlot);
73 | }
74 |
75 |
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/RedisMultilevelCache/RedisMultilevelCache.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Debug
6 | AnyCPU
7 | {AB7CE4DE-347A-468A-8B35-6DF42999F48B}
8 | Library
9 | Properties
10 | RedisMultilevelCache
11 | RedisMultilevelCache
12 | v4.5.2
13 | 512
14 |
15 |
16 | true
17 | full
18 | false
19 | bin\Debug\
20 | DEBUG;TRACE
21 | prompt
22 | 4
23 |
24 |
25 | pdbonly
26 | true
27 | bin\Release\
28 | TRACE
29 | prompt
30 | 4
31 |
32 |
33 |
34 | ..\packages\StackExchange.Redis.1.2.6\lib\net45\StackExchange.Redis.dll
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/TestHarness/TestHarness.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Debug
6 | AnyCPU
7 | {9141F124-2A57-4E2A-A2E4-C1349AEC5BD7}
8 | Exe
9 | TestHarness
10 | TestHarness
11 | v4.5.2
12 | 512
13 | true
14 |
15 |
16 | AnyCPU
17 | true
18 | full
19 | false
20 | bin\Debug\
21 | DEBUG;TRACE
22 | prompt
23 | 4
24 |
25 |
26 | AnyCPU
27 | pdbonly
28 | true
29 | bin\Release\
30 | TRACE
31 | prompt
32 | 4
33 |
34 |
35 |
36 | ..\packages\StackExchange.Redis.1.2.6\lib\net45\StackExchange.Redis.dll
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | {ab7ce4de-347a-468a-8b35-6df42999f48b}
59 | RedisMultilevelCache
60 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/RedisMultilevelCache/HashSlotCalculator.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 |
6 | namespace RedisMultilevelCache
7 | {
8 | internal static class HashSlotCalculator
9 | {
10 | private const int HASH_SLOT_COUNT = 16384;
11 |
12 | private static readonly ushort[] crc16Table =
13 | {
14 | 0x0000,0x1021,0x2042,0x3063,0x4084,0x50a5,0x60c6,0x70e7,
15 | 0x8108,0x9129,0xa14a,0xb16b,0xc18c,0xd1ad,0xe1ce,0xf1ef,
16 | 0x1231,0x0210,0x3273,0x2252,0x52b5,0x4294,0x72f7,0x62d6,
17 | 0x9339,0x8318,0xb37b,0xa35a,0xd3bd,0xc39c,0xf3ff,0xe3de,
18 | 0x2462,0x3443,0x0420,0x1401,0x64e6,0x74c7,0x44a4,0x5485,
19 | 0xa56a,0xb54b,0x8528,0x9509,0xe5ee,0xf5cf,0xc5ac,0xd58d,
20 | 0x3653,0x2672,0x1611,0x0630,0x76d7,0x66f6,0x5695,0x46b4,
21 | 0xb75b,0xa77a,0x9719,0x8738,0xf7df,0xe7fe,0xd79d,0xc7bc,
22 | 0x48c4,0x58e5,0x6886,0x78a7,0x0840,0x1861,0x2802,0x3823,
23 | 0xc9cc,0xd9ed,0xe98e,0xf9af,0x8948,0x9969,0xa90a,0xb92b,
24 | 0x5af5,0x4ad4,0x7ab7,0x6a96,0x1a71,0x0a50,0x3a33,0x2a12,
25 | 0xdbfd,0xcbdc,0xfbbf,0xeb9e,0x9b79,0x8b58,0xbb3b,0xab1a,
26 | 0x6ca6,0x7c87,0x4ce4,0x5cc5,0x2c22,0x3c03,0x0c60,0x1c41,
27 | 0xedae,0xfd8f,0xcdec,0xddcd,0xad2a,0xbd0b,0x8d68,0x9d49,
28 | 0x7e97,0x6eb6,0x5ed5,0x4ef4,0x3e13,0x2e32,0x1e51,0x0e70,
29 | 0xff9f,0xefbe,0xdfdd,0xcffc,0xbf1b,0xaf3a,0x9f59,0x8f78,
30 | 0x9188,0x81a9,0xb1ca,0xa1eb,0xd10c,0xc12d,0xf14e,0xe16f,
31 | 0x1080,0x00a1,0x30c2,0x20e3,0x5004,0x4025,0x7046,0x6067,
32 | 0x83b9,0x9398,0xa3fb,0xb3da,0xc33d,0xd31c,0xe37f,0xf35e,
33 | 0x02b1,0x1290,0x22f3,0x32d2,0x4235,0x5214,0x6277,0x7256,
34 | 0xb5ea,0xa5cb,0x95a8,0x8589,0xf56e,0xe54f,0xd52c,0xc50d,
35 | 0x34e2,0x24c3,0x14a0,0x0481,0x7466,0x6447,0x5424,0x4405,
36 | 0xa7db,0xb7fa,0x8799,0x97b8,0xe75f,0xf77e,0xc71d,0xd73c,
37 | 0x26d3,0x36f2,0x0691,0x16b0,0x6657,0x7676,0x4615,0x5634,
38 | 0xd94c,0xc96d,0xf90e,0xe92f,0x99c8,0x89e9,0xb98a,0xa9ab,
39 | 0x5844,0x4865,0x7806,0x6827,0x18c0,0x08e1,0x3882,0x28a3,
40 | 0xcb7d,0xdb5c,0xeb3f,0xfb1e,0x8bf9,0x9bd8,0xabbb,0xbb9a,
41 | 0x4a75,0x5a54,0x6a37,0x7a16,0x0af1,0x1ad0,0x2ab3,0x3a92,
42 | 0xfd2e,0xed0f,0xdd6c,0xcd4d,0xbdaa,0xad8b,0x9de8,0x8dc9,
43 | 0x7c26,0x6c07,0x5c64,0x4c45,0x3ca2,0x2c83,0x1ce0,0x0cc1,
44 | 0xef1f,0xff3e,0xcf5d,0xdf7c,0xaf9b,0xbfba,0x8fd9,0x9ff8,
45 | 0x6e17,0x7e36,0x4e55,0x5e74,0x2e93,0x3eb2,0x0ed1,0x1ef0
46 | };
47 |
48 |
49 | ///
50 | /// Return the Redis cluster hash slot for a key
51 | ///
52 | /// Cached item key
53 | /// Hash slot for the key
54 | public static ushort CalculateHashSlot(string key)
55 | {
56 | return (ushort)(CalculateCrc16(Encoding.UTF8.GetBytes(key)) % HASH_SLOT_COUNT);
57 | }
58 |
59 |
60 | ///
61 | /// Calculate the 16-bit CRC for an array of bytes, using the CRC-16-CCITT algorithm
62 | ///
63 | /// Cached item key
64 | /// 16-bit CRC
65 |
66 | private static ushort CalculateCrc16(byte[] key)
67 | {
68 | UInt32 crc = 0;
69 |
70 | for (int i = 0; i < key.Length; i++)
71 | {
72 | crc = ((crc << 8) ^ crc16Table[((crc >> 8) ^ key[i]) & 0x00FF]) & 0x0000FFFF;
73 | }
74 |
75 | return (ushort)crc;
76 | }
77 |
78 |
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/TestHarness/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading;
3 | using System.Threading.Tasks;
4 | using System.Diagnostics;
5 | using RedisMultilevelCache;
6 |
7 | namespace TestHarness
8 | {
9 | class Program
10 | {
11 | enum Operation { Read, Update };
12 |
13 | private const int KEY_COUNT = 1000;
14 | private const int ITERATION_COUNT = 100000;
15 | private const int MIN_DATA_SIZE = 512;
16 | private const int MAX_DATA_SIZE = 4096;
17 |
18 | private static Random _rnd = new Random();
19 |
20 | static void Main(string[] args)
21 | {
22 | if (args.Length < 1)
23 | {
24 | Console.WriteLine("Usage: TestHarness redis_address[:redis_port]");
25 | Console.WriteLine("Examples:");
26 | Console.WriteLine(" Use locally installed Redis server, default port of 6379: TestHarness localhost");
27 | Console.WriteLine(" Use Redis cluster which includes a node at cacheserver, port 6382: TestHarness cacheserver:6382");
28 | return;
29 | }
30 |
31 | var cache = new MultilevelCacheProvider(args[0]);
32 |
33 | Console.WriteLine("Loading cache data...");
34 |
35 | // Prefill the cache with data, using parallel operations to better
36 | // simulate a multi-threaded client
37 |
38 | Parallel.For(0, KEY_COUNT, (i) =>
39 | {
40 | cache.Set(BuildKey(i), GetRandomData(), TimeSpan.FromMinutes(2));
41 | });
42 |
43 | Console.WriteLine("Executing test...");
44 |
45 | var stopwatch = Stopwatch.StartNew();
46 |
47 | long totalBytesTransfered = 0;
48 |
49 | // Run test loop in parallel, to better simulate multiple threads on a web
50 | // server accessing the cache
51 |
52 | Parallel.For(0, ITERATION_COUNT, (i) => {
53 | string key = GetRandomKey();
54 | Operation op = GetRandomOp();
55 |
56 | if (op == Operation.Update)
57 | {
58 | var data = GetRandomData();
59 | cache.Set(key, data, TimeSpan.FromMinutes(2));
60 | Interlocked.Add(ref totalBytesTransfered, data.Length);
61 | }
62 | else
63 | {
64 | var data = cache.Get(key);
65 | Interlocked.Add(ref totalBytesTransfered, data.Length);
66 | }
67 | });
68 |
69 | var elapsedTime = stopwatch.Elapsed;
70 |
71 | double opsPerSecond = ITERATION_COUNT / elapsedTime.TotalSeconds;
72 | double bytesPerSecond = totalBytesTransfered / elapsedTime.TotalSeconds;
73 |
74 | Console.WriteLine($"{opsPerSecond.ToString("N1")} op/sec");
75 | Console.WriteLine($"{bytesPerSecond.ToString("N1")} bytes/sec");
76 | }
77 |
78 |
79 | private static string GetRandomKey()
80 | {
81 | lock (_rnd)
82 | {
83 | return BuildKey(_rnd.Next(KEY_COUNT));
84 | }
85 | }
86 |
87 |
88 | ///
89 | /// Random op generator, generates 95% reads and 5% updates
90 | ///
91 | ///
92 | private static Operation GetRandomOp()
93 | {
94 | Operation op = Operation.Read;
95 |
96 | lock (_rnd)
97 | {
98 | if (_rnd.NextDouble() > 0.95) op = Operation.Update;
99 | }
100 |
101 | return op;
102 | }
103 |
104 |
105 | private static byte[] GetRandomData()
106 | {
107 | byte[] data = null;
108 |
109 | lock (_rnd)
110 | {
111 | data = new byte[_rnd.Next(MAX_DATA_SIZE - MIN_DATA_SIZE) + MIN_DATA_SIZE];
112 | _rnd.NextBytes(data);
113 | }
114 |
115 | return data;
116 | }
117 |
118 |
119 |
120 | private static string BuildKey(int i)
121 | {
122 | return string.Concat("TestHarness:", i);
123 | }
124 |
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.suo
8 | *.user
9 | *.userosscache
10 | *.sln.docstates
11 |
12 | # User-specific files (MonoDevelop/Xamarin Studio)
13 | *.userprefs
14 |
15 | # Build results
16 | [Dd]ebug/
17 | [Dd]ebugPublic/
18 | [Rr]elease/
19 | [Rr]eleases/
20 | x64/
21 | x86/
22 | bld/
23 | [Bb]in/
24 | [Oo]bj/
25 | [Ll]og/
26 |
27 | # Visual Studio 2015/2017 cache/options directory
28 | .vs/
29 | # Uncomment if you have tasks that create the project's static files in wwwroot
30 | #wwwroot/
31 |
32 | # Visual Studio 2017 auto generated files
33 | Generated\ Files/
34 |
35 | # MSTest test Results
36 | [Tt]est[Rr]esult*/
37 | [Bb]uild[Ll]og.*
38 |
39 | # NUNIT
40 | *.VisualState.xml
41 | TestResult.xml
42 |
43 | # Build Results of an ATL Project
44 | [Dd]ebugPS/
45 | [Rr]eleasePS/
46 | dlldata.c
47 |
48 | # Benchmark Results
49 | BenchmarkDotNet.Artifacts/
50 |
51 | # .NET Core
52 | project.lock.json
53 | project.fragment.lock.json
54 | artifacts/
55 | **/Properties/launchSettings.json
56 |
57 | # StyleCop
58 | StyleCopReport.xml
59 |
60 | # Files built by Visual Studio
61 | *_i.c
62 | *_p.c
63 | *_i.h
64 | *.ilk
65 | *.meta
66 | *.obj
67 | *.iobj
68 | *.pch
69 | *.pdb
70 | *.ipdb
71 | *.pgc
72 | *.pgd
73 | *.rsp
74 | *.sbr
75 | *.tlb
76 | *.tli
77 | *.tlh
78 | *.tmp
79 | *.tmp_proj
80 | *.log
81 | *.vspscc
82 | *.vssscc
83 | .builds
84 | *.pidb
85 | *.svclog
86 | *.scc
87 |
88 | # Chutzpah Test files
89 | _Chutzpah*
90 |
91 | # Visual C++ cache files
92 | ipch/
93 | *.aps
94 | *.ncb
95 | *.opendb
96 | *.opensdf
97 | *.sdf
98 | *.cachefile
99 | *.VC.db
100 | *.VC.VC.opendb
101 |
102 | # Visual Studio profiler
103 | *.psess
104 | *.vsp
105 | *.vspx
106 | *.sap
107 |
108 | # Visual Studio Trace Files
109 | *.e2e
110 |
111 | # TFS 2012 Local Workspace
112 | $tf/
113 |
114 | # Guidance Automation Toolkit
115 | *.gpState
116 |
117 | # ReSharper is a .NET coding add-in
118 | _ReSharper*/
119 | *.[Rr]e[Ss]harper
120 | *.DotSettings.user
121 |
122 | # JustCode is a .NET coding add-in
123 | .JustCode
124 |
125 | # TeamCity is a build add-in
126 | _TeamCity*
127 |
128 | # DotCover is a Code Coverage Tool
129 | *.dotCover
130 |
131 | # AxoCover is a Code Coverage Tool
132 | .axoCover/*
133 | !.axoCover/settings.json
134 |
135 | # Visual Studio code coverage results
136 | *.coverage
137 | *.coveragexml
138 |
139 | # NCrunch
140 | _NCrunch_*
141 | .*crunch*.local.xml
142 | nCrunchTemp_*
143 |
144 | # MightyMoose
145 | *.mm.*
146 | AutoTest.Net/
147 |
148 | # Web workbench (sass)
149 | .sass-cache/
150 |
151 | # Installshield output folder
152 | [Ee]xpress/
153 |
154 | # DocProject is a documentation generator add-in
155 | DocProject/buildhelp/
156 | DocProject/Help/*.HxT
157 | DocProject/Help/*.HxC
158 | DocProject/Help/*.hhc
159 | DocProject/Help/*.hhk
160 | DocProject/Help/*.hhp
161 | DocProject/Help/Html2
162 | DocProject/Help/html
163 |
164 | # Click-Once directory
165 | publish/
166 |
167 | # Publish Web Output
168 | *.[Pp]ublish.xml
169 | *.azurePubxml
170 | # Note: Comment the next line if you want to checkin your web deploy settings,
171 | # but database connection strings (with potential passwords) will be unencrypted
172 | *.pubxml
173 | *.publishproj
174 |
175 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
176 | # checkin your Azure Web App publish settings, but sensitive information contained
177 | # in these scripts will be unencrypted
178 | PublishScripts/
179 |
180 | # NuGet Packages
181 | *.nupkg
182 | # The packages folder can be ignored because of Package Restore
183 | **/[Pp]ackages/*
184 | # except build/, which is used as an MSBuild target.
185 | !**/[Pp]ackages/build/
186 | # Uncomment if necessary however generally it will be regenerated when needed
187 | #!**/[Pp]ackages/repositories.config
188 | # NuGet v3's project.json files produces more ignorable files
189 | *.nuget.props
190 | *.nuget.targets
191 |
192 | # Microsoft Azure Build Output
193 | csx/
194 | *.build.csdef
195 |
196 | # Microsoft Azure Emulator
197 | ecf/
198 | rcf/
199 |
200 | # Windows Store app package directories and files
201 | AppPackages/
202 | BundleArtifacts/
203 | Package.StoreAssociation.xml
204 | _pkginfo.txt
205 | *.appx
206 |
207 | # Visual Studio cache files
208 | # files ending in .cache can be ignored
209 | *.[Cc]ache
210 | # but keep track of directories ending in .cache
211 | !*.[Cc]ache/
212 |
213 | # Others
214 | ClientBin/
215 | ~$*
216 | *~
217 | *.dbmdl
218 | *.dbproj.schemaview
219 | *.jfm
220 | *.pfx
221 | *.publishsettings
222 | orleans.codegen.cs
223 |
224 | # Including strong name files can present a security risk
225 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
226 | #*.snk
227 |
228 | # Since there are multiple workflows, uncomment next line to ignore bower_components
229 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
230 | #bower_components/
231 |
232 | # RIA/Silverlight projects
233 | Generated_Code/
234 |
235 | # Backup & report files from converting an old project file
236 | # to a newer Visual Studio version. Backup files are not needed,
237 | # because we have git ;-)
238 | _UpgradeReport_Files/
239 | Backup*/
240 | UpgradeLog*.XML
241 | UpgradeLog*.htm
242 | ServiceFabricBackup/
243 |
244 | # SQL Server files
245 | *.mdf
246 | *.ldf
247 | *.ndf
248 |
249 | # Business Intelligence projects
250 | *.rdl.data
251 | *.bim.layout
252 | *.bim_*.settings
253 | *.rptproj.rsuser
254 |
255 | # Microsoft Fakes
256 | FakesAssemblies/
257 |
258 | # GhostDoc plugin setting file
259 | *.GhostDoc.xml
260 |
261 | # Node.js Tools for Visual Studio
262 | .ntvs_analysis.dat
263 | node_modules/
264 |
265 | # Visual Studio 6 build log
266 | *.plg
267 |
268 | # Visual Studio 6 workspace options file
269 | *.opt
270 |
271 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
272 | *.vbw
273 |
274 | # Visual Studio LightSwitch build output
275 | **/*.HTMLClient/GeneratedArtifacts
276 | **/*.DesktopClient/GeneratedArtifacts
277 | **/*.DesktopClient/ModelManifest.xml
278 | **/*.Server/GeneratedArtifacts
279 | **/*.Server/ModelManifest.xml
280 | _Pvt_Extensions
281 |
282 | # Paket dependency manager
283 | .paket/paket.exe
284 | paket-files/
285 |
286 | # FAKE - F# Make
287 | .fake/
288 |
289 | # JetBrains Rider
290 | .idea/
291 | *.sln.iml
292 |
293 | # CodeRush
294 | .cr/
295 |
296 | # Python Tools for Visual Studio (PTVS)
297 | __pycache__/
298 | *.pyc
299 |
300 | # Cake - Uncomment if you are using it
301 | # tools/**
302 | # !tools/packages.config
303 |
304 | # Tabs Studio
305 | *.tss
306 |
307 | # Telerik's JustMock configuration file
308 | *.jmconfig
309 |
310 | # BizTalk build output
311 | *.btp.cs
312 | *.btm.cs
313 | *.odx.cs
314 | *.xsd.cs
315 |
316 | # OpenCover UI analysis results
317 | OpenCover/
318 |
319 | # Azure Stream Analytics local run output
320 | ASALocalRun/
321 |
322 | # MSBuild Binary and Structured Log
323 | *.binlog
324 |
325 | # NVidia Nsight GPU debugger configuration file
326 | *.nvuser
327 |
328 | # MFractors (Xamarin productivity tool) working folder
329 | .mfractor/
--------------------------------------------------------------------------------
/RedisMultilevelCache/MultilevelCacheProvider.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics;
3 | using System.IO;
4 | using System.Runtime.Caching;
5 | using StackExchange.Redis;
6 |
7 |
8 | namespace RedisMultilevelCache
9 | {
10 | public class MultilevelCacheProvider
11 | {
12 | private const int HASH_SLOT_COUNT = 16384;
13 | private const string SYNC_CHANNEL_NAME = "RedisMultilevelCache_Sync";
14 |
15 | private class LocalCacheEntry
16 | {
17 | ///
18 | /// Hashslot of this key this value is stored under
19 | ///
20 | public ushort KeyHashSlot { get; private set; }
21 | ///
22 | /// Time this entry was written to the cache
23 | ///
24 | public long Timestamp { get; private set; }
25 | ///
26 | /// Actual data value associated with this key
27 | ///
28 | public T Data { get; private set; }
29 |
30 | ///
31 | /// Public constructor
32 | ///
33 | /// Hashslot of the key associated with this entry
34 | /// Time the entry was written to the cache
35 | /// Actual data to associated with the key
36 | public LocalCacheEntry(ushort keyHashSlot, long timestamp, T data)
37 | {
38 | KeyHashSlot = keyHashSlot;
39 | Timestamp = timestamp;
40 | Data = data;
41 | }
42 | }
43 |
44 | ///
45 | /// This GUID is used to uniqely identify the cache instance
46 | ///
47 | private readonly Guid _instanceId = Guid.NewGuid();
48 | ///
49 | /// Serialization provider to encode/decode byte arrays sent to Redis
50 | ///
51 | private readonly ISerializationProvider _serializationProvider;
52 |
53 | ///
54 | /// This array stores the timestamp that a key with the corresponding hash slot was last updated
55 | ///
56 | ///
57 | /// .Net will initialize all items to zero, which is a fine default for us
58 | ///
59 | private readonly long[] _lastUpdated = new long[HASH_SLOT_COUNT]; // the array element index is the hash slot; for hash slot 3142 look up _lastUpdatedDictionary[3142]
60 |
61 | ///
62 | /// StackExchange.Redis connection to the Redis server
63 | ///
64 | private readonly ConnectionMultiplexer _redisConnection;
65 |
66 | ///
67 | /// StackExchange.Redis database
68 | ///
69 | private readonly IDatabase _redisDb;
70 |
71 | ///
72 | /// In-process cache used as the L1 cache
73 | ///
74 | private readonly MemoryCache _inProcessCache = MemoryCache.Default;
75 |
76 |
77 | ///
78 | /// Public constructor
79 | ///
80 | /// StackExchange.Redis connection string
81 | public MultilevelCacheProvider(string redisConnectionString) : this(redisConnectionString, new DefaultSerializationProvider())
82 | {
83 | }
84 |
85 |
86 | ///
87 | /// Public constructor
88 | ///
89 | /// StackExchange.Redis connection string
90 | /// Alternate serialization provider
91 | public MultilevelCacheProvider(string redisConnectionString, ISerializationProvider serializationProvider)
92 | {
93 | _serializationProvider = serializationProvider ?? throw new ArgumentNullException(nameof(serializationProvider));
94 |
95 | _redisConnection = ConnectionMultiplexer.Connect(redisConnectionString);
96 | _redisConnection.PreserveAsyncOrder = false; // This improves performance since message order isn't important for us; see https://stackexchange.github.io/StackExchange.Redis/PubSubOrder.html
97 | _redisDb = _redisConnection.GetDatabase();
98 |
99 | // wire up Redis pub/sub for cache synchronization messages:
100 |
101 | _redisConnection.GetSubscriber().Subscribe(SYNC_CHANNEL_NAME, DataSynchronizationMessageHandler);
102 | }
103 |
104 |
105 | ///
106 | /// Message handler for hash key invalidation messages
107 | ///
108 | /// Redis pub/sub channel name
109 | /// Pub/sub message
110 | private void DataSynchronizationMessageHandler(RedisChannel channel, RedisValue message)
111 | {
112 | // Early out, if channel name doesn't match our sync channel
113 |
114 | if (string.Compare(channel, SYNC_CHANNEL_NAME, StringComparison.InvariantCultureIgnoreCase) != 0)
115 | {
116 | return;
117 | }
118 |
119 | // otherwise, deserialize the message
120 |
121 | var dataSyncMessage = DataSyncMessage.Deserialize(message);
122 |
123 | // and update the appropriate _lastUpdated element with the current timestamp
124 | // Invalidate.Exchange() would make more sense here, but the lock statement
125 | // makes the purpose more evident for an example
126 |
127 | lock (_lastUpdated)
128 | {
129 | _lastUpdated[dataSyncMessage.KeyHashSlot] = Stopwatch.GetTimestamp();
130 | }
131 | }
132 |
133 |
134 | ///
135 | /// Add/update an entry in the cache
136 | ///
137 | ///
138 | /// Key used to locate value in the cache
139 | ///
140 | ///
141 | public void Set(string key, T value, TimeSpan ttl)
142 | {
143 | // parameter validation
144 |
145 | if (string.IsNullOrWhiteSpace(key)) throw new ArgumentNullException(nameof(key));
146 | if (value == null) throw new ArgumentNullException(nameof(value));
147 | if (ttl.TotalSeconds < 1) throw new ArgumentNullException(nameof(ttl));
148 |
149 | // Get the current timestamp before we do anything else. Decrement it by one so any sync messages processed during
150 | // this method call will force an immediate expiration of the key's hash slot
151 |
152 | var timestamp = Stopwatch.GetTimestamp() - 1;
153 |
154 | var keyHashSlot = HashSlotCalculator.CalculateHashSlot(key);
155 |
156 | byte[] serializedData = null;
157 |
158 | // Serialize the data and write to Redis
159 |
160 | using (MemoryStream ms = new MemoryStream())
161 | {
162 | _serializationProvider.Serialize(ms, value);
163 | serializedData = ms.ToArray();
164 | }
165 |
166 | // Execute the Redis SET and PUBLISH operations in one round trip using Lua
167 | // (Could use StackExchange.Redis batch here, instead)
168 |
169 | string luaScript = @"
170 | redis.call('SET', KEYS[1], ARGV[1], 'EX', ARGV[2])
171 | redis.call('PUBLISH', ARGV[3], ARGV[4])
172 | ";
173 |
174 | var scriptArgs = new RedisValue[4];
175 | scriptArgs[0] = serializedData;
176 | scriptArgs[1] = ttl.TotalSeconds;
177 | scriptArgs[2] = SYNC_CHANNEL_NAME;
178 | scriptArgs[3] = DataSyncMessage.Create(_instanceId, keyHashSlot).Serialize();
179 |
180 | _redisDb.ScriptEvaluate(luaScript, new RedisKey[] { key }, scriptArgs);
181 |
182 | // Update the in-process cache
183 |
184 | _inProcessCache.Set(key, new LocalCacheEntry(keyHashSlot, timestamp, value), DateTimeOffset.UtcNow.Add(ttl));
185 | }
186 |
187 |
188 | ///
189 | /// Retrieve an item from the cache
190 | ///
191 | /// Object type to return
192 | /// Key
193 | /// Object of type T associated with the key
194 | public T Get(string key)
195 | {
196 | // Get the current timestamp before we do anything else. Decrement it by one so any sync messages processed during
197 | // this method call will force an immediate expiration of the key's hash slot
198 |
199 | var timestamp = Stopwatch.GetTimestamp() - 1;
200 |
201 | int keyHashSlot = -1; // -1 is used as a sentinel value used to determine if the key's hash slot has already been computed
202 |
203 | // attempt to retreive value from the in-process cache
204 |
205 | var inProcessCacheEntry = _inProcessCache.Get(key) as LocalCacheEntry;
206 |
207 | if (inProcessCacheEntry != null)
208 | {
209 | // found the entry in the in-process cache, now
210 | // need to check if the entry may be stale and we
211 | // need to re-read from Redis
212 |
213 | keyHashSlot = inProcessCacheEntry.KeyHashSlot;
214 |
215 | // Check the timestamp of the cache entry versus the _lastUpdated array
216 | // If the timestamp of the cache entry is greater than what's in _lastUpdated,
217 | // then we can just return the value from the in-process cache
218 |
219 | // Could use and Interlocked operation here, but lock is a more obvious in intent
220 |
221 | lock (_lastUpdated)
222 | {
223 | if (_lastUpdated[keyHashSlot] < inProcessCacheEntry.Timestamp)
224 | {
225 | return (T) inProcessCacheEntry.Data;
226 | }
227 | }
228 | }
229 |
230 | // if we've made it to here, the key is either not in the in-process cache or
231 | // the in-process cache entry is stale. In either case we need to hit Redis to
232 | // get the correct value, and the key's remaining TTL in Redis
233 |
234 | T value = default(T);
235 |
236 | string luaScript = @"
237 | local result={}
238 | result[1] = redis.call('GET', KEYS[1])
239 | result[2] = redis.call('TTL', KEYS[1])
240 | return result;
241 | ";
242 |
243 | RedisValue[] results = (RedisValue[]) _redisDb.ScriptEvaluate(luaScript, new RedisKey[] { key });
244 |
245 | if (!results[0].IsNull)
246 | {
247 | var serializedData = (byte[])results[0];
248 |
249 | if (serializedData.Length > 0)
250 | {
251 | // Deserialize the bytes returned from Redis
252 |
253 | using (MemoryStream ms = new MemoryStream(serializedData))
254 | {
255 | value = _serializationProvider.Deserialize(ms);
256 | }
257 |
258 | // Don't want to have to recalculate the hashslot twice, so test if it's already
259 | // been computed
260 |
261 | if (keyHashSlot == -1)
262 | {
263 | keyHashSlot = HashSlotCalculator.CalculateHashSlot(key);
264 | }
265 |
266 | // Update the in-proces cache with the value retrieved from Redis
267 |
268 | _inProcessCache.Set(key, new LocalCacheEntry((ushort) keyHashSlot, timestamp, value), DateTimeOffset.UtcNow.AddSeconds((double) results[1]));
269 | }
270 | }
271 |
272 | return (T) inProcessCacheEntry.Data;
273 | }
274 |
275 |
276 | }
277 | }
278 |
--------------------------------------------------------------------------------