Redis初探

一、什么是Redis

Redis是一个开源的使用ANSI C语言编写、遵守BSD协议、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。

1 Redis特点

  Redis本质上是一个Key-Value类型的内存数据库,很像memcached,整个数据库统统加载在内存当中进行操作,定期通过异步操作把数据库数据flush到硬盘上进行保存。因为是纯内存操作,Redis的性能非常出色,每秒可以处理超过 10万次读写操作,是已知性能最快的Key-Value DB。而且Redis支持五大数据结构string,list,set,sorted set(zset),hash ,对单个Value大小的限制是1GB。

  虽然Redis是Nosql数据库的一种,但他有着其他Nosql数据库不具备的优点:Redis是支持事务(ACID)的,Redis的操作都是原子性的,而且支持回滚,且Redis可以定期持久化到本地硬盘中。

Redis可用于缓存,消息,按key设置过期时间,过期后将会自动删除

  与Memcached相比,Redis不仅能在内存中读写,同时还能把内存中的数据持久化到硬盘中,这样就可以在一定程度上避免断电后内存数据消失的问题。

  Redis的主要缺点是数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此Redis适合的场景主要局限在较小数据量的高性能操作和运算上。

2 Redis应用场景

  由于Redis有着速度快,支持数据结构多等特点,他在工业界的主要应用场景:缓存;轻量级消息队列(用List来做FIFO双向链表);处理数据过期(精确到毫秒):对存入的Key-Value设置expire时间,因此也可以被当作一个功能加强版的memcached来用。

 

二、Redis使用

1 序列化

  序列化 (Serialization)是将对象的状态信息转换为可以存储或传输的形式的过程。在序列化期间,对象将其当前状态写入到临时或持久性存储区。以后,可以通过从存储区中读取或反序列化对象的状态,重新创建该对象。

网络传输中和本地存储中,Java对象并不是平时在Java堆中的数据结构,不序列化的Java对象很难跨平台传输,安全性也无法保证。

序列化:把Java对象转换为字节序列的过程。

  反序列化:把字节序列恢复为Java对象的过程。

2 Protostuff序列化

  网络中传输的和硬盘中存储的都是序列化后的数据(二进制流)所以要对存入的Redis数据进行序列化,从Redis取出的数据要做反序列化。当两个进程在进行远程通信时,彼此可以发送各种类型的数据。无论是何种类型的数据,都会以二进制序列的形式在网络上传送。发送方需要把这个Java对象转换为字节序列,才能在网络上传送;接收方则需要把字节序列再恢复为Java对象。

  Jedis是首选的Java客户端开发包,但与其他数据库不同的是,Jedis和Redis内部并没有实现序列化操作,需要程序员自己实现序列化操作。这里比较初级的做法是在需要序列化的类实现java.io.Serializable接口。

  在众多的序列化第三方库中,谷歌开发的Protostuff是目前效率(时间、空间)最高的一种方式。

  Protostuff使用demo

    public Seckill getSeckill(long seckillId) {
        //redis操作逻辑
        try {
            Jedis jedis = jedisPool.getResource();
            try {
                String key = "seckill:" + seckillId;
                //并没有实现内部序列化操作
                // get-> byte[] -> 反序列化 ->Object(Seckill)
                // 采用自定义序列化
                //protostuff : pojo.
                byte[] bytes = jedis.get(key.getBytes());
                //缓存中获取到bytes
                if (bytes != null) {
                    //空对象
                    Seckill seckill = schema.newMessage();
                    ProtostuffIOUtil.mergeFrom(bytes, seckill, schema);
                    //seckill 被反序列化
                    return seckill;
                }
            } finally {
                jedis.close();
            }
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        }
        return null;
    }

    public String putSeckill(Seckill seckill) {
        // set Object(Seckill) -> 序列化 -> byte[]
        try {
            Jedis jedis = jedisPool.getResource();
            try {
                String key = "seckill:" + seckill.getSeckillId();
                byte[] bytes = ProtostuffIOUtil.toByteArray(seckill, schema,
                        LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE));
                //超时缓存
                int timeout = 60 * 60;//1小时
                String result = jedis.setex(key.getBytes(), timeout, bytes);
                return result;
            } finally {
                jedis.close();
            }
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        }

        return null;
    }

 

