Почему данные сохраняются со странными ключами в Redis при использовании Jedis с Spring Data?

Я использую Spring Data Redis с Jedis. Я пытаюсь сохранить хеш с ключом vc:${list_id}. Мне удалось успешно вставить в Redis. Однако, когда я проверяю ключи с помощью redis-cli, я не вижу ключа vc:501381. Вместо этого я вижу \xac\xed\x00\x05t\x00\tvc:501381.

Почему это происходит и как мне это изменить?


person arun    schedule 04.11.2012    source источник


Ответы (5)


Хорошо, немного погуглил и нашел справку по адресу http://java.dzone.com/articles/spring-data-redis.

Произошло это из-за сериализации Java.

Сериализатор ключей для redisTemplate должен быть настроен на StringRedisSerializer, т.е. вот так:

<bean 
    id="jedisConnectionFactory" 
    class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory" 
    p:host-name="${redis.server}" 
    p:port="${redis.port}" 
    p:use-pool="true"/>

<bean 
    id="stringRedisSerializer" 
    class="org.springframework.data.redis.serializer.StringRedisSerializer"/>

<bean 
    id="redisTemplate" 
    class="org.springframework.data.redis.core.RedisTemplate"
    p:connection-factory-ref="jedisConnectionFactory" 
    p:keySerializer-ref="stringRedisSerializer"
    p:hashKeySerializer-ref="stringRedisSerializer" 
/>

Теперь ключ в Redis - vc:501381.

Или, как говорит @niconic, мы также можем установить сериализатор по умолчанию для сериализатора строк следующим образом:

<bean 
    id="redisTemplate" 
    class="org.springframework.data.redis.core.RedisTemplate"
    p:connection-factory-ref="jedisConnectionFactory" 
    p:defaultSerializer-ref="stringRedisSerializer"
/>

что означает, что все наши ключи и значения являются строками. Однако обратите внимание, что это может быть нежелательно, поскольку вы можете захотеть, чтобы ваши значения были не просто строками.

Если ваше значение является объектом домена, вы можете использовать сериализатор Джексона и настроить сериализатор, как указано здесь, то есть вот так:

<bean id="userJsonRedisSerializer" class="org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer">
    <constructor-arg type="java.lang.Class" value="com.mycompany.redis.domain.User"/>
</bean>

и настройте свой шаблон как:

<bean 
    id="redisTemplate" 
    class="org.springframework.data.redis.core.RedisTemplate"
    p:connection-factory-ref="jedisConnectionFactory" 
    p:keySerializer-ref="stringRedisSerializer"
    p:hashKeySerializer-ref="stringRedisSerializer" 
    p:valueSerialier-ref="userJsonRedisSerializer"
/>
person arun    schedule 04.11.2012
comment
При изменении всех ссылок на сериализаторы может быть полезно напрямую изменить сериализатор по умолчанию. - person reallynice; 28.11.2013
comment
обратите внимание, что если вы используете convertAndSend с redisTemplate, p:keySerializer и hashKeySerializer недостаточно. изменение default serializer выполнило свою работу - person oak; 19.08.2015
comment
Спасибо! Поскольку я использовал весеннюю загрузку и решил это, сославшись на этот документ: stackoverflow.com/questions/34201135/ - person zhuguowei; 11.06.2016

Я знаю, что этот вопрос был давно, но недавно я снова провел некоторое исследование по этой теме, поэтому я хотел бы поделиться, как этот «полухешированный» ключ генерируется путем прохождения части исходного кода Spring здесь.

Прежде всего, Spring использует АОП для разрешения аннотаций типа @Cacheable, @CacheEvict or @CachePut и т. Д. Класс совета - это CacheInterceptor из зависимости контекста Spring, которая является подклассом CacheAspectSupport (также из контекста Spring). Для простоты объяснения я бы использовал @Cacheable в качестве примера, чтобы пройти здесь по части исходного кода.

