Redis工作总结

1. 需求:把ChrmMember类存入Redis, 以备重启server后,聊天室的成员不会丢失。

ChrmMember类的结构:
-- private int msgNum; //聊天室中的消息数
-- private long lastMsgTime; //聊天室中最新的消息时间
-- private Stack<String> list = new Stack<>(); //存储聊天室成员userId的stack, 按加入聊天室时间顺序存储
-- private HashMap<String, Member> members = new HashMap<String, Member>(); //hashmap的聊天室成员,key是userId, value是Member类的实例,表示聊天室成员。

2. 工作要思考的问题:
1)存入redis时候,是否可以把整个ChrmMember类序列化后存储?
答:这样不好。因为将来的扩展性考虑,如果将来要添加ChrmMember或者Member的成员变量,那么redis存储的被序列化过的旧的数据有问题(只有要序列化的类中只包含成员变量的时候,这样当成员变量添加或删除时,旧的数据才可用)。
实际采用的办法是,将ChrmMember拆分成两个类,存到redis里面,分别为:
-- ChrmInfoForRedis.java: 包含msgNum和lastMsgTime
-- ChrmMemForRedis.java: 与Member结构完全一致,只是没有set,get方法,成员变量是public的,这样是为了减小序列化后文件的大小,提高传输性能
这里,我们没有把list存入redis, 而是在反序列化之后通过排序方式重新构建

2)何时调用Redis操作?
在对于缓存做set, get操作时,同样操作redis.

3) 如何设计redis的存储结构?
-- 存储ChrmInfoForRedis: 使用map结构。
选map而不是key/value结构是因为:对于ChrmInfoForRedis类本身要有一个Key,这里我用appId类表示, 第二个key用chatroomId表示,这样既可以查询聊天室个数(根据appId), 也可以查询具体的聊天室(根据appId+chatroomId)。所以,我用map结构,相当于有两个Key, 第一个key标识该业务(存储ChrmInfoForRedis),第二个Key表示聊天室,value表示ChrmInfoForRedis序列化后的结果。

-- 存储ChrmMemForRedis同理:key1: chrmMem_appId_chrmId, key2: userId, value: ChrmMemForRedis反序列化后的结果

4)写redis操作(添加、删除)时需要注意的问题?
最重要的是redis什么时候从redis池获取ShardedJedis的实例,及何时释放资源,通常这样写redis操作。
ShardedJedis jedis = RedisManager.getShardedJedis();
try {
    ...//jedis操作,如:jedis.hdel(key1, key2);
} catch (Exception e) {
    logger.error(e.getMessage(), e);
    RedisManager.releaseShardeJedis(jedis);
} finally {
    RedisManager.returnShardedJedisResource(jedis);
}
但要注意的是,a)避免多次不必要的创建、释放资源操作(如果redis资源会一直被使用,不用每次都释放资源然后再申请)
b)用完千万不要忘记释放redis,否则会outOfMemory.

5)写jedis时需要考虑的性能问题?
jedis操作时,由于存储redis是要经过网络的(存储到专门的redis服务器),所以其性能不如直接写入内存快。
当我们操作缓存时,同时操作Redis,在大数据高并发情况下,redis操作会造成性能瓶颈,给用户造成延时甚至超时请求失败。
基于这样的考虑,我做了如下优化:
采用异步方式:对redis存的时候(因为存时不严格要求数据实时),取的时候要求实时,就不适合异步模型了。
为redis写了个生产者/消费者异步模型,添加了阻塞队列:
private static LinkedBlockingQueue<ArrayList<byte[]>> setBytesQueue = new LinkedBlockingQueue<ArrayList<byte[]>>(1024*256);
作为成员变量,当操作redis时(这里主要是存),直接存入该缓存,相当于生产者。

那么,怎样写消费者呢? 在类的开头static代码块里面

new Thread(new Runnable() {
    @Override
    public void run() {
        while (true) {
        try {
            String key = delByStrKeyQueue.take();
            //通过key执行Redis操作
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
            try {
                Thread.sleep(200);
            } catch (InterruptedException e1) {
                logger.error(e.getMessage(), e1);
            }
        }
    }
}).start();

这样就可以异步消费了。

这里面有一个技巧:使用了LinkedBlockingQueue的take方法,而不是poll. 这是因为,redis中的数据(可能是删除掉的聊天室),不是每时每刻queue中都有数据的,所以用take方法做(当没有值时,线程阻塞等待),而不是用poll一直去queue中取数据。这样节约了CUP资源。

这样够优化了吗?如果当queue中的数据非常多需要消费时,我们必须要等待redis执行完毕,再去消费另一条,而redis操作是需要通过网络的(传输到redis服务器),所以解决此问题的方法:redis操作使用线程池做异步操作:每次对Redis操作时,只把这个任务存入线程池(此步骤为内存操作),然后就可以继续消费queue中的下一条数据了。然后线程池异步消费每一个线程。

注意初始化时线程池的大小,我们通常这样做:
public static ExecutorService pool = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2);

然后为每一个需要消费的队列专门写一个内部线程类,如:
public static class SetBytesQueueTask implements Runnable {
    private ArrayList<byte[]> list;

    public SetBytesQueueTask(ArrayList<byte[]> list) {
        this.list = list;
    }

    @Override
    public void run() {
        if (list != null && list.size() == 3) {
            byte[] key1 = list.get(0);
            byte[] key2 = list.get(1);
            byte[] value = list.get(2);
            if (key1 != null && key2 != null && value != null) {
                ShardedJedis jedis = RedisManager.getShardedJedis();
                try {
                    jedis.hset(key1, key2, value);
                } catch (Exception e) {
                    logger.error("Error happened when consuming SetBytesQueueTask in RedisUtil", e);
                    RedisManager.releaseShardeJedis(jedis);
                } finally {
                    RedisManager.returnShardedJedisResource(jedis);
                }
            } else {
                logger.error("Key has null value in SetBytesQueueTask");
            }
        } else {
            logger.error("SetBytesQueueTask element's size should be 3, but now it is {}, skip...", list.size());
        }
    }
}

然后这样消费线程:
new Thread(new Runnable() {
    @Override
    public void run() {
        while (true) {
            try {
                String key = delByStrKeyQueue.take();
                pool.submit(new DelByStrKeyQueueTask(key));
            } catch (Exception e) {
                logger.error(e.getMessage(), e);
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e1) {
                    logger.error(e.getMessage(), e1);
                }
            }
        }
    }
}).start();










猜你喜欢

转载自doudou-001.iteye.com/blog/2284747