Redis缓存Object,List对象

原文地址  转载 转载 转载

一、到目前为止(jedis-2.2.0.jar),在Jedis中其实并没有提供这样的API对对象,或者是List对象的直接缓存,即并没有如下类似的API

jedis.set(String key, Object value)

jedis.set(String key, List<M> values)

而更多的API是类似于 jedis.set(String key, String value)或者jedis.set(String key, String ... value)

那如果我们想缓存对象,怎么办呢?

二、通过研究Jedis的API,我们发现其提供了这样的API

jedis.set(byte[], byte[]),

通过这个API,很显然我们能够实现

jedis.set(String key, Object value)

jedis.set(String key, List<M> values)

我们需要关注的就是在cache的时候将Object和List对象转换成字节数组并且需要提供将字节数组转换成对象返回。

三、考虑到扩展性,设计了3个类,抽象父类SerializeTranscoder, 子类ListTranscoder和ObjectsTranscoder 以及一个Unit test 类 用于模拟对象,list对象和字节数组的转换,即Serialize和de-searilize的过程。

1. SerializeTranscoder

package com.chuanliu.platform.activity.basic.util.serialize;

import java.io.Closeable;

import org.apache.log4j.Logger;


/**
 * @author Josh Wang(Sheng)
 *
 * @email  josh_wang23@hotmail.com
 */
public abstract class SerializeTranscoder {

  protected static Logger logger = Logger.getLogger(SerializeTranscoder.class);
  
  public abstract byte[] serialize(Object value);
  
  public abstract Object deserialize(byte[] in);
  
  public void close(Closeable closeable) {
    if (closeable != null) {
      try {
        closeable.close();
      } catch (Exception e) {
         logger.info("Unable to close " + closeable, e); 
      }
    }
  }
}

2. ListTranscoder

package com.chuanliu.platform.activity.basic.util.serialize;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

import com.chuanliu.platform.activity.basic.util.LoggerUtils;


/**
 * Case 1.
 * Jedis does not support cache the Object directly, the Objects needed to be 
 * serialized and de-serialized
 * 
 * Case 2.
 * coming very soon...
 * 
 * @author Josh Wang(Sheng)
 *
 * @email  josh_wang23@hotmail.com
 */
public class ListTranscoder<M extends Serializable> extends SerializeTranscoder {
  
  @SuppressWarnings("unchecked")
  public List<M> deserialize(byte[] in) {
    List<M> list = new ArrayList<>();
    ByteArrayInputStream bis = null;
    ObjectInputStream is = null;
    try {
      if (in != null) {
        bis = new ByteArrayInputStream(in);
        is = new ObjectInputStream(bis);
        while (true) {
          M m = (M)is.readObject();
          if (m == null) {
            break;
          }
          
          list.add(m);
          
        }
        is.close();
        bis.close();
      }
    } catch (IOException e) {  
      LoggerUtils.error(logger, String.format("Caught IOException decoding %d bytes of data",  
        in == null ? 0 : in.length) + e);  
  } catch (ClassNotFoundException e) {  
      LoggerUtils.error(logger, String.format("Caught CNFE decoding %d bytes of data",  
        in == null ? 0 : in.length) + e);  
  }  finally {
      close(is);
      close(bis);
    }
    
    return  list;
  }
  

  @SuppressWarnings("unchecked")
  @Override
  public byte[] serialize(Object value) {
    if (value == null)
      throw new NullPointerException("Can't serialize null");
    
    List<M> values = (List<M>) value;
    
    byte[] results = null;
    ByteArrayOutputStream bos = null;
    ObjectOutputStream os = null;
    
    try {
      bos = new ByteArrayOutputStream();
      os = new ObjectOutputStream(bos);
      for (M m : values) {
        os.writeObject(m);
      }
      
      // os.writeObject(null);
      os.close();
      bos.close();
      results = bos.toByteArray();
    } catch (IOException e) {
      throw new IllegalArgumentException("Non-serializable object", e);
    } finally {
      close(os);
      close(bos);
    }
    
    return results;
  }

  
}

3. ObjectsTranscoder

package com.chuanliu.platform.activity.basic.util.serialize;



import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

import com.chuanliu.platform.activity.basic.util.LoggerUtils;

/**
 * Case 1.
 * Jedis does not support cache the Object directly, the Objects needed to be 
 * serialized and de-serialized
 * 
 * Case 2.
 * coming very soon...
 * 
 * @author Josh Wang(Sheng)
 *
 * @email  josh_wang23@hotmail.com
 */