Когда вызывается метод, помеченный как @Cacheable, АОП направит его к этому методу protected Collection<? extends Cache> getCaches(CacheOperationInvocationContext<CacheOperation> context, CacheResolver cacheResolver) из класса CacheAspectSupport, в котором он попытается разрешить эту @Cacheable аннотацию. В свою очередь, это приводит к вызову этого метода public Cache getCache(String name) в реализации CacheManager. Для этого объяснения реализация CacheManage будет RedisCacheManager (из зависимости Spring-data-redis).

Если кеш не был поражен, он продолжит создание кеша. Ниже приведены ключевые методы из RedisCacheManager:

protected Cache getMissingCache(String name) {
    return this.dynamic ? createCache(name) : null;
}

@SuppressWarnings("unchecked")
protected RedisCache createCache(String cacheName) {
    long expiration = computeExpiration(cacheName);
    return new RedisCache(cacheName, (usePrefix ? cachePrefix.prefix(cacheName) : null), redisOperations, expiration,
            cacheNullValues);
}

По сути, он создаст экземпляр объекта RedisCache. Для этого требуются 4 параметра, а именно cacheName, prefix (это ключевой параметр для ответа на этот вопрос), redisOperation (он же настроенный redisTemplate), срок действия (по умолчанию 0) и cacheNullValues ​​(по умолчанию false). Конструктор ниже показывает более подробную информацию о RedisCache.

/**
 * Constructs a new {@link RedisCache} instance.
 *
 * @param name cache name
 * @param prefix must not be {@literal null} or empty.
 * @param redisOperations
 * @param expiration
 * @param allowNullValues
 * @since 1.8
 */
public RedisCache(String name, byte[] prefix, RedisOperations<? extends Object, ? extends Object> redisOperations,
        long expiration, boolean allowNullValues) {

    super(allowNullValues);

    Assert.hasText(name, "CacheName must not be null or empty!");

    RedisSerializer<?> serializer = redisOperations.getValueSerializer() != null ? redisOperations.getValueSerializer()
            : (RedisSerializer<?>) new JdkSerializationRedisSerializer();

    this.cacheMetadata = new RedisCacheMetadata(name, prefix);
    this.cacheMetadata.setDefaultExpiration(expiration);
    this.redisOperations = redisOperations;
    this.cacheValueAccessor = new CacheValueAccessor(serializer);

    if (allowNullValues) {

        if (redisOperations.getValueSerializer() instanceof StringRedisSerializer
                || redisOperations.getValueSerializer() instanceof GenericToStringSerializer
                || redisOperations.getValueSerializer() instanceof JacksonJsonRedisSerializer
                || redisOperations.getValueSerializer() instanceof Jackson2JsonRedisSerializer) {
            throw new IllegalArgumentException(String.format(
                    "Redis does not allow keys with null value ¯\\_(ツ)_/¯. "
                            + "The chosen %s does not support generic type handling and therefore cannot be used with allowNullValues enabled. "
                            + "Please use a different RedisSerializer or disable null value support.",
                    ClassUtils.getShortName(redisOperations.getValueSerializer().getClass())));
        }
    }
}

Так в чем же польза от prefix в этом RedisCache? -> Как показано в конструкторе about, он используется в этом операторе this.cacheMetadata = new RedisCacheMetadata(name, prefix);, а конструктор RedisCacheMetadata ниже показывает более подробную информацию:

/**
     * @param cacheName must not be {@literal null} or empty.
     * @param keyPrefix can be {@literal null}.
     */
    public RedisCacheMetadata(String cacheName, byte[] keyPrefix) {

        Assert.hasText(cacheName, "CacheName must not be null or empty!");
        this.cacheName = cacheName;
        this.keyPrefix = keyPrefix;

        StringRedisSerializer stringSerializer = new StringRedisSerializer();

        // name of the set holding the keys
        this.setOfKnownKeys = usesKeyPrefix() ? new byte[] {} : stringSerializer.serialize(cacheName + "~keys");
        this.cacheLockName = stringSerializer.serialize(cacheName + "~lock");
    }

