Redis实战篇(四)

1 Redis客户端

官网推荐的Java客户端有3个:Jedis,Lettuce和Redisson

配置 作用
Jedis A blazingly small and sane redis java client(体积非常小,但功能很完善)
lettuce Advanced Redis client for thread-safe sync, async, and reactive usage. Supports Cluster, Sentinel, Pipelining, and codecs(高级客户端,支持线程安全、异步、反应式编程、集群、哨兵、pipeline、编解码).
Redisson distributed and scalable Java data structures on top of Redis server (基于Redis服务实现的Java高级可拓展的数据结构)

在SpringBoot 2.x之前,RedisTemplate默认使用Jedis,2.x之后默认使用lettuce。

1.1 Jedis

Jedis是我们最熟悉和最常用的客户端。如果不使用RedisTemplate,就可以直接创建Jedis的连接。

public static void main(String[] args) {
	Jedis jedis = new Jedis("192.168.44.181", 6379);
	jedis.set("qingshan", "2673jedis");
	System.out.println(jedis.get("qingshan"));
	jedis.close();
}

Jedis有一个问题:多个线程使用一个连接的时候线程不安全。
下面也提供了解决思路:使用连接池,为每个连接创建不同的连接,基于Apache common pool实现。
Jedis的连接池有三个实现:JedisPool、ShardingJedisPool、JedisSentinelPool,都是用getResource从连接池获取一个连接。

	/**
     * 普通连接池
     */
    public static void ordinaryPool(){
        JedisPool pool = new JedisPool("192.168.44.181",6379);
        Jedis jedis = pool.getResource();
        jedis.set("qingshan","平平无奇盆鱼宴");
        System.out.println(jedis.get("qingshan"));
    }

    /**
     * 分片连接池
     */
    public static void shardedPool() {
        JedisPoolConfig poolConfig = new JedisPoolConfig();

        // Redis服务器
        JedisShardInfo shardInfo1 = new JedisShardInfo("192.168.44.181", 6379);

        // 连接池
        List<JedisShardInfo> infoList = Arrays.asList(shardInfo1);
        ShardedJedisPool jedisPool = new ShardedJedisPool(poolConfig, infoList);

        ShardedJedis jedis = jedisPool.getResource();
        jedis.set("qingshan","分片测试");
        System.out.println(jedis.get("qingshan"));
    }

    /**
     * 哨兵连接池
     */
    public static void sentinelPool() {
        String masterName = "redis-master";
        Set<String> sentinels = new HashSet<String>();
        sentinels.add("192.168.44.186:26379");
        sentinels.add("192.168.44.187:26379");
        sentinels.add("192.168.44.188:26379");

        JedisSentinelPool pool = new JedisSentinelPool(masterName, sentinels);
        pool.getResource().set("qingshan", "哨兵" + System.currentTimeMillis() + "盆鱼宴");
        System.out.println(pool.getResource().get("qingshan"));
    }

Jedis的功能比较完善,Redis官方的特性全部支持,比如发布订阅、事务、lua脚本、客户端分片、哨兵、集群、pipeline等等。

Sentinel和Cluster的功能上一章节已分析过。Jedis连接Sentinel需要配置所有的哨兵地址。Jedis连接Cluster只需要配置任何一个master或者slave的地址就可以了。

1.1.1 Sentinel获取连接原理

在构造方法中

//必须要指定masterName
JedisSentinelPool pool = new JedisSentinelPool(masterName, sentinels);
HostAndPort master = this.initSentinels(sentinels, masterName);
private HostAndPort initSentinels(Set<String> sentinels, final String masterName) {

    HostAndPort master = null;
    boolean sentinelAvailable = false;

    log.info("Trying to find master from available Sentinels...");
    // 有多个sentinel,遍历这些个sentinel
    for (String sentinel : sentinels) {
      //host:port表示的sentinel地址转化为一个HostAndPort对象
      final HostAndPort hap = HostAndPort.parseString(sentinel);

      log.fine("Connecting to Sentinel " + hap);

      Jedis jedis = null;
      try {
       // 连接到sentinel
        jedis = new Jedis(hap.getHost(), hap.getPort());
        // 根据masterName 得到master的地址,返回一个list,host=list[0],port=list[1]
        List<String> masterAddr = jedis.sentinelGetMasterAddrByName(masterName);

        // connected to sentinel...
        sentinelAvailable = true;

        if (masterAddr == null || masterAddr.size() != 2) {
          log.warning("Can not get master addr, master name: " + masterName + ". Sentinel: " + hap
              + ".");
          continue;
        }

        master = toHostAndPort(masterAddr);
        log.fine("Found Redis master at " + master);
        break;
      } catch (JedisException e) {
        // resolves #1036, it should handle JedisException there's another chance
        // of raising JedisDataException
        log.warning("Cannot get master address from sentinel running @ " + hap + ". Reason: " + e
            + ". Trying next one.");
      } finally {
        if (jedis != null) {
          jedis.close();
        }
      }
    }

    if (master == null) {
      if (sentinelAvailable) {
        // can connect to sentinel, but master name seems to not
        // monitored
        throw new JedisException("Can connect to sentinel, but " + masterName
            + " seems to be not monitored...");
      } else {
        throw new JedisConnectionException("All sentinels down, cannot determine where is "
            + masterName + " master is running...");
      }
    }

    log.info("Redis master running at " + master + ", starting Sentinel listeners...");

	// 启动每一个sentinel的监听,MasterListener 本质为一个线程,它会订阅sentinel上关于master节点地址改变的消息
    for (String sentinel : sentinels) {
      final HostAndPort hap = HostAndPort.parseString(sentinel);
      MasterListener masterListener = new MasterListener(masterName, hap.getHost(), hap.getPort());
      // whether MasterListener threads are alive or not, process can be stopped
      masterListener.setDaemon(true);
      masterListeners.add(masterListener);
      masterListener.start();
    }

    return master;
  }

