15
15
*/
16
16
package org .springframework .data .redis .cache ;
17
17
18
- import java .nio .charset .Charset ;
18
+ import java .nio .charset .StandardCharsets ;
19
19
import java .time .Duration ;
20
20
import java .util .concurrent .TimeUnit ;
21
+ import java .util .function .Consumer ;
22
+ import java .util .function .Function ;
21
23
22
- import org .springframework .data . redis . connection . RedisClusterConnection ;
24
+ import org .springframework .dao . PessimisticLockingFailureException ;
23
25
import org .springframework .data .redis .connection .RedisConnection ;
24
26
import org .springframework .data .redis .connection .RedisConnectionFactory ;
25
27
import org .springframework .data .redis .connection .RedisStringCommands .SetOption ;
26
- import org .springframework .data .redis .connection .ReturnType ;
27
28
import org .springframework .data .redis .core .types .Expiration ;
28
29
import org .springframework .util .Assert ;
29
30
30
31
/**
31
32
* {@link RedisCacheWriter} implementation capable of reading/writing binary data from/to Redis in {@literal standalone}
32
33
* and {@literal cluster} environments. Works upon a given {@link RedisConnectionFactory} to obtain the actual
33
34
* {@link RedisConnection}. <br />
34
- * {@link DefaultRedisCacheWriter} can be used in {@link #lockingRedisCacheWriter(RedisConnectionFactory) locking} or
35
- * {@link #nonLockingRedisCacheWriter(RedisConnectionFactory) non-locking} mode. While {@literal non-locking} aims for
36
- * maximum performance it may result in overlapping, non atomic, command execution for operations spanning multiple
37
- * Redis interactions like {@code putIfAbsent}. The {@literal locking} counterpart prevents command overlap by setting
38
- * an explicit lock key and checking against presence of this key which leads to additional requests and potential
39
- * command wait times.
35
+ * {@link DefaultRedisCacheWriter} can be used in
36
+ * {@link RedisCacheWriter#lockingRedisCacheWriter(RedisConnectionFactory) locking} or
37
+ * {@link RedisCacheWriter#nonLockingRedisCacheWriter(RedisConnectionFactory) non-locking} mode. While
38
+ * {@literal non-locking} aims for maximum performance it may result in overlapping, non atomic, command execution for
39
+ * operations spanning multiple Redis interactions like {@code putIfAbsent}. The {@literal locking} counterpart prevents
40
+ * command overlap by setting an explicit lock key and checking against presence of this key which leads to additional
41
+ * requests and potential command wait times.
40
42
*
41
43
* @author Christoph Strobl
44
+ * @author Mark Paluch
42
45
* @since 2.0
43
46
*/
44
- public class DefaultRedisCacheWriter implements RedisCacheWriter {
45
-
46
- private static final byte [] CLEAN_SCRIPT = "local keys = redis.call('KEYS', ARGV[1]); local keysCount = table.getn(keys); if(keysCount > 0) then for _, key in ipairs(keys) do redis.call('del', key); end; end; return keysCount;"
47
- .getBytes (Charset .forName ("UTF-8" ));
47
+ class DefaultRedisCacheWriter implements RedisCacheWriter {
48
48
49
49
private final RedisConnectionFactory connectionFactory ;
50
50
private final Duration sleepTime ;
@@ -70,30 +70,10 @@ public class DefaultRedisCacheWriter implements RedisCacheWriter {
70
70
this .sleepTime = sleepTime ;
71
71
}
72
72
73
- /**
74
- * Create new {@link RedisCacheWriter} without locking behavior.
75
- *
76
- * @param connectionFactory must not be {@literal null}.
77
- * @return new instance of {@link DefaultRedisCacheWriter}.
78
- */
79
- public static DefaultRedisCacheWriter nonLockingRedisCacheWriter (RedisConnectionFactory connectionFactory ) {
80
-
81
- Assert .notNull (connectionFactory , "ConnectionFactory must not be null!" );
82
- return new DefaultRedisCacheWriter (connectionFactory );
83
- }
84
-
85
- /**
86
- * Create new {@link RedisCacheWriter} with locking behavior.
87
- *
88
- * @param connectionFactory must not be {@literal null}.
89
- * @return new instance of {@link DefaultRedisCacheWriter}.
73
+ /*
74
+ * (non-Javadoc)
75
+ * @see org.springframework.data.redis.cache.RedisCacheWriter#put(java.lang.String, byte[], byte[], java.time.Duration)
90
76
*/
91
- public static DefaultRedisCacheWriter lockingRedisCacheWriter (RedisConnectionFactory connectionFactory ) {
92
-
93
- Assert .notNull (connectionFactory , "ConnectionFactory must not be null!" );
94
- return new DefaultRedisCacheWriter (connectionFactory , Duration .ofMillis (50 ));
95
- }
96
-
97
77
@ Override
98
78
public void put (String name , byte [] key , byte [] value , Duration ttl ) {
99
79
@@ -114,6 +94,10 @@ public void put(String name, byte[] key, byte[] value, Duration ttl) {
114
94
});
115
95
}
116
96
97
+ /*
98
+ * (non-Javadoc)
99
+ * @see org.springframework.data.redis.cache.RedisCacheWriter#get(java.lang.String, byte[])
100
+ */
117
101
@ Override
118
102
public byte [] get (String name , byte [] key ) {
119
103
@@ -123,6 +107,10 @@ public byte[] get(String name, byte[] key) {
123
107
return execute (name , connection -> connection .get (key ));
124
108
}
125
109
110
+ /*
111
+ * (non-Javadoc)
112
+ * @see org.springframework.data.redis.cache.RedisCacheWriter#putIfAbsent(java.lang.String, byte[], byte[], java.time.Duration)
113
+ */
126
114
@ Override
127
115
public byte [] putIfAbsent (String name , byte [] key , byte [] value , Duration ttl ) {
128
116
@@ -156,6 +144,10 @@ public byte[] putIfAbsent(String name, byte[] key, byte[] value, Duration ttl) {
156
144
});
157
145
}
158
146
147
+ /*
148
+ * (non-Javadoc)
149
+ * @see org.springframework.data.redis.cache.RedisCacheWriter#remove(java.lang.String, byte[])
150
+ */
159
151
@ Override
160
152
public void remove (String name , byte [] key ) {
161
153
@@ -165,46 +157,10 @@ public void remove(String name, byte[] key) {
165
157
execute (name , connection -> connection .del (key ));
166
158
}
167
159
168
- /**
169
- * Explicitly set a write lock on a cache.
170
- *
171
- * @param name the name of the cache to lock.
172
- */
173
- public void lock (String name ) {
174
- execute (name , connection -> doLock (name , connection ));
175
- }
176
-
177
- private Boolean doLock (String name , RedisConnection connection ) {
178
- return connection .setNX (createCacheLockKey (name ), new byte [] {});
179
- }
180
-
181
- /**
182
- * Explicitly remove a write lock from a cache.
183
- *
184
- * @param name the name of the cache to unlock.
185
- */
186
- public void unlock (String name ) {
187
- executeWithoutLockCheck (connection -> doUnlock (name , connection ));
188
- }
189
-
190
- private Long doUnlock (String name , RedisConnection connection ) {
191
- return connection .del (createCacheLockKey (name ));
192
- }
193
-
194
- /**
195
- * Check if a cache has set a lock.
196
- *
197
- * @param name the name of the cache to check for presence of a lock.
198
- * @return {@literal true} if lock found.
160
+ /*
161
+ * (non-Javadoc)
162
+ * @see org.springframework.data.redis.cache.RedisCacheWriter#clean(java.lang.String, byte[])
199
163
*/
200
- public boolean isLoked (String name ) {
201
- return executeWithoutLockCheck (connection -> doCheckLock (name , connection ));
202
- }
203
-
204
- private boolean doCheckLock (String name , RedisConnection connection ) {
205
- return connection .exists (createCacheLockKey (name ));
206
- }
207
-
208
164
@ Override
209
165
public void clean (String name , byte [] pattern ) {
210
166
@@ -216,17 +172,16 @@ public void clean(String name, byte[] pattern) {
216
172
boolean wasLocked = false ;
217
173
218
174
try {
219
- if (connection instanceof RedisClusterConnection ) {
220
175
221
- if (isLockingCacheWriter ()) {
222
- doLock (name , connection );
223
- wasLocked = true ;
224
- }
176
+ if (isLockingCacheWriter ()) {
177
+ doLock (name , connection );
178
+ wasLocked = true ;
179
+ }
180
+
181
+ byte [][] keys = connection .keys (pattern ).toArray (new byte [0 ][]);
225
182
226
- byte [][] keys = connection . keys ( pattern ). stream (). toArray ( size -> new byte [ size ][]);
183
+ if ( keys . length > 0 ) {
227
184
connection .del (keys );
228
- } else {
229
- connection .eval (CLEAN_SCRIPT , ReturnType .INTEGER , 0 , pattern );
230
185
}
231
186
} finally {
232
187
@@ -239,67 +194,92 @@ public void clean(String name, byte[] pattern) {
239
194
});
240
195
}
241
196
197
+ /**
198
+ * Explicitly set a write lock on a cache.
199
+ *
200
+ * @param name the name of the cache to lock.
201
+ */
202
+ void lock (String name ) {
203
+ execute (name , connection -> doLock (name , connection ));
204
+ }
205
+
206
+ /**
207
+ * Explicitly remove a write lock from a cache.
208
+ *
209
+ * @param name the name of the cache to unlock.
210
+ */
211
+ void unlock (String name ) {
212
+ executeLockFree (connection -> doUnlock (name , connection ));
213
+ }
214
+
215
+ private Boolean doLock (String name , RedisConnection connection ) {
216
+ return connection .setNX (createCacheLockKey (name ), new byte [0 ]);
217
+ }
218
+
219
+ private Long doUnlock (String name , RedisConnection connection ) {
220
+ return connection .del (createCacheLockKey (name ));
221
+ }
222
+
223
+ boolean doCheckLock (String name , RedisConnection connection ) {
224
+ return connection .exists (createCacheLockKey (name ));
225
+ }
226
+
242
227
/**
243
228
* @return {@literal true} if {@link RedisCacheWriter} uses locks.
244
229
*/
245
- public boolean isLockingCacheWriter () {
230
+ private boolean isLockingCacheWriter () {
246
231
return !sleepTime .isZero () && !sleepTime .isNegative ();
247
232
}
248
233
249
- <T > T execute (String name , ConnectionCallback < T > callback ) {
234
+ private <T > T execute (String name , Function < RedisConnection , T > callback ) {
250
235
251
236
RedisConnection connection = connectionFactory .getConnection ();
252
237
try {
253
238
254
239
checkAndPotentiallyWaitUntilUnlocked (name , connection );
255
- return callback .doWithConnection (connection );
240
+ return callback .apply (connection );
256
241
} finally {
257
242
connection .close ();
258
243
}
259
244
}
260
245
261
- private < T > T executeWithoutLockCheck ( ConnectionCallback < T > callback ) {
246
+ private void executeLockFree ( Consumer < RedisConnection > callback ) {
262
247
263
248
RedisConnection connection = connectionFactory .getConnection ();
264
249
265
250
try {
266
- return callback .doWithConnection (connection );
251
+ callback .accept (connection );
267
252
} finally {
268
253
connection .close ();
269
254
}
270
255
}
271
256
272
257
private void checkAndPotentiallyWaitUntilUnlocked (String name , RedisConnection connection ) {
273
258
274
- if (isLockingCacheWriter ()) {
259
+ if (!isLockingCacheWriter ()) {
260
+ return ;
261
+ }
275
262
276
- long timeout = sleepTime . toMillis ();
263
+ try {
277
264
278
265
while (doCheckLock (name , connection )) {
279
- try {
280
- Thread .sleep (timeout );
281
- } catch (InterruptedException ex ) {
282
- Thread .currentThread ().interrupt ();
283
- }
266
+ Thread .sleep (sleepTime .toMillis ());
284
267
}
268
+ } catch (InterruptedException ex ) {
269
+
270
+ // Re-interrupt current thread, to allow other participants to react.
271
+ Thread .currentThread ().interrupt ();
272
+
273
+ throw new PessimisticLockingFailureException (String .format ("Interrupted while waiting to unlock cache %s" , name ),
274
+ ex );
285
275
}
286
276
}
287
277
288
- private boolean shouldExpireWithin (Duration ttl ) {
278
+ private static boolean shouldExpireWithin (Duration ttl ) {
289
279
return ttl != null && !ttl .isZero () && !ttl .isNegative ();
290
280
}
291
281
292
- byte [] createCacheLockKey (String name ) {
293
- return (name + "~lock" ).getBytes (Charset .forName ("UTF-8" ));
294
- }
295
-
296
- /**
297
- * @param <T>
298
- * @author Christoph Strobl
299
- * @since 2.0
300
- */
301
- interface ConnectionCallback <T > {
302
-
303
- T doWithConnection (RedisConnection connection );
282
+ private static byte [] createCacheLockKey (String name ) {
283
+ return (name + "~lock" ).getBytes (StandardCharsets .UTF_8 );
304
284
}
305
285
}
0 commit comments