На данный момент мы знаем, что для некоторого параметра префикса было установлено значение RedisCacheMetadata, но как именно этот префикс используется для формирования ключа в Redis (например, \ xac \ xed \ x00 \ x05t \ x00 \ tvc: 501381, как вы упомянули) ?

По сути, CacheInterceptor впоследствии перейдет к вызову метода private RedisCacheKey getRedisCacheKey(Object key) из вышеупомянутого объекта RedisCache, который возвращает экземпляр RedisCacheKey, используя префикс из RedisCacheMetadata и keySerializer из RedisOperation.

private RedisCacheKey getRedisCacheKey(Object key) {
    return new RedisCacheKey(key).usePrefix(this.cacheMetadata.getKeyPrefix())
            .withKeySerializer(redisOperations.getKeySerializer());
}

Достигнув этой точки, "предварительная" рекомендация CacheInterceptor завершается, и он переходит к выполнению фактического метода, аннотированного @Cacheable. И после завершения выполнения фактического метода он выполнит «отправку» совета CacheInterceptor, который, по сути, отправит результат в RedisCache. Ниже приведен метод помещения результата в кеш Redis:

public void put(final Object key, final Object value) {

    put(new RedisCacheElement(getRedisCacheKey(key), toStoreValue(value))
            .expireAfter(cacheMetadata.getDefaultExpiration()));
}

/**
 * Add the element by adding {@link RedisCacheElement#get()} at {@link RedisCacheElement#getKeyBytes()}. If the cache
 * previously contained a mapping for this {@link RedisCacheElement#getKeyBytes()}, the old value is replaced by
 * {@link RedisCacheElement#get()}.
 *
 * @param element must not be {@literal null}.
 * @since 1.5
 */
public void put(RedisCacheElement element) {

    Assert.notNull(element, "Element must not be null!");

    redisOperations
            .execute(new RedisCachePutCallback(new BinaryRedisCacheElement(element, cacheValueAccessor), cacheMetadata));
}

Внутри объекта RedisCachePutCallback его метод обратного вызова doInRedis() фактически вызывает метод для формирования фактического ключа в redis, а имя метода - getKeyBytes() из RedisCacheKey экземпляра. Ниже показаны подробности этого метода:

/**
 * Get the {@link Byte} representation of the given key element using prefix if available.
 */
public byte[] getKeyBytes() {

    byte[] rawKey = serializeKeyElement();
    if (!hasPrefix()) {
        return rawKey;
    }

    byte[] prefixedKey = Arrays.copyOf(prefix, prefix.length + rawKey.length);
    System.arraycopy(rawKey, 0, prefixedKey, prefix.length, rawKey.length);

    return prefixedKey;
}

Как мы видим в методе getKeyBytes, он использует как необработанный ключ (vc: 501381 в вашем случае), так и префиксный ключ (\ xac \ xed \ x00 \ x05t \ x00 \ t в вашем случае).

person imarchuang    schedule 27.10.2017
comment
Очень красиво объяснено. - person ScanQR; 09.06.2018

Это очень старый вопрос, но мой ответ может быть полезен тем, у кого возникла та же проблема при работе с Redis с помощью Spring Boot. Я застрял на той же проблеме при хранении данных хеш-типа в Redis. Я написал необходимые изменения файла конфигурации для RedisTemplate.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan(basePackages = "com.redis")
public class AppCofiguration {

    @Bean
    JedisConnectionFactory jedisConnectionFactory() {
        JedisConnectionFactory jedisConFactory = new JedisConnectionFactory();
        jedisConFactory.setHostName("127.0.0.1");
        jedisConFactory.setPort(6379);
        return jedisConFactory;
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        final RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
        template.setConnectionFactory(jedisConnectionFactory());
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new StringRedisSerializer());

        // the following is not required      
        template.setHashValueSerializer(new StringRedisSerializer());
        template.setHashKeySerializer(new StringRedisSerializer());

        return template;
    }

}

Если тип данных String, template.setHashValueSerializer(new StringRedisSerializer()); и template.setHashKeySerializer(new StringRedisSerializer()); не требуются.