public class ObjectsTranscoder<M extends Serializable> extends SerializeTranscoder {

  
  @SuppressWarnings("unchecked")
  @Override
  public byte[] serialize(Object value) {
    if (value == null) {  
      throw new NullPointerException("Can't serialize null");  
    }  
    byte[] result = null;  
    ByteArrayOutputStream bos = null;  
    ObjectOutputStream os = null;  
    try {  
      bos = new ByteArrayOutputStream();  
      os = new ObjectOutputStream(bos);
      M m = (M) value;
      os.writeObject(m);  
      os.close();  
      bos.close();  
      result = bos.toByteArray();  
    } catch (IOException e) {  
      throw new IllegalArgumentException("Non-serializable object", e);  
    } finally {  
      close(os);  
      close(bos);  
    }  
    return result;  
  }

  @SuppressWarnings("unchecked")
  @Override
  public M deserialize(byte[] in) {
    M result = null;  
    ByteArrayInputStream bis = null;  
    ObjectInputStream is = null;  
    try {  
      if (in != null) {  
        bis = new ByteArrayInputStream(in);  
        is = new ObjectInputStream(bis);  
        result = (M) is.readObject();  
        is.close();  
        bis.close();  
      }  
    } catch (IOException e) {  
      LoggerUtils.error(logger, String.format("Caught IOException decoding %d bytes of data",  
          in == null ? 0 : in.length) + e);  
    } catch (ClassNotFoundException e) {  
      LoggerUtils.error(logger, String.format("Caught CNFE decoding %d bytes of data",  
          in == null ? 0 : in.length) + e);  
    } finally {  
      close(is);  
      close(bis);  
    }  
    return result;  
  }
}

4. TestSerializerTranscoder

package com.chuanliu.platform.activity.basic.util;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

import org.junit.Test;

import com.chuanliu.platform.activity.basic.util.serialize.ListTranscoder;
import com.chuanliu.platform.activity.basic.util.serialize.ObjectsTranscoder;

/**
 * @author Josh Wang(Sheng)
 *
 * @email  josh_wang23@hotmail.com
 */
public class TestSerializerTranscoder implements Serializable {
  
  private static final long serialVersionUID = -1941046831377985500L;

  public TestSerializerTranscoder() {
    
  }

  @Test
  public void testObject() {
    List<TestUser> lists = buildTestData();
    
    TestUser userA = lists.get(0);
    
    ObjectsTranscoder<TestUser> objTranscoder =  new ObjectsTranscoder<>();
    
    byte[] result1 = objTranscoder.serialize(userA);
    
    TestUser userA_userA = objTranscoder.deserialize(result1);
    
    System.out.println(userA_userA.getName() + "\t" + userA_userA.getAge());
  }
  
  @Test
  public void testList() {
    List<TestUser> lists = buildTestData();
    

    ListTranscoder<TestUser> listTranscoder = new ListTranscoder<>();
    
    byte[] result1 = listTranscoder.serialize(lists);
    
    List<TestUser> results = listTranscoder.deserialize(result1);
  
    for (TestUser user : results) {
      System.out.println(user.getName() + "\t" + user.getAge());
    }
    
  }
  
  private static List<TestUser> buildTestData() {
    TestSerializerTranscoder tst = new TestSerializerTranscoder();
    TestUser userA =  tst.new TestUser();
    userA.setName("lily"); 
        userA.setAge(25);
    
        
        TestUser userB = tst.new TestUser();  
        
        
        userB.setName("Josh Wang"); 
    userB.setAge(28);
        
        List<TestUser> list = new ArrayList<TestUser>();  
        list.add(userA);  
        list.add(userB);  
        
        return list;  
    }
  
  class TestUser implements Serializable {
    private static final long serialVersionUID = 1L;

      private String name;
      
      private int age;
      

      public String getName() {
        return name;
      }

      public void setName(String name) {
        this.name = name;
      }

      public int getAge() {
        return age;
      }

      public void setAge(int age) {
        this.age = age;
      }
      
    }
  
  
}

四、通过以上几步后,即可使用Jedis的API进行对象的缓存并将从缓存中返回的二进制数组转换成原始的对象或者是List对象了。

