├── 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 | --------------------------------------------------------------------------------