2222import java .util .List ;
2323import java .util .Map ;
2424import java .util .Map .Entry ;
25+ import java .util .Objects ;
2526import java .util .Set ;
2627import java .util .concurrent .TimeUnit ;
2728import java .util .concurrent .atomic .AtomicReference ;
103104 * @author Mark Paluch
104105 * @author Andrey Muchnik
105106 * @author John Blum
107+ * @author Kim Sumin
106108 * @since 1.7
107109 */
108110public class RedisKeyValueAdapter extends AbstractKeyValueAdapter
@@ -126,6 +128,7 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter
126128 private EnableKeyspaceEvents enableKeyspaceEvents = EnableKeyspaceEvents .OFF ;
127129 private @ Nullable String keyspaceNotificationsConfigParameter = null ;
128130 private ShadowCopy shadowCopy = ShadowCopy .DEFAULT ;
131+ private DeletionStrategy deletionStrategy = DeletionStrategy .DEL ;
129132
130133 /**
131134 * Lifecycle state of this factory.
@@ -134,6 +137,43 @@ enum State {
134137 CREATED , STARTING , STARTED , STOPPING , STOPPED , DESTROYED ;
135138 }
136139
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+
137177 /**
138178 * Creates new {@link RedisKeyValueAdapter} with default {@link RedisMappingContext} and default
139179 * {@link RedisCustomConversions}.
@@ -228,7 +268,7 @@ public Object put(Object id, Object item, String keyspace) {
228268 byte [] key = toBytes (rdo .getId ());
229269 byte [] objectKey = createKey (rdo .getKeyspace (), rdo .getId ());
230270
231- boolean isNew = connection . del ( objectKey ) == 0 ;
271+ boolean isNew = applyDeletionStrategy ( connection , objectKey ) == 0 ;
232272
233273 connection .hMSet (objectKey , rdo .getBucket ().rawMap ());
234274
@@ -245,11 +285,11 @@ public Object put(Object id, Object item, String keyspace) {
245285 byte [] phantomKey = ByteUtils .concat (objectKey , BinaryKeyspaceIdentifier .PHANTOM_SUFFIX );
246286
247287 if (expires (rdo )) {
248- connection . del ( phantomKey );
288+ applyDeletionStrategy ( connection , phantomKey );
249289 connection .hMSet (phantomKey , rdo .getBucket ().rawMap ());
250290 connection .expire (phantomKey , rdo .getTimeToLive () + PHANTOM_KEY_TTL );
251291 } else if (!isNew ) {
252- connection . del ( phantomKey );
292+ applyDeletionStrategy ( connection , phantomKey );
253293 }
254294 }
255295
@@ -323,7 +363,7 @@ public <T> T delete(Object id, String keyspace, Class<T> type) {
323363
324364 redisOps .execute ((RedisCallback <Void >) connection -> {
325365
326- connection . del ( keyToDelete );
366+ applyDeletionStrategy ( connection , keyToDelete );
327367 connection .sRem (binKeyspace , binId );
328368 new IndexWriter (connection , converter ).removeKeyFromIndexes (keyspace , binId );
329369
@@ -335,7 +375,7 @@ public <T> T delete(Object id, String keyspace, Class<T> type) {
335375
336376 byte [] phantomKey = ByteUtils .concat (keyToDelete , BinaryKeyspaceIdentifier .PHANTOM_SUFFIX );
337377
338- connection . del ( phantomKey );
378+ applyDeletionStrategy ( connection , phantomKey );
339379 }
340380 }
341381 return null ;
@@ -485,7 +525,7 @@ public void update(PartialUpdate<?> update) {
485525 connection .persist (redisKey );
486526
487527 if (keepShadowCopy ()) {
488- connection . del ( ByteUtils .concat (redisKey , BinaryKeyspaceIdentifier .PHANTOM_SUFFIX ));
528+ applyDeletionStrategy ( connection , ByteUtils .concat (redisKey , BinaryKeyspaceIdentifier .PHANTOM_SUFFIX ));
489529 }
490530 }
491531 }
@@ -495,6 +535,18 @@ public void update(PartialUpdate<?> update) {
495535 });
496536 }
497537
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+
498550 private RedisUpdateObject fetchDeletePathsFromHashAndUpdateIndex (RedisUpdateObject redisUpdateObject , String path ,
499551 RedisConnection connection ) {
500552
@@ -704,6 +756,30 @@ public boolean isRunning() {
704756 return State .STARTED .equals (this .state .get ());
705757 }
706758
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+
707783 /**
708784 * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
709785 * @since 1.8
@@ -792,7 +868,7 @@ private void initKeyExpirationListener(RedisMessageListenerContainer messageList
792868
793869 if (this .expirationListener .get () == null ) {
794870 MappingExpirationListener listener = new MappingExpirationListener (messageListenerContainer , this .redisOps ,
795- this .converter , this .shadowCopy );
871+ this .converter , this .shadowCopy , this . deletionStrategy );
796872
797873 listener .setKeyspaceNotificationsConfigParameter (keyspaceNotificationsConfigParameter );
798874
@@ -819,17 +895,19 @@ static class MappingExpirationListener extends KeyExpirationEventMessageListener
819895 private final RedisOperations <?, ?> ops ;
820896 private final RedisConverter converter ;
821897 private final ShadowCopy shadowCopy ;
898+ private final DeletionStrategy deletionStrategy ;
822899
823900 /**
824901 * Creates new {@link MappingExpirationListener}.
825902 */
826903 MappingExpirationListener (RedisMessageListenerContainer listenerContainer , RedisOperations <?, ?> ops ,
827- RedisConverter converter , ShadowCopy shadowCopy ) {
904+ RedisConverter converter , ShadowCopy shadowCopy , DeletionStrategy deletionStrategy ) {
828905
829906 super (listenerContainer );
830907 this .ops = ops ;
831908 this .converter = converter ;
832909 this .shadowCopy = shadowCopy ;
910+ this .deletionStrategy = deletionStrategy ;
833911 }
834912
835913 @ Override
@@ -883,7 +961,11 @@ private Object readShadowCopy(byte[] key) {
883961 Map <byte [], byte []> phantomValue = connection .hGetAll (phantomKey );
884962
885963 if (!CollectionUtils .isEmpty (phantomValue )) {
886- connection .del (phantomKey );
964+ if (deletionStrategy == DeletionStrategy .UNLINK ) {
965+ connection .unlink (phantomKey );
966+ } else {
967+ connection .del (phantomKey );
968+ }
887969 }
888970
889971 return phantomValue ;
0 commit comments