Skip to content

Commit 546ebda

Browse files
christophstroblmp911de
authored andcommitted
DATAREDIS-1091 - Add configuration to disable storage of shadow copies for entities using TimeToLive.
Original pull request: spring-projects#517.
1 parent 2f0a560 commit 546ebda

File tree

5 files changed

+136
-13
lines changed

5 files changed

+136
-13
lines changed

src/main/asciidoc/reference/redis-repositories.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -591,6 +591,8 @@ NOTE: The keyspace notification message listener alters `notify-keyspace-events`
591591

592592
NOTE: Redis Pub/Sub messages are not persistent. If a key expires while the application is down, the expiry event is not processed, which may lead to secondary indexes containing references to the expired object.
593593

594+
NOTE: `@EnableKeyspaceEvents(shadowCopy = OFF)` disable storage of phantom copies and reduces data size within Redis. `RedisKeyExpiredEvent` will only contain the `id` of the expired key.
595+
594596
[[redis.repositories.references]]
595597
== Persisting References
596598
Marking properties with `@Reference` allows storing a simple key reference instead of copying values into the hash itself.

src/main/java/org/springframework/data/redis/core/RedisKeyValueAdapter.java

Lines changed: 65 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter
117117

118118
private EnableKeyspaceEvents enableKeyspaceEvents = EnableKeyspaceEvents.OFF;
119119
private @Nullable String keyspaceNotificationsConfigParameter = null;
120+
private ShadowCopy shadowCopy = ShadowCopy.DEFAULT;
120121

