@@ -24,52 +24,23 @@ public partial class RedisCache : IDistributedCache, IDisposable
24
24
{
25
25
// Note that the "force reconnect" pattern as described https://learn.microsoft.com/en-us/azure/azure-cache-for-redis/cache-best-practices-connection#using-forcereconnect-with-stackexchangeredis
26
26
// can be enabled via the "Microsoft.AspNetCore.Caching.StackExchangeRedis.UseForceReconnect" app-context switch
27
- //
28
- // -- Explanation of why two kinds of SetScript are used --
29
- // * Redis 2.0 had HSET key field value for setting individual hash fields,
30
- // and HMSET key field value [field value ...] for setting multiple hash fields (against the same key).
31
- // * Redis 4.0 added HSET key field value [field value ...] and deprecated HMSET.
32
- //
33
- // On Redis versions that don't have the newer HSET variant, we use SetScriptPreExtendedSetCommand
34
- // which uses the (now deprecated) HMSET.
35
-
36
- // KEYS[1] = = key
37
- // ARGV[1] = absolute-expiration - ticks as long (-1 for none)
38
- // ARGV[2] = sliding-expiration - ticks as long (-1 for none)
39
- // ARGV[3] = relative-expiration (long, in seconds, -1 for none) - Min(absolute-expiration - Now, sliding-expiration)
40
- // ARGV[4] = data - byte[]
41
- // this order should not change LUA script depends on it
42
- private const string SetScript = ( @"
43
- redis.call('HSET', KEYS[1], 'absexp', ARGV[1], 'sldexp', ARGV[2], 'data', ARGV[4])
44
- if ARGV[3] ~= '-1' then
45
- redis.call('EXPIRE', KEYS[1], ARGV[3])
46
- end
47
- return 1" ) ;
48
- private const string SetScriptPreExtendedSetCommand = ( @"
49
- redis.call('HMSET', KEYS[1], 'absexp', ARGV[1], 'sldexp', ARGV[2], 'data', ARGV[4])
50
- if ARGV[3] ~= '-1' then
51
- redis.call('EXPIRE', KEYS[1], ARGV[3])
52
- end
53
- return 1" ) ;
54
27
55
28
private const string AbsoluteExpirationKey = "absexp" ;
56
29
private const string SlidingExpirationKey = "sldexp" ;
57
30
private const string DataKey = "data" ;
58
31
59
32
// combined keys - same hash keys fetched constantly; avoid allocating an array each time
60
- private static readonly RedisValue [ ] _hashMembersAbsoluteExpirationSlidingExpirationData = new RedisValue [ ] { AbsoluteExpirationKey , SlidingExpirationKey , DataKey } ;
61
- private static readonly RedisValue [ ] _hashMembersAbsoluteExpirationSlidingExpiration = new RedisValue [ ] { AbsoluteExpirationKey , SlidingExpirationKey } ;
33
+ private static readonly RedisValue [ ] _hashMembersAbsoluteExpirationSlidingExpirationData = [ AbsoluteExpirationKey , SlidingExpirationKey , DataKey ] ;
34
+ private static readonly RedisValue [ ] _hashMembersAbsoluteExpirationSlidingExpiration = [ AbsoluteExpirationKey , SlidingExpirationKey ] ;
62
35
63
36
private static RedisValue [ ] GetHashFields ( bool getData ) => getData
64
37
? _hashMembersAbsoluteExpirationSlidingExpirationData
65
38
: _hashMembersAbsoluteExpirationSlidingExpiration ;
66
39
67
40
private const long NotPresent = - 1 ;
68
- private static readonly Version ServerVersionWithExtendedSetCommand = new Version ( 4 , 0 , 0 ) ;
69
41
70
42
private volatile IDatabase ? _cache ;
71
43
private bool _disposed ;
72
- private string _setScript = SetScript ;
73
44
74
45
private readonly RedisCacheOptions _options ;
75
46
private readonly RedisKey _instancePrefix ;
@@ -169,14 +140,24 @@ public void Set(string key, byte[] value, DistributedCacheEntryOptions options)
169
140
170
141
try
171
142
{
172
- cache . ScriptEvaluate ( _setScript , new RedisKey [ ] { _instancePrefix . Append ( key ) } ,
173
- new RedisValue [ ]
174
- {
175
- absoluteExpiration ? . Ticks ?? NotPresent ,
176
- options . SlidingExpiration ? . Ticks ?? NotPresent ,
177
- GetExpirationInSeconds ( creationTime , absoluteExpiration , options ) ?? NotPresent ,
178
- value
179
- } ) ;
143
+ var prefixedKey = _instancePrefix . Append ( key ) ;
144
+ var ttl = GetExpirationInSeconds ( creationTime , absoluteExpiration , options ) ;
145
+ var fields = GetHashFields ( value , absoluteExpiration , options . SlidingExpiration ) ;
146
+
147
+ if ( ttl is null )
148
+ {
149
+ cache . HashSet ( prefixedKey , fields ) ;
150
+ }
151
+ else
152
+ {
153
+ // use the batch API to pipeline the two commands and wait synchronously;
154
+ // SE.Redis reuses the async API shape for this scenario
155
+ var batch = cache . CreateBatch ( ) ;
156
+ var setFields = batch . HashSetAsync ( prefixedKey , fields ) ;
157
+ var setTtl = batch . KeyExpireAsync ( prefixedKey , TimeSpan . FromSeconds ( ttl . GetValueOrDefault ( ) ) ) ;
158
+ batch . Execute ( ) ; // synchronous wait-for-all; the two tasks should be either complete or *literally about to* (race conditions)
159
+ cache . WaitAll ( setFields , setTtl ) ; // note this applies usual SE.Redis timeouts etc
160
+ }
180
161
}
181
162
catch ( Exception ex )
182
163
{
@@ -203,14 +184,21 @@ public async Task SetAsync(string key, byte[] value, DistributedCacheEntryOption
203
184
204
185
try
205
186
{
206
- await cache . ScriptEvaluateAsync ( _setScript , new RedisKey [ ] { _instancePrefix . Append ( key ) } ,
207
- new RedisValue [ ]
208
- {
209
- absoluteExpiration ? . Ticks ?? NotPresent ,
210
- options . SlidingExpiration ? . Ticks ?? NotPresent ,
211
- GetExpirationInSeconds ( creationTime , absoluteExpiration , options ) ?? NotPresent ,
212
- value
213
- } ) . ConfigureAwait ( false ) ;
187
+ var prefixedKey = _instancePrefix . Append ( key ) ;
188
+ var ttl = GetExpirationInSeconds ( creationTime , absoluteExpiration , options ) ;
189
+ var fields = GetHashFields ( value , absoluteExpiration , options . SlidingExpiration ) ;
190
+
191
+ if ( ttl is null )
192
+ {
193
+ await cache . HashSetAsync ( prefixedKey , fields ) . ConfigureAwait ( false ) ;
194
+ }
195
+ else
196
+ {
197
+ await Task . WhenAll (
198
+ cache . HashSetAsync ( prefixedKey , fields ) ,
199
+ cache . KeyExpireAsync ( prefixedKey , TimeSpan . FromSeconds ( ttl . GetValueOrDefault ( ) ) )
200
+ ) . ConfigureAwait ( false ) ;
201
+ }
214
202
}
215
203
catch ( Exception ex )
216
204
{
@@ -219,6 +207,13 @@ public async Task SetAsync(string key, byte[] value, DistributedCacheEntryOption
219
207
}
220
208
}
221
209
210
+ private static HashEntry [ ] GetHashFields ( RedisValue value , DateTimeOffset ? absoluteExpiration , TimeSpan ? slidingExpiration )
211
+ => [
212
+ new HashEntry ( AbsoluteExpirationKey , absoluteExpiration ? . Ticks ?? NotPresent ) ,
213
+ new HashEntry ( SlidingExpirationKey , slidingExpiration ? . Ticks ?? NotPresent ) ,
214
+ new HashEntry ( DataKey , value )
215
+ ] ;
216
+
222
217
/// <inheritdoc />
223
218
public void Refresh ( string key )
224
219
{
@@ -323,36 +318,10 @@ private async ValueTask<IDatabase> ConnectSlowAsync(CancellationToken token)
323
318
private void PrepareConnection ( IConnectionMultiplexer connection )
324
319
{
325
320
WriteTimeTicks ( ref _lastConnectTicks , DateTimeOffset . UtcNow ) ;
326
- ValidateServerFeatures ( connection ) ;
327
321
TryRegisterProfiler ( connection ) ;
328
322
TryAddSuffix ( connection ) ;
329
323
}
330
324
331
- private void ValidateServerFeatures ( IConnectionMultiplexer connection )
332
- {
333
- _ = connection ?? throw new InvalidOperationException ( $ "{ nameof ( connection ) } cannot be null.") ;
334
-
335
- try
336
- {
337
- foreach ( var endPoint in connection . GetEndPoints ( ) )
338
- {
339
- if ( connection . GetServer ( endPoint ) . Version < ServerVersionWithExtendedSetCommand )
340
- {
341
- _setScript = SetScriptPreExtendedSetCommand ;
342
- return ;
343
- }
344
- }
345
- }
346
- catch ( NotSupportedException ex )
347
- {
348
- Log . CouldNotDetermineServerVersion ( _logger , ex ) ;
349
-
350
- // The GetServer call may not be supported with some configurations, in which
351
- // case let's also fall back to using the older command.
352
- _setScript = SetScriptPreExtendedSetCommand ;
353
- }
354
- }
355
-
356
325
private void TryRegisterProfiler ( IConnectionMultiplexer connection )
357
326
{
358
327
_ = connection ?? throw new InvalidOperationException ( $ "{ nameof ( connection ) } cannot be null.") ;
@@ -372,7 +341,7 @@ private void TryAddSuffix(IConnectionMultiplexer connection)
372
341
}
373
342
catch ( Exception ex )
374
343
{
375
- Log . UnableToAddLibraryNameSuffix ( _logger , ex ) ; ;
344
+ Log . UnableToAddLibraryNameSuffix ( _logger , ex ) ; ;
376
345
}
377
346
}
378
347
@@ -557,6 +526,9 @@ private async Task RefreshAsync(IDatabase cache, string key, DateTimeOffset? abs
557
526
}
558
527
}
559
528
529
+ // it is not an oversight that this returns seconds rather than TimeSpan (which SE.Redis can accept directly); by
530
+ // leaving this as an integer, we use TTL rather than PTTL, which has better compatibility between servers
531
+ // (it also takes a handful fewer bytes, but that isn't a motivating factor)
560
532
private static long ? GetExpirationInSeconds ( DateTimeOffset creationTime , DateTimeOffset ? absoluteExpiration , DistributedCacheEntryOptions options )
561
533
{
562
534
if ( absoluteExpiration . HasValue && options . SlidingExpiration . HasValue )
0 commit comments