@@ -16,26 +16,42 @@ namespace Microsoft.Extensions.Caching.StackExchangeRedis;
1616/// </summary>
1717public class RedisCache : IDistributedCache , IDisposable
1818{
19+ // -- Explanation of why two kinds of SetScript are used --
20+ // * Redis 2.0 had HSET key field value for setting individual hash fields,
21+ // and HMSET key field value [field value ...] for setting multiple hash fields (against the same key)
22+ // * Redis 4.0 observes this redundancy, and adds HSET key field value [field value ...],
23+ // and also marks HMSET as deprecated, although it still works fine.
24+ // But HSET doesn't allows multiple field/value pairs for Redis prior 4.0.
25+
1926 // KEYS[1] = = key
2027 // ARGV[1] = absolute-expiration - ticks as long (-1 for none)
2128 // ARGV[2] = sliding-expiration - ticks as long (-1 for none)
2229 // ARGV[3] = relative-expiration (long, in seconds, -1 for none) - Min(absolute-expiration - Now, sliding-expiration)
2330 // ARGV[4] = data - byte[]
2431 // this order should not change LUA script depends on it
25- private const string SetScript = ( @"
32+ private const string SetScriptPerExtendedSetCommand = ( @"
2633 redis.call('HSET', KEYS[1], 'absexp', ARGV[1], 'sldexp', ARGV[2], 'data', ARGV[4])
2734 if ARGV[3] ~= '-1' then
2835 redis.call('EXPIRE', KEYS[1], ARGV[3])
2936 end
3037 return 1" ) ;
38+ private const string DeprecatedSetScript = ( @"
39+ redis.call('HMSET', KEYS[1], 'absexp', ARGV[1], 'sldexp', ARGV[2], 'data', ARGV[4])
40+ if ARGV[3] ~= '-1' then
41+ redis.call('EXPIRE', KEYS[1], ARGV[3])
42+ end
43+ return 1" ) ;
44+
3145 private const string AbsoluteExpirationKey = "absexp" ;
3246 private const string SlidingExpirationKey = "sldexp" ;
3347 private const string DataKey = "data" ;
3448 private const long NotPresent = - 1 ;
49+ private static readonly Version ServerVersionWithExtendedSetCommand = new Version ( 4 , 0 , 0 ) ;
3550
3651 private volatile IConnectionMultiplexer _connection ;
3752 private IDatabase _cache ;
3853 private bool _disposed ;
54+ private string _setScript = SetScriptPerExtendedSetCommand ;
3955
4056 private readonly RedisCacheOptions _options ;
4157 private readonly string _instance ;
@@ -107,7 +123,7 @@ public void Set(string key, byte[] value, DistributedCacheEntryOptions options)
107123
108124 var absoluteExpiration = GetAbsoluteExpiration ( creationTime , options ) ;
109125
110- var result = _cache . ScriptEvaluate ( SetScript , new RedisKey [ ] { _instance + key } ,
126+ var result = _cache . ScriptEvaluate ( _setScript , new RedisKey [ ] { _instance + key } ,
111127 new RedisValue [ ]
112128 {
113129 absoluteExpiration ? . Ticks ?? NotPresent ,
@@ -143,7 +159,7 @@ public void Set(string key, byte[] value, DistributedCacheEntryOptions options)
143159
144160 var absoluteExpiration = GetAbsoluteExpiration ( creationTime , options ) ;
145161
146- await _cache . ScriptEvaluateAsync ( SetScript , new RedisKey [ ] { _instance + key } ,
162+ await _cache . ScriptEvaluateAsync ( _setScript , new RedisKey [ ] { _instance + key } ,
147163 new RedisValue [ ]
148164 {
149165 absoluteExpiration ? . Ticks ?? NotPresent ,
@@ -206,7 +222,7 @@ private void Connect()
206222 _connection = _options . ConnectionMultiplexerFactory ( ) . GetAwaiter ( ) . GetResult ( ) ;
207223 }
208224
209- TryRegisterProfiler ( ) ;
225+ PrepareConnection ( ) ;
210226 _cache = _connection . GetDatabase ( ) ;
211227 }
212228 }
@@ -247,7 +263,7 @@ private void Connect()
247263 _connection = await _options . ConnectionMultiplexerFactory ( ) ;
248264 }
249265
250- TryRegisterProfiler ( ) ;
266+ PrepareConnection ( ) ;
251267 _cache = _connection . GetDatabase ( ) ;
252268 }
253269 }
@@ -257,9 +273,31 @@ private void Connect()
257273 }
258274 }
259275
276+ private void PrepareConnection ( )
277+ {
278+ ValidateServerFeatures ( ) ;
279+ TryRegisterProfiler ( ) ;
280+ }
281+
282+ private void ValidateServerFeatures ( )
283+ {
284+ _ = _connection ?? throw new InvalidOperationException ( $ "{ nameof ( _connection ) } cannot be null.") ;
285+
286+ foreach ( var endPoint in _connection . GetEndPoints ( ) )
287+ {
288+ if ( _connection . GetServer ( endPoint ) . Version < ServerVersionWithExtendedSetCommand )
289+ {
290+ _setScript = DeprecatedSetScript ;
291+ return ;
292+ }
293+ }
294+ }
295+
260296 private void TryRegisterProfiler ( )
261297 {
262- if ( _connection != null && _options . ProfilingSession != null )
298+ _ = _connection ?? throw new InvalidOperationException ( $ "{ nameof ( _connection ) } cannot be null.") ;
299+
300+ if ( _options . ProfilingSession != null )
263301 {
264302 _connection . RegisterProfiler ( _options . ProfilingSession ) ;
265303 }
0 commit comments