22
22
import java .util .List ;
23
23
import java .util .Map ;
24
24
import java .util .Map .Entry ;
25
+ import java .util .Objects ;
25
26
import java .util .Set ;
26
27
import java .util .concurrent .TimeUnit ;
27
28
import java .util .concurrent .atomic .AtomicReference ;
103
104
* @author Mark Paluch
104
105
* @author Andrey Muchnik
105
106
* @author John Blum
107
+ * @author Kim Sumin
106
108
* @since 1.7
107
109
*/
108
110
public class RedisKeyValueAdapter extends AbstractKeyValueAdapter
@@ -126,6 +128,7 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter
126
128
private EnableKeyspaceEvents enableKeyspaceEvents = EnableKeyspaceEvents .OFF ;
127
129
private @ Nullable String keyspaceNotificationsConfigParameter = null ;
128
130
private ShadowCopy shadowCopy = ShadowCopy .DEFAULT ;
131
+ private DeletionStrategy deletionStrategy = DeletionStrategy .DEL ;
129
132
130
133
/**
131
134
* Lifecycle state of this factory.
@@ -134,6 +137,43 @@ enum State {
134
137
CREATED , STARTING , STARTED , STOPPING , STOPPED , DESTROYED ;
135
138
}
136
139
140
+ /**
141
+ * Strategy for deleting Redis keys in Repository operations.
142
+ * <p>
143
+ * Allows configuration of whether to use synchronous {@literal DEL} or asynchronous {@literal UNLINK} commands for
144
+ * key deletion operations.
145
+ *
146
+ * @author [Your Name]
147
+ * @since 3.6
148
+ * @see <a href="https://redis.io/commands/del">Redis DEL</a>
149
+ * @see <a href="https://redis.io/commands/unlink">Redis UNLINK</a>
150
+ */
151
+ public enum DeletionStrategy {
152
+
153
+ /**
154
+ * Use Redis {@literal DEL} command for key deletion.
155
+ * <p>
156
+ * 기key from memory. The command blocks until the key is completely removed, which can cause performance issues when
157
+ * deleting large data structures under high load.
158
+ * <p>
159
+ * This is the default strategy for backward compatibility.
160
+ */
161
+ DEL ,
162
+
163
+ /**
164
+ * Use Redis {@literal UNLINK} command for key deletion.
165
+ * <p>
166
+ * This is a non-blocking operation that asynchronously removes the key. The key is immediately removed from the
167
+ * keyspace, but the actual memory reclamation happens in the background, providing better performance for
168
+ * applications with frequent updates on existing keys.
169
+ * <p>
170
+ * Requires Redis 4.0 or later.
171
+ *
172
+ * @since Redis 4.0
173
+ */
174
+ UNLINK
175
+ }
176
+
137
177
/**
138
178
* Creates new {@link RedisKeyValueAdapter} with default {@link RedisMappingContext} and default
139
179
* {@link RedisCustomConversions}.
@@ -228,7 +268,7 @@ public Object put(Object id, Object item, String keyspace) {
228
268
byte [] key = toBytes (rdo .getId ());
229
269
byte [] objectKey = createKey (rdo .getKeyspace (), rdo .getId ());
230
270
231
- boolean isNew = connection . del ( objectKey ) == 0 ;
271
+ boolean isNew = applyDeletionStrategy ( connection , objectKey ) == 0 ;
232
272
233
273
connection .hMSet (objectKey , rdo .getBucket ().rawMap ());
234
274
@@ -245,11 +285,11 @@ public Object put(Object id, Object item, String keyspace) {
245
285
byte [] phantomKey = ByteUtils .concat (objectKey , BinaryKeyspaceIdentifier .PHANTOM_SUFFIX );
246
286
247
287
if (expires (rdo )) {
248
- connection . del ( phantomKey );
288
+ applyDeletionStrategy ( connection , phantomKey );
249
289
connection .hMSet (phantomKey , rdo .getBucket ().rawMap ());
250
290
connection .expire (phantomKey , rdo .getTimeToLive () + PHANTOM_KEY_TTL );
251
291
} else if (!isNew ) {
252
- connection . del ( phantomKey );
292
+ applyDeletionStrategy ( connection , phantomKey );
253
293
}
254
294
}
255
295
@@ -323,7 +363,7 @@ public <T> T delete(Object id, String keyspace, Class<T> type) {
323
363
324
364
redisOps .execute ((RedisCallback <Void >) connection -> {
325
365
326
- connection . del ( keyToDelete );
366
+ applyDeletionStrategy ( connection , keyToDelete );
327
367
connection .sRem (binKeyspace , binId );
328
368
new IndexWriter (connection , converter ).removeKeyFromIndexes (keyspace , binId );
329
369
@@ -335,7 +375,7 @@ public <T> T delete(Object id, String keyspace, Class<T> type) {
335
375
336
376
byte [] phantomKey = ByteUtils .concat (keyToDelete , BinaryKeyspaceIdentifier .PHANTOM_SUFFIX );
337
377
338
- connection . del ( phantomKey );
378
+ applyDeletionStrategy ( connection , phantomKey );
339
379
}
340
380
}
341
381
return null ;
@@ -485,7 +525,7 @@ public void update(PartialUpdate<?> update) {
485
525
connection .persist (redisKey );
486
526
487
527
if (keepShadowCopy ()) {
488
- connection . del ( ByteUtils .concat (redisKey , BinaryKeyspaceIdentifier .PHANTOM_SUFFIX ));
528
+ applyDeletionStrategy ( connection , ByteUtils .concat (redisKey , BinaryKeyspaceIdentifier .PHANTOM_SUFFIX ));
489
529
}
490
530
}
491
531
}
@@ -495,6 +535,18 @@ public void update(PartialUpdate<?> update) {
495
535
});
496
536
}
497
537
538
+ /**
539
+ * Apply the configured deletion strategy to delete the given key.
540
+ *
541
+ * @param connection the Redis connection
542
+ * @param key the key to delete
543
+ * @return the number of keys that were removed
544
+ */
545
+ private Long applyDeletionStrategy (RedisConnection connection , byte [] key ) {
546
+ return Objects
547
+ .requireNonNull (deletionStrategy == DeletionStrategy .UNLINK ? connection .unlink (key ) : connection .del (key ));
548
+ }
549
+
498
550
private RedisUpdateObject fetchDeletePathsFromHashAndUpdateIndex (RedisUpdateObject redisUpdateObject , String path ,
499
551
RedisConnection connection ) {
500
552
@@ -704,6 +756,30 @@ public boolean isRunning() {
704
756
return State .STARTED .equals (this .state .get ());
705
757
}
706
758
759
+ /**
760
+ * Configure the deletion strategy for Redis keys.
761
+ * <p>
762
+ * {@link DeletionStrategy#DEL DEL} performs synchronous key deletion, while {@link DeletionStrategy#UNLINK UNLINK}
763
+ * performs asynchronous deletion which can improve performance under high load scenarios.
764
+ *
765
+ * @param deletionStrategy the strategy to use for key deletion operations
766
+ * @since 3.6
767
+ */
768
+ public void setDeletionStrategy (DeletionStrategy deletionStrategy ) {
769
+ Assert .notNull (deletionStrategy , "DeletionStrategy must not be null" );
770
+ this .deletionStrategy = deletionStrategy ;
771
+ }
772
+
773
+ /**
774
+ * Get the current deletion strategy.
775
+ *
776
+ * @return the current deletion strategy
777
+ * @since 3.6
778
+ */
779
+ public DeletionStrategy getDeletionStrategy () {
780
+ return this .deletionStrategy ;
781
+ }
782
+
707
783
/**
708
784
* @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
709
785
* @since 1.8
@@ -792,7 +868,7 @@ private void initKeyExpirationListener(RedisMessageListenerContainer messageList
792
868
793
869
if (this .expirationListener .get () == null ) {
794
870
MappingExpirationListener listener = new MappingExpirationListener (messageListenerContainer , this .redisOps ,
795
- this .converter , this .shadowCopy );
871
+ this .converter , this .shadowCopy , this . deletionStrategy );
796
872
797
873
listener .setKeyspaceNotificationsConfigParameter (keyspaceNotificationsConfigParameter );
798
874
@@ -819,17 +895,19 @@ static class MappingExpirationListener extends KeyExpirationEventMessageListener
819
895
private final RedisOperations <?, ?> ops ;
820
896
private final RedisConverter converter ;
821
897
private final ShadowCopy shadowCopy ;
898
+ private final DeletionStrategy deletionStrategy ;
822
899
823
900
/**
824
901
* Creates new {@link MappingExpirationListener}.
825
902
*/
826
903
MappingExpirationListener (RedisMessageListenerContainer listenerContainer , RedisOperations <?, ?> ops ,
827
- RedisConverter converter , ShadowCopy shadowCopy ) {
904
+ RedisConverter converter , ShadowCopy shadowCopy , DeletionStrategy deletionStrategy ) {
828
905
829
906
super (listenerContainer );
830
907
this .ops = ops ;
831
908
this .converter = converter ;
832
909
this .shadowCopy = shadowCopy ;
910
+ this .deletionStrategy = deletionStrategy ;
833
911
}
834
912
835
913
@ Override
@@ -883,7 +961,11 @@ private Object readShadowCopy(byte[] key) {
883
961
Map <byte [], byte []> phantomValue = connection .hGetAll (phantomKey );
884
962
885
963
if (!CollectionUtils .isEmpty (phantomValue )) {
886
- connection .del (phantomKey );
964
+ if (deletionStrategy == DeletionStrategy .UNLINK ) {
965
+ connection .unlink (phantomKey );
966
+ } else {
967
+ connection .del (phantomKey );
968
+ }
887
969
}
888
970
889
971
return phantomValue ;
0 commit comments