redis机制总结


  • 事务
  • pipeline
  • 持久化
  • 主从复制
  • 虚拟内存
  • pub/sub
  • 分布式
  • 并发

事务

  • muliti->…->exec
  • muliti->…->discard
  • 如果事务中的一个命令失败了,并不回滚其他命令
  • 事务中的写操作不能依赖事务中的读操作结果

pipeline

  • 从client 打包多条命令一起发出,不需要等待单条命令的响应返回,而 redis 服务端会处理完多条命令后会将多条命令的处理结果打包到一起返回给客户端。
  • 可以节省很多原来浪费在网络延迟的时间。
  • 需要注意用pipeline 方式打包命令发送,redis 必须在处理完所有命令前先缓存起所有命令的处理结果。打包的命令越多,缓存消耗内存也越多。所以并是不是打包的命令越多越好。具体多少合适需要根据具体情况测试。

pipeline示例代码:

package jredisStudy; 
import org.jredis.JRedis;  
import org.jredis.connector.ConnectionSpec;  
import org.jredis.ri.alphazero.JRedisClient; 
import  org.jredis.ri.alphazero.JRedisPipelineService; 
import org.jredis.ri.alphazero.connection.DefaultConnectionSpec;  
public class PipeLineTest {  
     public static void main(String[] args) { 
             long start = System.currentTimeMillis(); 
             usePipeline(); 
             long end = System.currentTimeMillis();  
             System.out.println(end-start); 
             start =    System.currentTimeMillis(); 
             withoutPipeline(); 
             end   = System.currentTimeMillis(); 
             S ystem.out.println(end -start); 
     } 
     private static void withoutPipeline() 
     { 
           try {   
                JRedis    jredis = new JRedisClient("192.168.56.55",6379);  
                    for(int i =0 ; i < 100000 ; i++)  
                    { 
                         jredis.incr("test2");  
                    } 
                    jredis.quit();  
          } catch (Exception e) { 
          } 
     } 
    private static void usePipeline() { 
              try {  
                   ConnectionSpec spec = DefaultConnectionSpec.newSpec("192.168.56.55", 6379, 0, null);  
                   JRedis jredis = new JRedisPipelineService(spec);  
                   for(int i =0 ; i < 100000 ; i++)  
                   { 
                        jredis.incr("test2");  
                   } 
                   jredis.quit();  
              } catch (Exception e) { 
              } 
     } 
}

持久化

Snapshotting(快照)

  • 快照是默认的持久化方式。这种方式是就是将内存中数据快照的方式写入到二进制文件中,默认的文件名为dump.rdb
  • save 或者bgsave 命令通知redis 做一次快照持久化
  • 可配置

Append-only file(缩写aof)

  • aof 比快照方式有更好的持久化性,是由于在使用aof 持久化方式时, redis 会将每一个收到的写命令都通过write函数追加到文件中(默认是appendonly.aof) 。当redis 重启时会通过重新执行文件中保存的写命令来在内存中重建整个数据库的内容。
  • redis 提供了bgrewriteaof命令。收到此命令redis 将使用与快照类似的方式将内存中的数据以命令的方式保存到临时文件中,最后替换原来的文件。
  • 可配置

主从复制

  • master 可以有多个slave。
  • 除了多个slave连到相同的master 外,slave也可以连接其他slave形成图状结构。
  • 主从复制不会阻塞master,相反slave在初次同步数据时则会阻塞不能处理client的请求。
  • 主从复制可以用来提高系统的可伸缩性,我们可以用多个slave 专门用于client 的读请求,比如sort 操作可以使用slave来处理,也可以用来做简单的数据冗余(读写分离)。
  • 可以在master 禁用数据持久化,只需要注释掉 master 配置文件中的所有save 配置,然后只在slave上配置数据持久化(提升master性能)

主从复制扩展

  1. 主动复制避开Redis复制缺陷。
    所谓主动复制是指由业务端或者通过代理中间件对Redis存储的数据进行双写或多写,通过数据的多份存储来达到与复制相同的目的(解决被动复制的延迟问题),但也带来了新的问题,就是数据的一致性问题,数据写2次或多次,如何保证多份数据的一致性呢?如果你的应用对数据一致性要求不高,允许最终一致性的话,那么通常简单的解决方案是可以通过时间戳或者vector clock等方式,让客户端同时取到多份数据并进行校验,如果你的应用对数据一致性要求非常高,那么就需要引入一些复杂的一致性算法比如Paxos来保证数据的一致性,但是写入性能也会相应下降很多。)
  2. 通过presharding进行Redis在线扩容。
    Redis的作者提出了一种叫做presharding的方案来解决动态扩容和数据分区的问题,实际就是在同一台机器上部署多个Redis实例的方式,当容量不够时将多个实例拆分到不同的机器上,这样实际就达到了扩容的效果。Redis作者的思路是预先设定Redis instances数量,假设实例数量n,n = 机器数*单台机器redis实例数,后期扩展只需要将旧机器上的部分redis实例迁移到新的机器上,达到平滑扩容。迁移步骤如下:
  • 在新的机器上创建实例,并且每个实例设置为被迁移实例的从机。
  • 主从复制完成之后,设置程序将新的实例作为主,将客户端分片列表旧实例的IP和端口改为新物理机上新实例的IP和端口(解决一致性哈稀分片后同样的key算出来落到跟原来不同的机器上)
  • 停止旧的实例
  • 经过如上步骤之后,旧机器的内存就变大了,最后内存最大为每台机器一个Redis实例。

     按作者文章中所说的,一个机器启动多个实例,其实并不会耗费太多资源,因为Redis够轻量,另外多个实例一个接一个的重写AOF文件或者生成内存快照,可以降低内存的占用,而不影响对外的服务。以上拆分流程是Redis作者提出的一个平滑迁移的过程,不过该拆分方法还是很依赖Redis本身的复制功能的,如果主库快照数据文件过大,这个复制的过程也会很久,同时会给主库带来压力。所以做这个拆分的过程最好选择为业务访问低峰时段进行。

  1. 增量复制改进(参考连接)

