/* * Copyright 2015-2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.redis.core; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Optional; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import org.springframework.beans.BeansException; import org.springframework.beans.factory.InitializingBean; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationListener; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.ConverterNotFoundException; import org.springframework.dao.DataAccessException; import org.springframework.data.keyvalue.core.AbstractKeyValueAdapter; import org.springframework.data.keyvalue.core.KeyValueAdapter; import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentProperty; import org.springframework.data.redis.connection.DataType; import org.springframework.data.redis.connection.Message; import org.springframework.data.redis.connection.MessageListener; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.core.PartialUpdate.PropertyUpdate; import org.springframework.data.redis.core.PartialUpdate.UpdateCommand; import org.springframework.data.redis.core.convert.CustomConversions; import org.springframework.data.redis.core.convert.GeoIndexedPropertyValue; import org.springframework.data.redis.core.convert.KeyspaceConfiguration; import org.springframework.data.redis.core.convert.MappingRedisConverter; import org.springframework.data.redis.core.convert.PathIndexResolver; import org.springframework.data.redis.core.convert.RedisConverter; import org.springframework.data.redis.core.convert.RedisCustomConversions; import org.springframework.data.redis.core.convert.RedisData; import org.springframework.data.redis.core.convert.ReferenceResolverImpl; import org.springframework.data.redis.core.mapping.RedisMappingContext; import org.springframework.data.redis.core.mapping.RedisPersistentEntity; import org.springframework.data.redis.core.mapping.RedisPersistentProperty; import org.springframework.data.redis.listener.KeyExpirationEventMessageListener; import org.springframework.data.redis.listener.RedisMessageListenerContainer; import org.springframework.data.redis.util.ByteUtils; import org.springframework.data.util.CloseableIterator; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; /** * Redis specific {@link KeyValueAdapter} implementation. Uses binary codec to read/write data from/to Redis. Objects * are stored in a Redis Hash using the value of {@link RedisHash}, the {@link KeyspaceConfiguration} or just * {@link Class#getName()} as a prefix. <br /> * <strong>Example</strong> * * <pre> * <code> * @RedisHash("persons") * class Person { * @Id String id; * String name; * } * * * prefix ID * | | * V V * hgetall persons:5d67b7e1-8640-4475-beeb-c666fab4c0e5 * 1) id * 2) 5d67b7e1-8640-4475-beeb-c666fab4c0e5 * 3) name * 4) Rand al'Thor * </code> * </pre> * * <br /> * The {@link KeyValueAdapter} is <strong>not</strong> intended to store simple types such as {@link String} values. * Please use {@link RedisTemplate} for this purpose. * * @author Christoph Strobl * @author Mark Paluch * @since 1.7 */ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter implements InitializingBean, ApplicationContextAware, ApplicationListener<RedisKeyspaceEvent> { private RedisOperations<?, ?> redisOps; private RedisConverter converter; private RedisMessageListenerContainer messageListenerContainer; private final AtomicReference<KeyExpirationEventMessageListener> expirationListener = new AtomicReference<KeyExpirationEventMessageListener>( null); private ApplicationEventPublisher eventPublisher; private EnableKeyspaceEvents enableKeyspaceEvents = EnableKeyspaceEvents.OFF; private String keyspaceNotificationsConfigParameter = null; /** * Creates new {@link RedisKeyValueAdapter} with default {@link RedisMappingContext} and default * {@link RedisCustomConversions}. * * @param redisOps must not be {@literal null}. */ public RedisKeyValueAdapter(RedisOperations<?, ?> redisOps) { this(redisOps, new RedisMappingContext()); } /** * Creates new {@link RedisKeyValueAdapter} with default {@link RedisCustomConversions}. * * @param redisOps must not be {@literal null}. * @param mappingContext must not be {@literal null}. */ public RedisKeyValueAdapter(RedisOperations<?, ?> redisOps, RedisMappingContext mappingContext) { this(redisOps, mappingContext, new RedisCustomConversions()); } /** * Creates new {@link RedisKeyValueAdapter}. * * @param redisOps must not be {@literal null}. * @param mappingContext must not be {@literal null}. * @param customConversions can be {@literal null}. * @deprecated since 2.0, use * {@link #RedisKeyValueAdapter(RedisOperations, RedisMappingContext, org.springframework.data.convert.CustomConversions)}. */ @Deprecated public RedisKeyValueAdapter(RedisOperations<?, ?> redisOps, RedisMappingContext mappingContext, CustomConversions customConversions) { this(redisOps, mappingContext, (org.springframework.data.convert.CustomConversions) customConversions); } /** * Creates new {@link RedisKeyValueAdapter}. * * @param redisOps must not be {@literal null}. * @param mappingContext must not be {@literal null}. * @param customConversions can be {@literal null}. * @since 2.0 */ public RedisKeyValueAdapter(RedisOperations<?, ?> redisOps, RedisMappingContext mappingContext, org.springframework.data.convert.CustomConversions customConversions) { super(new RedisQueryEngine()); Assert.notNull(redisOps, "RedisOperations must not be null!"); Assert.notNull(mappingContext, "RedisMappingContext must not be null!"); MappingRedisConverter mappingConverter = new MappingRedisConverter(mappingContext, new PathIndexResolver(mappingContext), new ReferenceResolverImpl(redisOps)); mappingConverter.setCustomConversions(customConversions == null ? new RedisCustomConversions() : customConversions); mappingConverter.afterPropertiesSet(); this.converter = mappingConverter; this.redisOps = redisOps; initMessageListenerContainer(); } /** * Creates new {@link RedisKeyValueAdapter} with specific {@link RedisConverter}. * * @param redisOps must not be {@literal null}. * @param redisConverter must not be {@literal null}. */ public RedisKeyValueAdapter(RedisOperations<?, ?> redisOps, RedisConverter redisConverter) { super(new RedisQueryEngine()); Assert.notNull(redisOps, "RedisOperations must not be null!"); this.converter = redisConverter; this.redisOps = redisOps; initMessageListenerContainer(); } /** * Default constructor. */ protected RedisKeyValueAdapter() {} /* * (non-Javadoc) * @see org.springframework.data.keyvalue.core.KeyValueAdapter#put(java.io.Serializable, java.lang.Object, java.io.Serializable) */ public Object put(final Serializable id, final Object item, final Serializable keyspace) { final RedisData rdo = item instanceof RedisData ? (RedisData) item : new RedisData(); if (!(item instanceof RedisData)) { converter.write(item, rdo); } if (ObjectUtils.nullSafeEquals(EnableKeyspaceEvents.ON_DEMAND, enableKeyspaceEvents) && this.expirationListener.get() == null) { if (rdo.getTimeToLive() != null && rdo.getTimeToLive().longValue() > 0) { initKeyExpirationListener(); } } if (rdo.getId() == null) { rdo.setId(converter.getConversionService().convert(id, String.class)); if (!(item instanceof RedisData)) { KeyValuePersistentProperty idProperty = converter.getMappingContext().getPersistentEntity(item.getClass()).get() .getIdProperty().get(); converter.getMappingContext().getPersistentEntity(item.getClass()).get().getPropertyAccessor(item) .setProperty(idProperty, Optional.ofNullable(id)); } } redisOps.execute(new RedisCallback<Object>() { @Override public Object doInRedis(RedisConnection connection) throws DataAccessException { byte[] key = toBytes(rdo.getId()); byte[] objectKey = createKey(rdo.getKeyspace(), rdo.getId()); boolean isNew = connection.del(objectKey) == 0; connection.hMSet(objectKey, rdo.getBucket().rawMap()); if (rdo.getTimeToLive() != null && rdo.getTimeToLive().longValue() > 0) { connection.expire(objectKey, rdo.getTimeToLive().longValue()); // add phantom key so values can be restored byte[] phantomKey = ByteUtils.concat(objectKey, toBytes(":phantom")); connection.del(phantomKey); connection.hMSet(phantomKey, rdo.getBucket().rawMap()); connection.expire(phantomKey, rdo.getTimeToLive().longValue() + 300); } connection.sAdd(toBytes(rdo.getKeyspace()), key); IndexWriter indexWriter = new IndexWriter(connection, converter); if (isNew) { indexWriter.createIndexes(key, rdo.getIndexedData()); } else { indexWriter.deleteAndUpdateIndexes(key, rdo.getIndexedData()); } return null; } }); return item; } /* * (non-Javadoc) * @see org.springframework.data.keyvalue.core.KeyValueAdapter#contains(java.io.Serializable, java.io.Serializable) */ public boolean contains(final Serializable id, final Serializable keyspace) { Boolean exists = redisOps.execute(new RedisCallback<Boolean>() { @Override public Boolean doInRedis(RedisConnection connection) throws DataAccessException { return connection.sIsMember(toBytes(keyspace), toBytes(id)); } }); return exists != null ? exists.booleanValue() : false; } /* * (non-Javadoc) * @see org.springframework.data.keyvalue.core.KeyValueAdapter#get(java.io.Serializable, java.io.Serializable) */ public Object get(Serializable id, Serializable keyspace) { return get(id, keyspace, Object.class); } /** * @param id * @param keyspace * @param type * @return */ public <T> T get(Serializable id, Serializable keyspace, Class<T> type) { String stringId = asString(id); String stringKeyspace = asString(keyspace); final byte[] binId = createKey(stringKeyspace, stringId); Map<byte[], byte[]> raw = redisOps.execute(new RedisCallback<Map<byte[], byte[]>>() { @Override public Map<byte[], byte[]> doInRedis(RedisConnection connection) throws DataAccessException { return connection.hGetAll(binId); } }); RedisData data = new RedisData(raw); data.setId(stringId); data.setKeyspace(stringKeyspace); return readBackTimeToLiveIfSet(binId, converter.read(type, data)); } /* * (non-Javadoc) * @see org.springframework.data.keyvalue.core.KeyValueAdapter#delete(java.io.Serializable, java.io.Serializable) */ public Object delete(final Serializable id, final Serializable keyspace) { return delete(id, keyspace, Object.class); } /* * (non-Javadoc) * @see org.springframework.data.keyvalue.core.AbstractKeyValueAdapter#delete(java.io.Serializable, java.io.Serializable, java.lang.Class) */ public <T> T delete(final Serializable id, final Serializable keyspace, final Class<T> type) { final byte[] binId = toBytes(id); final byte[] binKeyspace = toBytes(keyspace); T o = get(id, keyspace, type); if (o != null) { final byte[] keyToDelete = createKey(asString(keyspace), asString(id)); redisOps.execute(new RedisCallback<Void>() { @Override public Void doInRedis(RedisConnection connection) throws DataAccessException { connection.del(keyToDelete); connection.sRem(binKeyspace, binId); new IndexWriter(connection, converter).removeKeyFromIndexes(asString(keyspace), binId); return null; } }); } return o; } /* * (non-Javadoc) * @see org.springframework.data.keyvalue.core.KeyValueAdapter#getAllOf(java.io.Serializable) */ public List<?> getAllOf(final Serializable keyspace) { return getAllOf(keyspace, -1, -1); } public List<?> getAllOf(final Serializable keyspace, long offset, int rows) { final byte[] binKeyspace = toBytes(keyspace); Set<byte[]> ids = redisOps.execute(new RedisCallback<Set<byte[]>>() { @Override public Set<byte[]> doInRedis(RedisConnection connection) throws DataAccessException { return connection.sMembers(binKeyspace); } }); List<Object> result = new ArrayList<Object>(); List<byte[]> keys = new ArrayList<byte[]>(ids); if (keys.isEmpty() || keys.size() < offset) { return Collections.emptyList(); } offset = Math.max(0, offset); if (offset >= 0 && rows > 0) { keys = keys.subList((int) offset, Math.min((int) offset + rows, keys.size())); } for (byte[] key : keys) { result.add(get(key, keyspace)); } return result; } /* * (non-Javadoc) * @see org.springframework.data.keyvalue.core.KeyValueAdapter#deleteAllOf(java.io.Serializable) */ public void deleteAllOf(final Serializable keyspace) { redisOps.execute(new RedisCallback<Void>() { @Override public Void doInRedis(RedisConnection connection) throws DataAccessException { connection.del(toBytes(keyspace)); new IndexWriter(connection, converter).removeAllIndexes(asString(keyspace)); return null; } }); } /* * (non-Javadoc) * @see org.springframework.data.keyvalue.core.KeyValueAdapter#entries(java.io.Serializable) */ public CloseableIterator<Entry<Serializable, Object>> entries(Serializable keyspace) { throw new UnsupportedOperationException("Not yet implemented"); } /* * (non-Javadoc) * @see org.springframework.data.keyvalue.core.KeyValueAdapter#count(java.io.Serializable) */ public long count(final Serializable keyspace) { Long count = redisOps.execute(new RedisCallback<Long>() { @Override public Long doInRedis(RedisConnection connection) throws DataAccessException { return connection.sCard(toBytes(keyspace)); } }); return count != null ? count.longValue() : 0; } public void update(final PartialUpdate<?> update) { final RedisPersistentEntity<?> entity = this.converter.getMappingContext().getPersistentEntity(update.getTarget()) .get(); final String keyspace = entity.getKeySpace(); final Object id = update.getId(); final byte[] redisKey = createKey(keyspace, converter.getConversionService().convert(id, String.class)); final RedisData rdo = new RedisData(); this.converter.write(update, rdo); redisOps.execute(new RedisCallback<Void>() { @Override public Void doInRedis(RedisConnection connection) throws DataAccessException { RedisUpdateObject redisUpdateObject = new RedisUpdateObject(redisKey, keyspace, id); for (PropertyUpdate pUpdate : update.getPropertyUpdates()) { String propertyPath = pUpdate.getPropertyPath(); if (UpdateCommand.DEL.equals(pUpdate.getCmd()) || pUpdate.getValue() instanceof Collection || pUpdate.getValue() instanceof Map || (pUpdate.getValue() != null && pUpdate.getValue().getClass().isArray()) || (pUpdate.getValue() != null && !converter.getConversionService().canConvert(pUpdate.getValue().getClass(), byte[].class))) { redisUpdateObject = fetchDeletePathsFromHashAndUpdateIndex(redisUpdateObject, propertyPath, connection); } } if (!redisUpdateObject.fieldsToRemove.isEmpty()) { connection.hDel(redisKey, redisUpdateObject.fieldsToRemove.toArray(new byte[redisUpdateObject.fieldsToRemove.size()][])); } for (RedisUpdateObject.Index index : redisUpdateObject.indexesToUpdate) { if (ObjectUtils.nullSafeEquals(DataType.ZSET, index.type)) { connection.zRem(index.key, toBytes(redisUpdateObject.targetId)); } else { connection.sRem(index.key, toBytes(redisUpdateObject.targetId)); } } if (!rdo.getBucket().isEmpty()) { if (rdo.getBucket().size() > 1 || (rdo.getBucket().size() == 1 && !rdo.getBucket().asMap().containsKey("_class"))) { connection.hMSet(redisKey, rdo.getBucket().rawMap()); } } if (update.isRefreshTtl()) { if (rdo.getTimeToLive() != null && rdo.getTimeToLive().longValue() > 0) { connection.expire(redisKey, rdo.getTimeToLive().longValue()); // add phantom key so values can be restored byte[] phantomKey = ByteUtils.concat(redisKey, toBytes(":phantom")); connection.hMSet(phantomKey, rdo.getBucket().rawMap()); connection.expire(phantomKey, rdo.getTimeToLive().longValue() + 300); } else { connection.persist(redisKey); connection.persist(ByteUtils.concat(redisKey, toBytes(":phantom"))); } } new IndexWriter(connection, converter).updateIndexes(toBytes(id), rdo.getIndexedData()); return null; } }); } private RedisUpdateObject fetchDeletePathsFromHashAndUpdateIndex(RedisUpdateObject redisUpdateObject, String path, RedisConnection connection) { redisUpdateObject.addFieldToRemove(toBytes(path)); byte[] value = connection.hGet(redisUpdateObject.targetKey, toBytes(path)); if (value != null && value.length > 0) { byte[] existingValueIndexKey = value != null ? ByteUtils.concatAll(toBytes(redisUpdateObject.keyspace), toBytes((":" + path)), toBytes(":"), value) : null; if (connection.exists(existingValueIndexKey)) { redisUpdateObject.addIndexToUpdate(new RedisUpdateObject.Index(existingValueIndexKey, DataType.SET)); } return redisUpdateObject; } Set<byte[]> existingFields = connection.hKeys(redisUpdateObject.targetKey); for (byte[] field : existingFields) { if (asString(field).startsWith(path + ".")) { redisUpdateObject.addFieldToRemove(field); value = connection.hGet(redisUpdateObject.targetKey, toBytes(field)); if (value != null) { byte[] existingValueIndexKey = value != null ? ByteUtils.concatAll(toBytes(redisUpdateObject.keyspace), toBytes(":"), field, toBytes(":"), value) : null; if (connection.exists(existingValueIndexKey)) { redisUpdateObject.addIndexToUpdate(new RedisUpdateObject.Index(existingValueIndexKey, DataType.SET)); } } } } String pathToUse = GeoIndexedPropertyValue.geoIndexName(path); byte[] existingGeoIndexKey = ByteUtils.concatAll(toBytes(redisUpdateObject.keyspace), toBytes(":"), toBytes(pathToUse)); if (connection.zRank(existingGeoIndexKey, toBytes(redisUpdateObject.targetId)) != null) { redisUpdateObject.addIndexToUpdate(new RedisUpdateObject.Index(existingGeoIndexKey, DataType.ZSET)); } return redisUpdateObject; } /** * Execute {@link RedisCallback} via underlying {@link RedisOperations}. * * @param callback must not be {@literal null}. * @see RedisOperations#execute(RedisCallback) * @return */ public <T> T execute(RedisCallback<T> callback) { return redisOps.execute(callback); } /** * Get the {@link RedisConverter} in use. * * @return never {@literal null}. */ public RedisConverter getConverter() { return this.converter; } public void clear() { // nothing to do } private String asString(Serializable value) { return value instanceof String ? (String) value : getConverter().getConversionService().convert(value, String.class); } public byte[] createKey(String keyspace, String id) { return toBytes(keyspace + ":" + id); } /** * Convert given source to binary representation using the underlying {@link ConversionService}. * * @param source * @return * @throws ConverterNotFoundException */ public byte[] toBytes(Object source) { if (source instanceof byte[]) { return (byte[]) source; } return converter.getConversionService().convert(source, byte[].class); } /** * Read back and set {@link TimeToLive} for the property. * * @param key * @param target * @return */ @SuppressWarnings({ "unchecked", "rawtypes" }) private <T> T readBackTimeToLiveIfSet(final byte[] key, T target) { if (target == null || key == null) { return target; } RedisPersistentEntity<?> entity = this.converter.getMappingContext().getPersistentEntity(target.getClass()).get(); if (entity.hasExplictTimeToLiveProperty()) { Optional<RedisPersistentProperty> ttlProperty = entity.getExplicitTimeToLiveProperty(); if (!ttlProperty.isPresent()) { return target; } final Optional<TimeToLive> ttl = ttlProperty.get().findAnnotation(TimeToLive.class); Long timeout = redisOps.execute(new RedisCallback<Long>() { @Override public Long doInRedis(RedisConnection connection) throws DataAccessException { if (ObjectUtils.nullSafeEquals(TimeUnit.SECONDS, ttl.get().unit())) { return connection.ttl(key); } return connection.pTtl(key, ttl.get().unit()); } }); if (timeout != null || !ttlProperty.get().getType().isPrimitive()) { entity.getPropertyAccessor(target).setProperty(ttlProperty.get(), Optional.ofNullable(converter.getConversionService().convert(timeout, ttlProperty.get().getType()))); } } return target; } /** * Configure usage of {@link KeyExpirationEventMessageListener}. * * @param enableKeyspaceEvents * @since 1.8 */ public void setEnableKeyspaceEvents(EnableKeyspaceEvents enableKeyspaceEvents) { this.enableKeyspaceEvents = enableKeyspaceEvents; } /** * Configure the {@literal notify-keyspace-events} property if not already set. Use an empty {@link String} or * {@literal null} to retain existing server settings. * * @param keyspaceNotificationsConfigParameter can be {@literal null}. * @since 1.8 */ public void setKeyspaceNotificationsConfigParameter(String keyspaceNotificationsConfigParameter) { this.keyspaceNotificationsConfigParameter = keyspaceNotificationsConfigParameter; } /** * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet() * @since 1.8 */ @Override public void afterPropertiesSet() { if (ObjectUtils.nullSafeEquals(EnableKeyspaceEvents.ON_STARTUP, this.enableKeyspaceEvents)) { initKeyExpirationListener(); } } /* * (non-Javadoc) * @see org.springframework.beans.factory.DisposableBean#destroy() */ public void destroy() throws Exception { if (this.expirationListener.get() != null) { this.expirationListener.get().destroy(); } if (this.messageListenerContainer != null) { this.messageListenerContainer.destroy(); } } /* * (non-Javadoc) * @see org.springframework.context.ApplicationListener#onApplicationEvent(org.springframework.context.ApplicationEvent) */ @Override public void onApplicationEvent(RedisKeyspaceEvent event) { // just a customization hook } /* * (non-Javadoc) * @see org.springframework.context.ApplicationContextAware#setApplicationContext(org.springframework.context.ApplicationContext) */ @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.eventPublisher = applicationContext; } private void initMessageListenerContainer() { this.messageListenerContainer = new RedisMessageListenerContainer(); this.messageListenerContainer.setConnectionFactory(((RedisTemplate<?, ?>) redisOps).getConnectionFactory()); this.messageListenerContainer.afterPropertiesSet(); this.messageListenerContainer.start(); } private void initKeyExpirationListener() { if (this.expirationListener.get() == null) { MappingExpirationListener listener = new MappingExpirationListener(this.messageListenerContainer, this.redisOps, this.converter); listener.setKeyspaceNotificationsConfigParameter(keyspaceNotificationsConfigParameter); if (this.eventPublisher != null) { listener.setApplicationEventPublisher(this.eventPublisher); } if (this.expirationListener.compareAndSet(null, listener)) { listener.init(); } } } /** * {@link MessageListener} implementation used to capture Redis keypspace notifications. Tries to read a previously * created phantom key {@code keyspace:id:phantom} to provide the expired object as part of the published * {@link RedisKeyExpiredEvent}. * * @author Christoph Strobl * @since 1.7 */ static class MappingExpirationListener extends KeyExpirationEventMessageListener { private final RedisOperations<?, ?> ops; private final RedisConverter converter; /** * Creates new {@link MappingExpirationListener}. * * @param listenerContainer * @param ops * @param converter */ public MappingExpirationListener(RedisMessageListenerContainer listenerContainer, RedisOperations<?, ?> ops, RedisConverter converter) { super(listenerContainer); this.ops = ops; this.converter = converter; } /* * (non-Javadoc) * @see org.springframework.data.redis.listener.KeyspaceEventMessageListener#onMessage(org.springframework.data.redis.connection.Message, byte[]) */ @Override public void onMessage(Message message, byte[] pattern) { if (!isKeyExpirationMessage(message)) { return; } byte[] key = message.getBody(); final byte[] phantomKey = ByteUtils.concat(key, converter.getConversionService().convert(":phantom", byte[].class)); Map<byte[], byte[]> hash = ops.execute(new RedisCallback<Map<byte[], byte[]>>() { @Override public Map<byte[], byte[]> doInRedis(RedisConnection connection) throws DataAccessException { Map<byte[], byte[]> hash = connection.hGetAll(phantomKey); if (!org.springframework.util.CollectionUtils.isEmpty(hash)) { connection.del(phantomKey); } return hash; } }); Object value = converter.read(Object.class, new RedisData(hash)); String channel = !ObjectUtils.isEmpty(message.getChannel()) ? converter.getConversionService().convert(message.getChannel(), String.class) : null; final RedisKeyExpiredEvent event = new RedisKeyExpiredEvent(channel, key, value); ops.execute(new RedisCallback<Void>() { @Override public Void doInRedis(RedisConnection connection) throws DataAccessException { connection.sRem(converter.getConversionService().convert(event.getKeyspace(), byte[].class), event.getId()); new IndexWriter(connection, converter).removeKeyFromIndexes(event.getKeyspace(), event.getId()); return null; } }); publishEvent(event); } private boolean isKeyExpirationMessage(Message message) { if (message == null || message.getChannel() == null || message.getBody() == null) { return false; } byte[][] args = ByteUtils.split(message.getBody(), ':'); if (args.length != 2) { return false; } return true; } } /** * @author Christoph Strobl * @since 1.8 */ public static enum EnableKeyspaceEvents { /** * Initializes the {@link KeyExpirationEventMessageListener} on startup. */ ON_STARTUP, /** * Initializes the {@link KeyExpirationEventMessageListener} on first insert having expiration time set. */ ON_DEMAND, /** * Turn {@link KeyExpirationEventMessageListener} usage off. No expiration events will be received. */ OFF } /** * Container holding update information like fields to remove from the Redis Hash. * * @author Christoph Strobl */ private static class RedisUpdateObject { private final String keyspace; private final Object targetId; private final byte[] targetKey; private Set<byte[]> fieldsToRemove = new LinkedHashSet<byte[]>(); private Set<Index> indexesToUpdate = new LinkedHashSet<Index>(); RedisUpdateObject(byte[] targetKey, String keyspace, Object targetId) { this.targetKey = targetKey; this.keyspace = keyspace; this.targetId = targetId; } void addFieldToRemove(byte[] field) { fieldsToRemove.add(field); } void addIndexToUpdate(Index index) { indexesToUpdate.add(index); } static class Index { final DataType type; final byte[] key; public Index(byte[] key, DataType type) { this.key = key; this.type = type; } } } }