person Mithun Debnath    schedule 10.05.2018
comment
Да, это будет работать, но это дает то же поведение, что и использование StringRedisTemplate для начала, за исключением того, что это связано с избыточными конфигурациями. - person Tom Silverman; 05.07.2021

Используйте StringRedisTemplate для замены RedisTemplate.

По умолчанию RedisTemplate использует сериализацию Java, StringRedisTemplate использует StringRedisSerializer.

<bean id="stringRedisTemplate" class="org.springframework.data.redis.core.StringRedisTemplate">
    <property name="connectionFactory" ref="jedisConnectionFactory" />
</bean>
person xxg    schedule 09.10.2016
comment
Это точный ответ. RedisTemplate использует сериализацию Java, поэтому убедитесь, что ваш класс написан с помощью java.io.Serializable, этого достаточно. - person Kanagavelu Sugumar; 14.11.2016
comment
Это работает, только если ваши значения также являются строками, поскольку StringRedisTemplate поддерживает только строковые значения. - person Madbreaks; 09.01.2020

Вам необходимо сериализовать объекты, которые вы отправляете в redis. Ниже приведен полный рабочий пример этого. Он использует интерфейс DomainObject как Serializable

Ниже приведены шаги

1) сделайте свой maven pom.xml со следующими банками

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-core</artifactId>
        <version>${spring.version}</version>
    </dependency>

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>${spring.version}</version>
    </dependency>

    <dependency>
        <groupId>cglib</groupId>
        <artifactId>cglib</artifactId>
        <version>2.2.2</version>
    </dependency>

    <dependency>
           <groupId>org.springframework.data</groupId>
           <artifactId>spring-data-redis</artifactId>
           <version>1.3.0.RELEASE</version>
        </dependency>

            <dependency>
               <groupId>redis.clients</groupId>
               <artifactId>jedis</artifactId>
               <version>2.4.1</version>
            </dependency>

    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-pool2</artifactId>
        <version>2.0</version>
    </dependency>

2) сделайте свою конфигурацию xml следующим образом

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:p="http://www.springframework.org/schema/p"
        xmlns:c="http://www.springframework.org/schema/c"
        xmlns:cache="http://www.springframework.org/schema/cache"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans     
        http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
        http://www.springframework.org/schema/context 
        http://www.springframework.org/schema/context/spring-context-3.0.xsd

        http://www.springframework.org/schema/cache 
        http://www.springframework.org/schema/cache/spring-cache.xsd">



    <bean id="jeidsConnectionFactory"
      class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory"
      p:host-name="localhost" p:port="6379" p:password="" />

     <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate"
      p:connection-factory-ref="jeidsConnectionFactory" />

     <bean id="imageRepository" class="com.self.common.api.poc.ImageRepository">
      <property name="redisTemplate" ref="redisTemplate"/>
     </bean>

</beans>

3) Сделайте свои классы следующим образом

package com.self.common.api.poc;

import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;

import javax.imageio.ImageIO;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;

public class RedisMainApp {

 public static void main(String[] args) throws IOException {
  ApplicationContext applicationContext = new ClassPathXmlApplicationContext("mvc-dispatcher-servlet.xml");
  ImageRepository imageRepository = (ImageRepository) applicationContext.getBean("imageRepository");

  BufferedImage img = ImageIO.read(new File("files/img/TestImage.png"));
  BufferedImage newImg;
  String imagestr;
  imagestr = encodeToString(img, "png");
  Image image1 = new Image("1", imagestr);

  img = ImageIO.read(new File("files/img/TestImage2.png"));
  imagestr = encodeToString(img, "png");
  Image image2 = new Image("2", imagestr);

  imageRepository.put(image1);
  System.out.println(" Step 1 output : " + imageRepository.getObjects());
  imageRepository.put(image2);
  System.out.println(" Step 2 output : " + imageRepository.getObjects());
  imageRepository.delete(image1);
  System.out.println(" Step 3 output : " + imageRepository.getObjects());

 }