<think>我们正在讨论Java中使用Redis缓存,特别是如何覆盖更新一个List。根据之前引用的内容,我们知道Redis的数据结构存储在redisDb中,其中dict *dict指向键空间(keyspace)。对于List类型,Redis内部使用quicklist实现(在较新版本中),但用户操作时通常使用列表命令。 用户需求:在Java中更新Redis缓存List,要求覆盖更新(即完全替换旧列表)。 实现思路: 1. 使用RedisTemplate(Spring Data Redis)操作Redis。 2. 由于是覆盖更新,我们可以先删除旧的key,再重新设置一个新的列表。或者直接使用set操作覆盖(但Redis的列表没有直接的覆盖命令,通常使用删除后重新添加的方式)。 具体步骤: a. 删除现有的key(如果存在) b. 将新的List整个添加到这个key中 注意:Redis的列表操作命令中,没有直接替换整个列表的命令。但是,我们可以通过事务或管道来保证原子性。 然而,考虑到覆盖更新,我们可以使用以下两种方式: 方式1:使用事务(multi)或管道(pipeline)组合DEL和RPUSH(或LPUSH)命令。 方式2:使用序列化方式存储整个List(比如使用String结构存储序列化的List),然后直接覆盖写。但这样就不能使用列表的命令了(如LPOP, RPUSH等)。 根据用户需求,如果后续需要操作列表(如按索引获取元素,弹出元素等),那么应该使用RedisList结构。如果只是整体存储和读取,也可以考虑使用String结构。 但问题中明确要求更新的是List,所以我们假设用户希望使用RedisList结构。 因此,我们采用: 1. 删除旧的key(使用DEL命令) 2. 将新的List元素全部添加到key中(使用RPUSH或LPUSH) 在Java中,使用Spring Data RedisRedisTemplate,我们可以这样做: 步骤: 1. 获取RedisTemplate实例 2. 使用delete方法删除key 3. 使用leftPushAll或rightPushAll方法添加整个列表 但是,注意:删除和添加两个操作不是原子的。在并发环境下,可能会出现问题。因此,我们可以使用Redis的事务支持,或者使用Lua脚本来保证原子性。 考虑到性能,使用Lua脚本是最佳选择,但这里我们先给出一个简单的事务方式。 示例代码(使用事务): ```java redisTemplate.execute(new SessionCallback<Object>() { @Override public Object execute(RedisOperations operations) throws DataAccessException { operations.multi(); // 开启事务 operations.delete(key); // 删除旧列表 operations.opsForList().rightPushAll(key, newList); // 添加新列表 operations.exec(); // 执行事务 return null; } }); ``` 或者使用管道(pipeline)来提高性能(特别是在列表很大时): ```java redisTemplate.executePipelined(new SessionCallback<Object>() { @Override public Object execute(RedisOperations operations) throws DataAccessException { operations.delete(key); operations.opsForList().rightPushAll(key, newList); return null; } }); ``` 注意:管道不是原子的,但可以批量发送命令,减少网络开销。而事务是原子的。根据需求选择。 另外,如果用户不需要事务,也可以直接执行两个命令(因为覆盖更新本身就是先删后加,如果中间没有其他操作,在非并发情况下也可以)。 但是,在并发环境下,多个客户端可能同时更新同一个key,因此需要根据业务场景决定是否使用事务或乐观锁(watch)等机制。 然而,覆盖更新通常意味着该key对应的数据完全由当前更新决定,所以一般情况下,先删除再添加是安全的(即使并发,最终结果也是最后一次更新的数据,但中间状态可能不一致)。如果要求强一致性,则需要使用事务或分布式锁。 因此,我们给出两种方案: 方案1(非原子,但简单): ```java redisTemplate.delete(key); redisTemplate.opsForList().rightPushAll(key, newList); ``` 方案2(使用事务保证原子性): 如上所示的事务代码。 方案3(使用Lua脚本,原子性): 编写Lua脚本: ```lua -- 参数:key, 新的列表元素(用数组传递) -- 先删除key,然后重新创建列表 redis.call('DEL', KEYS[1]) for i, v in ipairs(ARGV) do redis.call('RPUSH', KEYS[1], v) end ``` 在Java中执行Lua脚本: ```java // 定义脚本 String script = "local key = KEYS[1]\n" + "redis.call('DEL', key)\n" + "for i, v in ipairs(ARGV) do\n" + " redis.call('RPUSH', key, v)\n" + "end"; DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class); // 执行脚本,参数:key, 其他参数(列表元素) // 注意:将列表元素转换为数组,作为ARGV传递 List<String> keys = new ArrayList<>(); keys.add(key); // 将newList转换为字符串数组(根据序列化器,这里假设元素是字符串) String[] args = newList.toArray(new String[0]); redisTemplate.execute(redisScript, keys, args); ``` 注意:Lua脚本中的元素传递需要根据实际情况调整,如果列表元素不是字符串,需要序列化。这里假设列表元素是字符串。 另外,使用Lua脚本时,要注意脚本不能太大(因为ARGV有长度限制),如果列表元素非常多,可能会导致问题。此时,建议使用管道批量添加。 总结: 根据列表大小和并发要求选择合适的方式: - 小列表且并发要求不高:方案1(简单删除再添加) - 需要事务保证:方案2(事务) - 需要原子性且列表不是特别大:方案3(Lua脚本) 另外,引用[2]中提到Redis的渐进式Rehash,但这里我们操作的是单个key,不会触发Rehash(除非整个Redis正在扩容),所以不用考虑。 但是,注意:如果列表非常大,一次性添加可能会阻塞Redis(因为Redis是单线程)。所以,对于大列表,建议分批次更新(比如使用管道分批添加),或者考虑使用其他数据结构(如分片存储)。 不过,覆盖更新通常要求整体替换,所以如果列表非常大,可以考虑使用String结构存储序列化后的整个列表,然后使用set命令覆盖。这样只需要一次操作,但无法使用列表命令。 因此,如果用户需要频繁操作列表中的元素,则使用List结构;如果只是整体存取,则使用String结构。 针对String结构的覆盖更新: ```java // 假设使用StringRedisTemplate stringRedisTemplate.opsForValue().set(key, objectMapper.writeValueAsString(newList)); ``` 这里需要将List序列化为字符串(如JSON)。 所以,最终需要根据实际业务场景选择数据结构。 建议:如果业务中需要操作列表中的单个元素(如通过索引获取、修改某个元素),则使用List结构;如果总是整体读取和更新,则使用String结构。 因此,在回答中,我们应该先询问用户的具体使用场景,或者根据问题描述(更新List)假设用户需要List结构。 根据问题“update cached list with overwrite”,我们假设用户需要List结构,所以我们提供List结构的覆盖更新方法。 最后,我们给出一个完整的示例(使用事务方式): ```java @Autowired private RedisTemplate<String, Object> redisTemplate; public void overwriteList(String key, List<Object> newList) { redisTemplate.execute(new SessionCallback<Object>() { @Override public Object execute(RedisOperations operations) throws DataAccessException { operations.multi(); operations.delete(key); operations.opsForList().rightPushAll(key, newList); operations.exec(); return null; } }); } ``` 注意:这里RedisTemplate的value序列化器需要能够序列化List中的元素类型。 如果使用StringRedisTemplate,则List中的元素必须是字符串,或者需要手动序列化。 因此,我们假设用户已经配置好了合适的RedisTemplate。 相关问题: 1. 在覆盖更新Redis列表时,如何保证原子性? 2. 如果列表非常大,如何优化覆盖更新的性能? 3. 使用List结构和String结构存储列表各自的优缺点是什么?</think>### Java中Redis缓存List的覆盖更新方法 在Java中使用Redis覆盖更新List缓存时,核心思路是**先删除旧列表再创建新列表**。RedisList数据结构不支持直接覆盖更新,需要通过组合操作实现原子性替换。以下是具体实现方法: #### 方法1:事务操作(推荐) 使用Redis事务保证删除和重建的原子性: ```java // 使用Spring Data RedisRedisTemplate public void overwriteList(String key, List<String> newList) { redisTemplate.execute(new SessionCallback<>() { public Object execute(RedisOperations operations) { operations.multi(); // 开启事务 operations.delete(key); // 删除旧列表 operations.opsForList().rightPushAll(key, newList); // 添加新列表 operations.exec(); // 提交事务 return null; } }); } ``` #### 方法2:Lua脚本 通过Lua脚本实现原子操作(性能更优): ```java // 定义Lua脚本 String script = "redis.call('DEL', KEYS[1])\n" + "for i, v in ipairs(ARGV) do\n" + " redis.call('RPUSH', KEYS[1], v)\n" + "end"; // 执行脚本 public void overwriteList(String key, List<String> newList) { RedisScript<Void> redisScript = new DefaultRedisScript<>(script, Void.class); redisTemplate.execute(redisScript, Collections.singletonList(key), newList.toArray()); } ``` #### 方法3:管道操作 适用于大数据量场景,减少网络开销: ```java public void overwriteList(String key, List<String> newList) { redisTemplate.executePipelined((RedisCallback<Object>) connection -> { connection.del(key.getBytes()); // 删除旧数据 for (String item : newList) { connection.rPush(key.getBytes(), item.getBytes()); // 批量添加新数据 } return null; }); } ``` ### 关键注意事项 1. **原子性保障** 事务或Lua脚本可防止删除后重建过程中出现其他操作干扰[^2]。 2. **性能优化** - 大数据量时使用管道(pipeline)减少网络往返次数 - 考虑使用渐进式更新(部分替换)替代全量覆盖[^2] 3. **序列化一致性** 确保RedisTemplate的序列化配置与数据类型匹配: ```java // 配置示例 redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(List.class)); ``` 4. **异常处理** 添加重试机制应对网络波动: ```java @Retryable(maxAttempts = 3, backoff = @Backoff(delay = 100)) public void safeOverwrite(...) { ... } ``` ### 不同场景选择建议 | **场景** | **推荐方案** | **优势** | |------------------------|--------------------|------------------------------| | 小数据量+强一致性要求 | Lua脚本 | 原子性保证 | | 大数据量(>1000元素) | 管道操作 | 网络开销最小化 | | 需要事务管理 | Redis事务 | 与Spring事务体系集成 | > 注:RedisList底层采用quicklist结构(压缩列表+链表组合),全量替换时需评估内存开销[^1]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值