1.1.2 Cluster原理

使用jedis连接Cluster的时候,我们只需要连接任意一个或者多个redis group中的实例地址,那我们是怎么获取到需要操作的Redis Master实例呢?

为了表面set、get的时候发生重定向错误,我们需要把slot和redis节点的关系保存起来,在本地计算slot,就可以获得Redis节点信息。

在这里插入图片描述
问题:如何存储slot和redis连接池的关系

public static void main(String[] args) throws IOException {
    // 不管是连主备,还是连几台机器都是一样的效果
	/*        
	HostAndPort hp1 = new HostAndPort("192.168.44.181",7291);
    HostAndPort hp2 = new HostAndPort("192.168.44.181",7292);
    HostAndPort hp3 = new HostAndPort("192.168.44.181",7293);
    */
    HostAndPort hp4 = new HostAndPort("192.168.44.181",7294);
    HostAndPort hp5 = new HostAndPort("192.168.44.181",7295);
    HostAndPort hp6 = new HostAndPort("192.168.44.181",7296);

    Set nodes = new HashSet<HostAndPort>();
	/*        
	nodes.add(hp1);
    nodes.add(hp2);
    nodes.add(hp3);
    */
    nodes.add(hp4);
    nodes.add(hp5);
    nodes.add(hp6);

    JedisCluster cluster = new JedisCluster(nodes);
    cluster.set("gupao:cluster", "qingshan2673--------------");
    System.out.println(cluster.get("gupao:cluster"));;
    cluster.close();
}
private void initializeSlotsCache(Set<HostAndPort> startNodes, GenericObjectPoolConfig poolConfig, String password) {
	for (HostAndPort hostAndPort : startNodes) {
	   //获取一个jedis实例
	  Jedis jedis = new Jedis(hostAndPort.getHost(), hostAndPort.getPort());
	  if (password != null) {
	    jedis.auth(password);
	  }
	  try {
	     //获取redis节点和slot虚拟槽
	    cache.discoverClusterNodesAndSlots(jedis);
	    //直接跳出循环
	    break;
	  } catch (JedisConnectionException e) {
	    // try next nodes
	  } finally {
	    if (jedis != null) {
	      jedis.close();
	    }
	  }
	}
}
public void discoverClusterNodesAndSlots(Jedis jedis) {
  w.lock();

  try {
    reset();
    //获取节点集合
    List<Object> slots = jedis.clusterSlots();
     //遍历三个主节点
    for (Object slotInfoObj : slots) {
      //slotinfo 槽开始,槽结束,主,从
      //{[0,5460,7291,7294],[5461,10922,7292,7295],[10923,16383,7293,7296]}
      List<Object> slotInfo = (List<Object>) slotInfoObj;
      // 如果size<2,代表没有分配slot
      if (slotInfo.size() <= MASTER_NODE_INDEX) {
        continue;
      }
	  //获取分配到当前master节点的数据槽,例如7291节点的{0,1,2,3.......5460}
      List<Integer> slotNums = getAssignedSlotArray(slotInfo);

      // hostInfos
      int size = slotInfo.size(); //size=4,槽开始,槽结束,主,从
      for (int i = MASTER_NODE_INDEX; i < size; i++) {
         //第3位和第4位是主从端口的信息
        List<Object> hostInfos = (List<Object>) slotInfo.get(i);
        if (hostInfos.size() <= 0) {
          continue;
        }
         //根据ip 和端口生成HostAndPort 实例
        HostAndPort targetNode = generateHostAndPort(hostInfos);
        setupNodeIfNotExist(targetNode);
        if (i == MASTER_NODE_INDEX) {
           //把slot和jedisPool缓存起来(16384个),key是slot下标,value是连接池
          assignSlotsToNode(slotNums, targetNode);
        }
      }
    }
  } finally {
    w.unlock();
  }
}

获取slot和redis实例对应关系之后,接下来就是从集群环境中存取值。

Jedis集群模式下所有的命令都要调用这个方法:JedisClusterCommand#runWithRetries

connection = this.connectionHandler.getConnectionFromSlot(JedisClusterCRC16.getSlot(key));

步骤如下:

  1. 把key作为参数,执行CRC16算法,获取key所在的slot值
  2. 通过该slot值,去slot对应的map集合中获取jedisPool实例
  3. 通过jedisPool实例获取jedis实例,最终完成redis数据存取操作。

1.1.3 Jedis实现分布式锁

分布式锁的基本需求:

    • 0
      点赞
    • 0
      收藏
      觉得还不错? 一键收藏
    • 1
      评论
    评论 1
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

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

    抵扣说明:

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

    余额充值