三、Redis持久化

Redis相比其他内存型缓存数据库还有一个显著性的优势,就是支持持久化。Redis提供两种持久化方式,一种是RDB持久化(原理是将Reids在内存中的数据库记录定时dump到磁盘上的RDB持久化),另外一种是AOF(append only file)持久化(原理是将Reids的操作日志以追加的方式写入文件)。

RDB持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘,实际操作过程是fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。

 

AOF持久化以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录。服务器每次启动都会读取日志记录,在内存中形成记录。

1 RDB持久化的优缺点

优点:

1). 一旦采用该方式,那么你的整个Redis数据库将只包含一个文件,这对于文件备份而言是非常完美的。比如,你可能打算每个小时归档一次最近24小时的数据,同时还要每天归档一次最近30天的数据。通过这样的备份策略,一旦系统出现灾难性故障,我们可以非常容易的进行恢复。

2). 对于灾难恢复而言,RDB是非常不错的选择。因为我们可以非常轻松的将一个单独的文件压缩后再转移到其它存储介质上。

3). 性能最大化。对于Redis的服务进程而言,在开始持久化时,它唯一需要做的只是fork出子进程,之后再由子进程完成这些持久化的工作,这样就可以极大的避免服务进程执行IO操作了。

4). 相比于AOF机制,如果数据集很大,RDB的启动效率会更高。

缺点:

1). 毕竟存在时间间隔,不能保证百分百的灾备。如果你想保证数据的高可用性,即最大限度的避免数据丢失,那么RDB将不是一个很好的选择。因为系统一旦在定时持久化之前出现宕机现象,此前没有来得及写入磁盘的数据都将丢失。

2). 由于RDB是通过fork子进程来协助完成数据持久化工作的,因此,如果当数据集较大时,可能会导致整个服务器停止服务几百毫秒,甚至是1秒钟。

2 AOF持久化的优缺点

优点:

1). 该机制可以带来更高的数据安全性,即数据持久性。Redis中提供了3中同步策略,即每秒同步、每修改同步和不同步。事实上,每秒同步也是异步完成的,其效率也是非常高的,所差的是一旦系统出现宕机现象,那么这一秒钟之内修改的数据将会丢失。而每修改同步,我们可以将其视为同步持久化,即每次发生的数据变化都会被立即记录到磁盘中。可以预见,这种方式在效率上是最低的。至于无同步,无需多言,我想大家都能正确的理解它。

2). 由于该机制对日志文件的写入操作采用的是append模式,因此在写入过程中即使出现宕机现象,也不会破坏日志文件中已经存在的内容。然而如果我们本次操作只是写入了一半数据就出现了系统崩溃问题,不用担心,在Redis下一次启动之前,我们可以通过redis-check-aof工具来帮助我们解决数据一致性的问题。

3). 如果日志过大,Redis可以自动启用rewrite机制。即Redis以append模式不断的将修改数据写入到老的磁盘文件中,同时Redis还会创建一个新的文件用于记录此期间有哪些修改命令被执行。因此在进行rewrite切换时可以更好的保证数据安全性。

4). AOF包含一个格式清晰、易于理解的日志文件用于记录所有的修改操作。事实上,我们也可以通过该文件完成数据的重建。

缺点:

1). 对于相同数量的数据集而言,AOF文件通常要大于RDB文件。RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。

2). 根据同步策略的不能,安全性能的提高,AOF在运行效率上往往会慢于RDB。总之,每秒同步策略的效率是比较高的,同步禁用策略的效率和RDB一样高效。