121122
/**
122123
* Creates new {@link RedisKeyValueAdapter} with default {@link RedisMappingContext} and default
@@ -238,11 +239,13 @@ public Object put(Object id, Object item, String keyspace) {
238239

239240
connection.expire(objectKey, rdo.getTimeToLive());
240241

241-
// add phantom key so values can be restored
242-
byte[] phantomKey = ByteUtils.concat(objectKey, BinaryKeyspaceIdentifier.PHANTOM_SUFFIX);
243-
connection.del(phantomKey);
244-
connection.hMSet(phantomKey, rdo.getBucket().rawMap());
245-
connection.expire(phantomKey, rdo.getTimeToLive() + PHANTOM_KEY_TTL);
242+
if (keepShadowCopy()) { // add phantom key so values can be restored
243+
244+
byte[] phantomKey = ByteUtils.concat(objectKey, BinaryKeyspaceIdentifier.PHANTOM_SUFFIX);
245+
connection.del(phantomKey);
246+
connection.hMSet(phantomKey, rdo.getBucket().rawMap());
247+
connection.expire(phantomKey, rdo.getTimeToLive() + PHANTOM_KEY_TTL);
248+
}
246249
}
247250

248251
connection.sAdd(toBytes(rdo.getKeyspace()), key);
@@ -475,10 +478,12 @@ public void update(PartialUpdate<?> update) {
475478

476479
connection.expire(redisKey, rdo.getTimeToLive());
477480

478-
// add phantom key so values can be restored
479-
byte[] phantomKey = ByteUtils.concat(redisKey, BinaryKeyspaceIdentifier.PHANTOM_SUFFIX);
480-
connection.hMSet(phantomKey, rdo.getBucket().rawMap());
481-
connection.expire(phantomKey, rdo.getTimeToLive() + PHANTOM_KEY_TTL);
481+
if (keepShadowCopy()) { // add phantom key so values can be restored
482+
483+
byte[] phantomKey = ByteUtils.concat(redisKey, BinaryKeyspaceIdentifier.PHANTOM_SUFFIX);
484+
connection.hMSet(phantomKey, rdo.getBucket().rawMap());
485+
connection.expire(phantomKey, rdo.getTimeToLive() + PHANTOM_KEY_TTL);
486+
}
482487

483488
} else {
484489

@@ -661,6 +666,16 @@ public void setKeyspaceNotificationsConfigParameter(String keyspaceNotifications
661666
this.keyspaceNotificationsConfigParameter = keyspaceNotificationsConfigParameter;
662667
}
663668

669+
/**
670+
* Configure storage of phantom keys (shadow copies) of expiring entities.
671+
*
672+
* @param shadowCopy must not be {@literal null}.
673+
* @since 2.3
674+
*/
675+
public void setShadowCopy(ShadowCopy shadowCopy) {
676+
this.shadowCopy = shadowCopy;
677+
}
678+
664679
/**
665680
* @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
666681
* @since 1.8
@@ -773,7 +788,8 @@ public void onMessage(Message message, @Nullable byte[] pattern) {
773788

774789
byte[] key = message.getBody();
775790

776-
byte[] phantomKey = ByteUtils.concat(key, converter.getConversionService().convert(KeyspaceIdentifier.PHANTOM_SUFFIX, byte[].class));
791+
byte[] phantomKey = ByteUtils.concat(key,
792+
converter.getConversionService().convert(KeyspaceIdentifier.PHANTOM_SUFFIX, byte[].class));
777793

778794
Map<byte[], byte[]> hash = ops.execute((RedisCallback<Map<byte[], byte[]>>) connection -> {
779795

@@ -789,7 +805,8 @@ public void onMessage(Message message, @Nullable byte[] pattern) {
789805
Object value = converter.read(Object.class, new RedisData(hash));
790806

791807
String channel = !ObjectUtils.isEmpty(message.getChannel())
792-
? converter.getConversionService().convert(message.getChannel(), String.class) : null;
808+
? converter.getConversionService().convert(message.getChannel(), String.class)
809+
: null;
793810

794811
RedisKeyExpiredEvent event = new RedisKeyExpiredEvent(channel, key, value);
795812

@@ -813,6 +830,18 @@ private boolean isKeyExpirationMessage(Message message) {
813830
}
814831
}
815832

833+
private boolean keepShadowCopy() {
834+
835+
switch (shadowCopy) {
836+
case OFF:
837+
return false;
838+
case ON:
839+
return true;
840+
default:
841+
return this.expirationListener.get() != null;
842+
}
843+
}
844+
816845
/**
817846
* @author Christoph Strobl
818847
* @since 1.8
@@ -835,6 +864,31 @@ public enum EnableKeyspaceEvents {
835864
OFF
836865
}
837866

867+
/**
868+
* Configuration flag controlling storage of phantom keys (shadow copies) of expiring entities to read them later when
869+
* publishing {@link RedisKeyspaceEvent}.
870+
*
871+
* @author Christoph Strobl
872+
* @since 2.3
873+
*/
874+
public enum ShadowCopy {
875+
876+
/**
877+
* Store shadow copies of expiring entities depending on the {@link EnableKeyspaceEvents}.
878+
*/
879+
DEFAULT,
880+
881+
/**
882+
* Store shadow copies of expiring entities.
883+
*/
884+
ON,
885+
886+
/**
887+
* Do not store shadow copies.
888+
*/
889+
OFF
890+
}
891+
838892
/**
839893
* Container holding update information like fields to remove from the Redis Hash.
840894
*

src/main/java/org/springframework/data/redis/repository/configuration/EnableRedisRepositories.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.springframework.data.keyvalue.core.KeyValueOperations;
2929
import org.springframework.data.keyvalue.repository.config.QueryCreatorType;
3030
import org.springframework.data.redis.core.RedisKeyValueAdapter.EnableKeyspaceEvents;
31+
import org.springframework.data.redis.core.RedisKeyValueAdapter.ShadowCopy;
3132
import org.springframework.data.redis.core.RedisOperations;
3233
import org.springframework.data.redis.core.convert.KeyspaceConfiguration;
3334
import org.springframework.data.redis.core.index.IndexConfiguration;
@@ -166,6 +167,15 @@
166167
*/
167168
EnableKeyspaceEvents enableKeyspaceEvents() default EnableKeyspaceEvents.OFF;
168169

