【问题标题】:Why is data getting stored with weird keys in Redis when using Jedis with Spring Data?为什么在将 Jedis 与 Spring Data 结合使用时,数据会以奇怪的键存储在 Redis 中?
【发布时间】:2023-04-10 10:14:01
【问题描述】:

我正在将 Spring Data Redis 与 Jedis 一起使用。我正在尝试使用密钥vc:${list_id} 存储散列。我能够成功插入到redis。但是,当我使用 redis-cli 检查密钥时,我看不到密钥 vc:501381。相反,我看到了\xac\xed\x00\x05t\x00\tvc:501381

为什么会发生这种情况,我该如何改变?

【问题讨论】:

    标签: redis spring-data jedis


    【解决方案1】:

    好的,在 Google 上搜索了一段时间,并在 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中的key是vc:501381

    或者像@niconic 说的那样,我们也可以将默认序列化器本身设置为字符串序列化器,如下所示:

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

    这意味着我们所有的键和值都是字符串。但是请注意,这可能并不可取,因为您可能希望您的值不仅仅是字符串。

    如果您的值是域对象,那么您可以使用 Jackson 序列化器并配置序列化器,如 here 所述,即:

    <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"
    />
    

    【讨论】:

    • 如果更改所有序列化程序引用,直接更改默认序列化程序可能很有用。
    • 请注意,如果将convertAndSendredisTemplate 一起使用,则p:keySerializerhashKeySerializer 是不够的。更改 default serializer 完成了这项工作
    • 谢谢!因为我使用了spring boot,我通过参考这个文档解决了这个问题:stackoverflow.com/questions/34201135/…
    【解决方案2】:

    我知道这个问题已经有一段时间了,但是我最近又对这个话题做了一些研究,所以我想在这里分享一下这个“半散列”密钥是如何通过部分 spring 源代码生成的。

    首先,Spring利用AOP来解析@Cacheable, @CacheEvict or @CachePut等注解。通知类是来自Spring-context依赖的CacheInterceptor,它是CacheAspectSupport的子类(也来自Spring-context)。为了便于解释,我将使用@Cacheable 作为示例,在这里查看部分源代码。

    当注解为@Cacheable 的方法被调用时,AOP 会将其从CacheAspectSupport 类路由到此方法protected Collection&lt;? extends Cache&gt; getCaches(CacheOperationInvocationContext&lt;CacheOperation&gt; context, CacheResolver cacheResolver),并在其中尝试解析此@Cacheable 注解。反过来,它会导致在实现 CacheManager 中调用此方法public Cache getCache(String name)。对于这个解释,实现 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)、expiration(默认为 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 随后会向前调用上述RedisCache 对象的private RedisCacheKey getRedisCacheKey(Object key) 方法,该方法通过利用前缀 中的前缀 返回RedisCacheKey 的实例RedisCacheMetadata 和来自 RedisOperation 的 keySerializer。

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

    到此为止,CacheInterceptor 的“pre”advice 就完成了,它会继续执行@Cacheable 注释的实际方法。并且在完成实际方法的执行后,它会做CacheInterceptor 的“post”advice,本质上是把结果放到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中的实际key,方法名是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)。

    【讨论】:

    • 解释得很好。
    【解决方案3】:

    这是一个非常古老的问题,但我的回答可能对在使用 Redis using 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;
        }
    
    }
    

    如果数据类型是字符串,则不需要template.setHashValueSerializer(new StringRedisSerializer());template.setHashKeySerializer(new StringRedisSerializer());

    【讨论】:

    • 是的,它会起作用,但这会产生与开始使用 StringRedisTemplate 完全相同的行为,除了它涉及冗余配置。
    【解决方案4】:

    使用StringRedisTemplate 替换RedisTemplate

    默认情况下,RedisTemplate 使用 Java 序列化,StringRedisTemplate 使用 StringRedisSerializer

    <bean id="stringRedisTemplate" class="org.springframework.data.redis.core.StringRedisTemplate">
        <property name="connectionFactory" ref="jedisConnectionFactory" />
    </bean>
    

    【讨论】:

    • 这是准确的答案。 RedisTemplate 使用 Java 序列化,因此确保您的类是用“implements java.io.Serializable”编写的,这就足够了。
    • 这仅适用于您的 也是字符串的情况,因为 StringRedisTemplate 仅支持字符串值。
    【解决方案5】:

    你必须序列化你发送到 redis 的对象。下面是它的完整运行示例。它使用接口DomainObject作为Serializable

    以下是步骤

    1) 使用以下 jars 制作您的 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;
     }
    
    }
    

    有关 srinf 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

    【讨论】:

      猜你喜欢
      • 2019-05-02
      • 2017-06-12
      • 2018-07-07
      • 2021-02-24
      • 2012-01-08
      • 1970-01-01
      • 2018-12-16
      • 2020-10-02
      • 2022-10-01
      相关资源
      最近更新 更多