二者选择的标准,就是看系统是愿意牺牲一些性能,换取更高的缓存一致性(aof),还是愿意写操作频繁的时候,不启用备份来换取更高的性能,待手动运行save的时候,再做备份(rdb)

3 无持久化

当然,Redis也可以关闭持久化功能,仅把Redis当做缓存来使用。

 

四、常见问题

1 Redis作为缓存时与DB的数据一致性维护

这个问题可深可浅,上升到Redis集群后,解决方案会更复杂,这里只先初步的分析一种简单的情况。

我们在使用redis过程中,或者网上一些资料,通常会这样做:先读取缓存,如果缓存不存在,则读取数据库。伪代码如下

    Object stuObj = new Object();

    public Stu getStuFromCache(String key){
        Stu stu = (Stu) redis.get(key);
        if(stu == null){
            synchronized (stuObj) {
                stu = (Stu) redis.get(key);
                if(stu == null){
                    Stu stuDb = db.query();
                    redis.set(key, stuDb);
                }
            }
        }

        return stu;
    }

上面加锁是为了防止过多的查询走到数据库层

写数据库伪代码:

public void setStu(){
    redis.del(key);
    db.write(obj);
}

不管是先写库,再删除缓存;还是先删缓存,再写库,都有可能出现数据不一致的情况
因为写和读是并发的,没法保证顺序,如果删了缓存,还没有来得及写库,另一个线程就来读取,发现缓存为空,则去数据库中读取数据写入缓存,此时缓存中为脏数据。如果先写了库,再删除缓存前,写库的线程宕机了,没有删除掉缓存,则也会出现数据不一致情况。

如果是redis集群,或者主从模式,写主读从,由于redis复制存在一定的时间延迟,也有可能导致数据不一致。

这里可以采用双删+超时的方式来解决问题

在写库前后都进行redis.del(key)操作,并且设定合理的超时时间。这样最差的情况是在超时时间内存在不一致,当然这种情况极其少见,可能的原因就是服务宕机。此种情况可以满足绝大多数需求。 

当然这种策略要考虑redis和数据库主从同步的耗时,所以在第二次删除前最好休眠一定时间,比如500毫秒,这样毫无疑问又增加了写请求的耗时。

2 缓存穿透

什么是缓存穿透?

一般的缓存系统,都是按照key去缓存查询,如果不存在对应的value,就应该去后端系统查找(比如DB)。如果key对应的value是一定不存在的,并且对该key并发请求量很大,就会对后端系统造成很大的压力。这就叫做缓存穿透。

如何避免?

1. 对查询结果为空的情况也进行缓存,缓存时间设置短一点,或者该key对应的数据insert了之后清理缓存。

2. 对一定不存在的key进行过滤。可以把所有的可能存在的key放到一个大的Bitmap中,查询时通过该bitmap过滤。

3 缓存雪崩

什么是缓存雪崩?

当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,也会给后端系统(比如DB)带来很大压力。

如何避免?

1.在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。

2.不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。

3.做二级缓存,A1为原始缓存,A2为拷贝缓存,A1失效时,可以访问A2,A1缓存失效时间设置为短期,A2设置为长期(此点为补充)

4 用Redis和任意语言实现一段恶意登录保护的代码,限制1小时内每用户Id最多只能登录5次

  用队列数据结构来实现,key为用户,value为登录时间,每条记录的过期时间设为1个小时,且维护队列数据长度为5。当用户发起新的登录请求且当前队列的长度已经为5时,拒绝请求。

 

 

参考链接

https://www.cnblogs.com/fidelQuan/p/4543387.html

https://www.cnblogs.com/chenliangcl/p/7240350.html

https://blog.csdn.net/liubenlong007/article/details/53690312?winzoom=1

http://www.cnblogs.com/Survivalist/p/8119891.html

posted @ 2018-10-29 13:17  Allegro  阅读(237)  评论(0编辑  收藏  举报