|
30 | 30 | /**
|
31 | 31 | * {@link RedisCacheWriter} implementation capable of reading/writing binary data from/to Redis in {@literal standalone}
|
32 | 32 | * and {@literal cluster} environments. Works upon a given {@link RedisConnectionFactory} to obtain the actual
|
33 |
| - * {@link RedisConnection}. |
| 33 | + * {@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. |
34 | 40 | *
|
35 | 41 | * @author Christoph Strobl
|
36 | 42 | * @since 2.0
|
37 | 43 | */
|
38 |
| -class DefaultRedisCacheWriter implements RedisCacheWriter { |
| 44 | +public class DefaultRedisCacheWriter implements RedisCacheWriter { |
39 | 45 |
|
40 | 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;"
|
41 | 47 | .getBytes(Charset.forName("UTF-8"));
|
@@ -64,12 +70,24 @@ class DefaultRedisCacheWriter implements RedisCacheWriter {
|
64 | 70 | this.sleepTime = sleepTime;
|
65 | 71 | }
|
66 | 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 | + */ |
67 | 79 | public static DefaultRedisCacheWriter nonLockingRedisCacheWriter(RedisConnectionFactory connectionFactory) {
|
68 | 80 |
|
69 | 81 | Assert.notNull(connectionFactory, "ConnectionFactory must not be null!");
|
70 | 82 | return new DefaultRedisCacheWriter(connectionFactory);
|
71 | 83 | }
|
72 | 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}. |
| 90 | + */ |
73 | 91 | public static DefaultRedisCacheWriter lockingRedisCacheWriter(RedisConnectionFactory connectionFactory) {
|
74 | 92 |
|
75 | 93 | Assert.notNull(connectionFactory, "ConnectionFactory must not be null!");
|
@@ -115,15 +133,26 @@ public byte[] putIfAbsent(String name, byte[] key, byte[] value, Duration ttl) {
|
115 | 133 |
|
116 | 134 | return execute(name, connection -> {
|
117 | 135 |
|
118 |
| - if (connection.setNX(key, value)) { |
| 136 | + if (isLockingCacheWriter()) { |
| 137 | + doLock(name, connection); |
| 138 | + } |
119 | 139 |
|
120 |
| - if (shouldExpireWithin(ttl)) { |
121 |
| - connection.pExpire(key, ttl.toMillis()); |
| 140 | + try { |
| 141 | + if (connection.setNX(key, value)) { |
| 142 | + |
| 143 | + if (shouldExpireWithin(ttl)) { |
| 144 | + connection.pExpire(key, ttl.toMillis()); |
| 145 | + } |
| 146 | + return null; |
122 | 147 | }
|
123 |
| - return null; |
124 |
| - } |
125 | 148 |
|
126 |
| - return connection.get(key); |
| 149 | + return connection.get(key); |
| 150 | + } finally { |
| 151 | + |
| 152 | + if (isLockingCacheWriter()) { |
| 153 | + doUnlock(name, connection); |
| 154 | + } |
| 155 | + } |
127 | 156 | });
|
128 | 157 | }
|
129 | 158 |
|
@@ -184,21 +213,24 @@ public void clean(String name, byte[] pattern) {
|
184 | 213 |
|
185 | 214 | execute(name, connection -> {
|
186 | 215 |
|
187 |
| - if (isLockingCacheWriter()) { |
188 |
| - doLock(name, connection); |
189 |
| - } |
| 216 | + boolean wasLocked = false; |
190 | 217 |
|
191 | 218 | try {
|
192 | 219 | if (connection instanceof RedisClusterConnection) {
|
193 | 220 |
|
| 221 | + if (isLockingCacheWriter()) { |
| 222 | + doLock(name, connection); |
| 223 | + wasLocked = true; |
| 224 | + } |
| 225 | + |
194 | 226 | byte[][] keys = connection.keys(pattern).stream().toArray(size -> new byte[size][]);
|
195 | 227 | connection.del(keys);
|
196 | 228 | } else {
|
197 | 229 | connection.eval(CLEAN_SCRIPT, ReturnType.INTEGER, 0, pattern);
|
198 | 230 | }
|
199 | 231 | } finally {
|
200 | 232 |
|
201 |
| - if (isLockingCacheWriter()) { |
| 233 | + if (wasLocked && isLockingCacheWriter()) { |
202 | 234 | doUnlock(name, connection);
|
203 | 235 | }
|
204 | 236 | }
|
@@ -262,11 +294,12 @@ byte[] createCacheLockKey(String name) {
|
262 | 294 | }
|
263 | 295 |
|
264 | 296 | /**
|
265 |
| - * @author Christoph Strobl |
266 | 297 | * @param <T>
|
| 298 | + * @author Christoph Strobl |
267 | 299 | * @since 2.0
|
268 | 300 | */
|
269 | 301 | interface ConnectionCallback<T> {
|
| 302 | + |
270 | 303 | T doWithConnection(RedisConnection connection);
|
271 | 304 | }
|
272 | 305 | }
|
0 commit comments