 /**
  * Decode string to image
  * @param imageString The string to decode
  * @return decoded image
  */
 public static BufferedImage decodeToImage(String imageString) {

     BufferedImage image = null;
     byte[] imageByte;
     try {
         BASE64Decoder decoder = new BASE64Decoder();
         imageByte = decoder.decodeBuffer(imageString);
         ByteArrayInputStream bis = new ByteArrayInputStream(imageByte);
         image = ImageIO.read(bis);
         bis.close();
     } catch (Exception e) {
         e.printStackTrace();
     }
     return image;
 }

 /**
  * Encode image to string
  * @param image The image to encode
  * @param type jpeg, bmp, ...
  * @return encoded string
  */
 public static String encodeToString(BufferedImage image, String type) {
     String imageString = null;
     ByteArrayOutputStream bos = new ByteArrayOutputStream();

     try {
         ImageIO.write(image, type, bos);
         byte[] imageBytes = bos.toByteArray();

         BASE64Encoder encoder = new BASE64Encoder();
         imageString = encoder.encode(imageBytes);

         bos.close();
     } catch (IOException e) {
         e.printStackTrace();
     }
     return imageString;
 }
}

package com.self.common.api.poc;

public class Image implements DomainObject {

 public static final String OBJECT_KEY = "IMAGE";

 public Image() {
 }

 public Image(String imageId, String imageAsStringBase64){
  this.imageId = imageId;
  this.imageAsStringBase64 = imageAsStringBase64;
 }
 private String imageId;
 private String imageAsStringBase64;

 public String getImageId() {
  return imageId;
 }

 public void setImageId(String imageId) {
  this.imageId = imageId;
 }

 public String getImageName() {
  return imageAsStringBase64;
 }

 public void setImageName(String imageAsStringBase64) {
  this.imageAsStringBase64 = imageAsStringBase64;
 }

 @Override
 public String toString() {
  return "User [id=" + imageAsStringBase64 + ", imageAsBase64String=" + imageAsStringBase64 + "]";
 }

 @Override
 public String getKey() {
  return getImageId();
 }

 @Override
 public String getObjectKey() {
  return OBJECT_KEY;
 }
}

package com.self.common.api.poc;

import java.io.Serializable;

public interface DomainObject extends Serializable {

 String getKey();

 String getObjectKey();
}

package com.self.common.api.poc;

import java.util.List;

import com.self.common.api.poc.DomainObject;

public interface Repository<V extends DomainObject> {

 void put(V obj);

 V get(V key);

 void delete(V key);

 List<V> getObjects();
}

package com.self.common.api.poc;

import java.util.ArrayList;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;

import com.self.common.api.poc.DomainObject;

public class ImageRepository implements Repository<Image>{

 @Autowired
 private RedisTemplate<String,Image> redisTemplate;

 public RedisTemplate<String,Image> getRedisTemplate() {
  return redisTemplate;
 }

 public void setRedisTemplate(RedisTemplate<String,Image> redisTemplate) {
  this.redisTemplate = redisTemplate;
 }

 @Override
 public void put(Image image) {
  redisTemplate.opsForHash()
    .put(image.getObjectKey(), image.getKey(), image);
 }

 @Override
 public void delete(Image key) {
  redisTemplate.opsForHash().delete(key.getObjectKey(), key.getKey());
 }

 @Override
 public Image get(Image key) {
  return (Image) redisTemplate.opsForHash().get(key.getObjectKey(),
    key.getKey());
 }

 @Override
 public List<Image> getObjects() {
  List<Image> users = new ArrayList<Image>();
  for (Object user : redisTemplate.opsForHash().values(Image.OBJECT_KEY) ){
   users.add((Image) user);
  }
  return users;
 }

}

Дополнительную информацию о sprinf jedis можно найти на странице http://www.javacodegeeks.com/2012/06/using-redis-with-spring.html

Пример кода взят из http://javakart.blogspot.in/2012/12/spring-data-redis-hello-world-example.html.

person ravi ranjan    schedule 10.06.2014