虚拟内存

  • 对于redis 这样的内存数据库,内存总是不够用的。除了可以将数据分割到多个redis server外,另外的能够提高数据库容量的办法就是使用vm把那些不经常访问的数据交换的磁盘上
  • redis 的vm 在设计上为了保证key 的查找速度只会将value交换到swap文件中。所以如果是内存问题是由于太多value很小的key 造成的,那么vm 并不能解决。

pub/sub

  • redis 作为一个pub/sub server ,在订阅者和发布者之间起到了消息路由的功能。
  • 订阅者可以通过subscribe 和psubscribe(订阅带通配符的通道)命令向redis server 订阅自己感兴趣的消息类型,redis 将消息类型称为通道(channel) 。
  • 发布者通过 publish 命令向redis server 发送特定类型的消息时,订阅该消息类型的全部client 都会收到此消息。这里消息的传递是多对多的。一个client 可以订阅多个channel,也可以向多个channel发送消息。

分布式

Redis-2.4.15目前没有提供集群的功能,Redis作者在博客中说将在3.0中实现集群机制。目前Redis实现集群的方法主要是采用一致性哈稀分片(Shard),将不同的key分配到不同的redis server上,达到横向扩展的目的。

对于一致性哈稀分片的算法,Jedis-2.0.0已经提供了,下面是使用示例代码(以ShardedJedisPool为例):

package com.jd.redis.client;
import java.util.ArrayList;
import java.util.List;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.JedisShardInfo;
import redis.clients.jedis.ShardedJedis;
import redis.clients.jedis.ShardedJedisPool;
import redis.clients.util.Hashing;
import redis.clients.util.Sharded;
publicclass RedisShardPoolTest {

    static ShardedJedisPoolpool;

    static{
        JedisPoolConfig config =new JedisPoolConfig();//Jedis池配置
        config.setMaxActive(500);//最大活动的对象个数
        config.setMaxIdle(1000 * 60);//对象最大空闲时间
        config.setMaxWait(1000 * 10);//获取对象时最大等待时间
        config.setTestOnBorrow(true);

        String hostA = "10.10.224.44";
        int portA = 6379;
        String hostB = "10.10.224.48";
        int portB = 6379;
        List<JedisShardInfo> jdsInfoList =new ArrayList<JedisShardInfo>(2);
        JedisShardInfo infoA = new JedisShardInfo(hostA, portA);
        infoA.setPassword("redis.360buy");
        JedisShardInfo infoB = new JedisShardInfo(hostB, portB);
        infoB.setPassword("redis.360buy");
        jdsInfoList.add(infoA);

        pool =new ShardedJedisPool(config, jdsInfoList, Hashing.MURMUR_HASH,Sharded.DEFAULT_KEY_TAG_PATTERN);
    }


    publicstaticvoid main(String[] args) {
        for(int i=0; i<100; i++){
            String key = generateKey();
            //key += "{aaa}";
            ShardedJedis jds = null;
            try {
                jds = pool.getResource();
                System.out.println(key+":"+jds.getShard(key).getClient().getHost());
                System.out.println(jds.set(key,"1111111111111111111111111111111"));                
            } catch (Exception e) {
                e.printStackTrace();
            }
            finally{
                pool.returnResource(jds);
            }
        }
    } 

    private static int index = 1;

    publicstatic String generateKey(){
        return String.valueOf(Thread.currentThread().getId())+"_"+(index++);
    }
}

更高效地提高redis client多线程操作的并发吞吐设计(参考链接)

  • Redis是一个非常高效的基于内存的NOSQL数据库,它提供非常高效的数据读写效能.
  • 在实际应用中往往是带宽CLIENT库读写损耗过高导致无法更好地发挥出Redis更出色的能力.

平常操作redis的情况

redis异步更新策略 redis更新机制_数据

上图是反映平常操作redis的情况,每个线程都独立的发起相应连接对redis的网络读写.(单线程可以通过批操作的方式来把当前多个操作合并成一个,而多线程相互隔离).

改进图

redis异步更新策略 redis更新机制_redis_02

  • 多线程操作REDIS,合并网络操作减少操作网络读写,把处理能力提升到最大化。这样Client总体的性能都会有所提升,而REDIS也因表层的网络读取减少而达到更好的利用率.
  • 其实就是把每个请求的操作放到一个队列中,后面开启一个线程来把前面的指令进行一个合并操操作.
  • 虽然在多线程高并发下这样的设计可以把吞吐能力和效能有一个非常不错的效果,但其也存在缺陷:因为每次操作都经过不同线程的处理,如果并发线程不多操作密集度不高,那效果并不理想;因为网络操作密集度不高,可得到并合的数量不多,这方面的损耗有可能低于操作跨线程调度所带来的损耗。