170+
/**
171+
* Configuration flag controlling storage of phantom keys (shadow copies) of expiring entities to read them later when
172+
* publishing {@link org.springframework.data.redis.core.RedisKeyspaceEvent keyspace events}.
173+
*
174+
* @return
175+
* @since 2.3
176+
*/
177+
ShadowCopy shadowCopy() default ShadowCopy.DEFAULT;
178+
169179
/**
170180
* Configure the {@literal notify-keyspace-events} property if not already set. <br />
171181
* Use an empty {@link String} to keep (<b>not</b> alter) existing server configuration.

src/main/java/org/springframework/data/redis/repository/configuration/RedisRepositoryConfigurationExtension.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.springframework.data.redis.core.RedisHash;
2929
import org.springframework.data.redis.core.RedisKeyValueAdapter;
3030
import org.springframework.data.redis.core.RedisKeyValueAdapter.EnableKeyspaceEvents;
31+
import org.springframework.data.redis.core.RedisKeyValueAdapter.ShadowCopy;
3132
import org.springframework.data.redis.core.RedisKeyValueTemplate;
3233
import org.springframework.data.redis.core.convert.MappingConfiguration;
3334
import org.springframework.data.redis.core.convert.MappingRedisConverter;
@@ -145,6 +146,8 @@ private static AbstractBeanDefinition createRedisKeyValueAdapter(RepositoryConfi
145146
configuration.getRequiredAttribute("enableKeyspaceEvents", EnableKeyspaceEvents.class)) //
146147
.addPropertyValue("keyspaceNotificationsConfigParameter",
147148
configuration.getAttribute("keyspaceNotificationsConfigParameter", String.class).orElse("")) //
149+
.addPropertyValue("shadowCopy",
150+
configuration.getRequiredAttribute("shadowCopy", ShadowCopy.class)) //
148151
.getBeanDefinition();
149152
}
150153

src/test/java/org/springframework/data/redis/core/RedisKeyValueAdapterTests.java

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@
3737
import org.junit.runner.RunWith;
3838
import org.junit.runners.Parameterized;
3939
import org.junit.runners.Parameterized.Parameters;
40-
4140
import org.springframework.beans.factory.InitializingBean;
4241
import org.springframework.data.annotation.Id;
4342
import org.springframework.data.annotation.Reference;
@@ -50,6 +49,7 @@
5049
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
5150
import org.springframework.data.redis.connection.lettuce.LettuceTestClientResources;
5251
import org.springframework.data.redis.core.RedisKeyValueAdapter.EnableKeyspaceEvents;
52+
import org.springframework.data.redis.core.RedisKeyValueAdapter.ShadowCopy;
5353
import org.springframework.data.redis.core.convert.KeyspaceConfiguration;
5454
import org.springframework.data.redis.core.convert.MappingConfiguration;
5555
import org.springframework.data.redis.core.index.GeoIndexed;
@@ -341,7 +341,6 @@ public void keyExpiredEventShouldRemoveHelperStructures() throws Exception {
341341
assertThat(template.hasKey("persons:firstname:rand")).isFalse();
342342
assertThat(template.hasKey("persons:1:idx")).isFalse();
343343
assertThat(template.opsForSet().members("persons")).doesNotContain("1");
344-
;
345344
}
346345

347346
@Test // DATAREDIS-744
@@ -687,6 +686,56 @@ public void updateShouldAlterGeoIndexCorrectlyOnUpdate() {
687686
assertThat(updatedLocation.getY()).isCloseTo(18D, offset(0.005));
688687
}
689688

689+
@Test // DATAREDIS-1091
690+
public void phantomKeyNotInsertedOnPutWhenShadowCopyIsTurnedOff() {
691+
692+
RedisMappingContext mappingContext = new RedisMappingContext(
693+
new MappingConfiguration(new IndexConfiguration(), new KeyspaceConfiguration()));
694+
mappingContext.afterPropertiesSet();
695+
696+
RedisKeyValueAdapter kvAdapter = new RedisKeyValueAdapter(template, mappingContext);
697+
kvAdapter.setShadowCopy(ShadowCopy.OFF);
698+
699+
ExpiringPerson rand = new ExpiringPerson();
700+
rand.age = 24;
701+
rand.ttl = 3000L;
702+
703+
kvAdapter.put("1", rand, "persons");
704+
705+
assertThat(template.hasKey("persons:1:phantom")).isFalse();
706+
}
707+
708+
@Test // DATAREDIS-1091
709+
public void phantomKeyInsertedOnPutWhenShadowCopyIsTurnedOn() {
710+
711+
RedisMappingContext mappingContext = new RedisMappingContext(
712+
new MappingConfiguration(new IndexConfiguration(), new KeyspaceConfiguration()));
713+
mappingContext.afterPropertiesSet();
714+
715+
RedisKeyValueAdapter kvAdapter = new RedisKeyValueAdapter(template, mappingContext);
716+
kvAdapter.setShadowCopy(ShadowCopy.ON);
717+
718+
ExpiringPerson rand = new ExpiringPerson();
719+
rand.age = 24;
720+
rand.ttl = 3000L;
721+
722+
kvAdapter.put("1", rand, "persons");
723+
724+
assertThat(template.hasKey("persons:1:phantom")).isTrue();
725+
}
726+
727+
@Test // DATAREDIS-1091
728+
public void phantomKeyInsertedOnPutWhenShadowCopyIsInDefaultAndKeyspaceNotificationEnabled() {
729+
730+
ExpiringPerson rand = new ExpiringPerson();
731+
rand.age = 24;
732+
rand.ttl = 3000L;
733+
734+
adapter.put("1", rand, "persons");
735+
736+
assertThat(template.hasKey("persons:1:phantom")).isTrue();
737+
}
738+
690739
/**
691740
* Wait up to 5 seconds until {@code key} is no longer available in Redis.
692741
*
@@ -776,6 +825,11 @@ static class TaVeren extends Person {
776825

777826
}
778827

828+
static class ExpiringPerson extends Person {
829+
830+
@TimeToLive Long ttl;
831+
}
832+
779833
@KeySpace("locations")
780834
static class Location {
781835

0 commit comments

Comments
 (0)