源码解析:探究JedisPool与CommonRedis的性能差异

实习的第一个项目是实现一个秒杀系统,一开始没用工程中已封装好的CommonRedis,直接手撸了一个原生jedis实现,后期优化时改成了通过CommonRedis实现,结果QPS反而下降了,遂进行源码分析探究性能下降原因

测试结果

利用ab进行并发测试,均为10w请求,分为1000并发场景和100并发场景,每个场景测试两次取平均值,涉及四个redis操作,hget,lpop,lpush,hsetnx
测试版本 git log : 655791b2b78c26997ee3caa2f7eb0645288c0d79
第一次测试使用的db为1号db,QPS结果如下:

1号DB-c 1000-c 100
jedis4021.274610.01
redisson3703.794415.19
commonRedis3248.283813.51

后将此结果给导师看时,被告知select db处可能也有影响,又采用默认的0号db进行测试,如下:

0号DB-c 1000性能增幅-c 100性能增幅
jedis4031.130.24%4646.980.80%
redisson3804.072.70%4786.898.41%
commonRedis3704.6914.05%4487.5617.67%

明显可见去掉切换db操作后,commonRedis的性能增幅远大于其余二者。

源码分析

一.jedisPool
  1. 调用代码,以lpush操作为例:
    public long lpush(String key, String strings) {
        Jedis jedis = jedisPool.getResource();
        long result = jedis.lpush(key, strings);
        jedis.close();
        return result;
    }
  1. 首先从pool中获取一个连接的对象,实际上是Pool内部的internalPool调用borrowObject()拿到一个实例,调用时会将设置pool中的maxWaitMillis(即获取Jedis实例最大等待wait时间)作为参数传入
  public T getResource() {
    try {
      return internalPool.borrowObject();
    } catch (NoSuchElementException nse) {
      throw new JedisException("Could not get a resource from the pool", nse);
    } catch (Exception e) {
      throw new JedisConnectionException("Could not get a resource from the pool", e);
    }
  }
    /**
     * Equivalent to <code>{@link #borrowObject(long)
     * borrowObject}({@link #getMaxWaitMillis()})</code>.
     * <p>
     * {@inheritDoc}
     */
    @Override
    public T borrowObject() throws Exception {
        return borrowObject(getMaxWaitMillis());
    }

在borrowObject时,当无空闲的连接实例时会根据BlockWhenExhausted中的策略进行处理

    public T borrowObject(long borrowMaxWaitMillis) throws Exception {
        assertOpen();

        AbandonedConfig ac = this.abandonedConfig;
        if (ac != null && ac.getRemoveAbandonedOnBorrow() &&
                (getNumIdle() < 2) &&
                (getNumActive() > getMaxTotal() - 3) ) {
            removeAbandoned(ac);
        }

        PooledObject<T> p = null;

        // Get local copy of current config so it is consistent for entire
        // method execution
        boolean blockWhenExhausted = getBlockWhenExhausted();

        boolean create;
        long waitTime = System.currentTimeMillis();

        while (p == null) {
            create = false;
            if (blockWhenExhausted) { //确认当实例用完的策略是阻塞还是非阻塞,此处为阻塞的策略
                p = idleObjects.pollFirst();  //pollFirst中有锁,因此其实线程安全的
                if (p == null) {
                    p = create();
                    if (p != null) {
                        create = true;
                    }
                }
                if (p == null) { 
                    if (borrowMaxWaitMillis < 0) { //默认值为-1,表示一直阻塞等待,直到获取对象
                        p = idleObjects.takeFirst();
                    } else {
                        p = idleObjects.pollFirst(borrowMaxWaitMillis,
                                TimeUnit.MILLISECONDS); //根据设置的maxWait超时抛出异常
                    }
                }
                if (p == null) {
                    throw new NoSuchElementException(
                            "Timeout waiting for idle object");
                }
                if (!p.allocate()) {
                    p = null;
                }
            } else { //未采用阻塞的方式
                p = idleObjects.pollFirst();
                if (p == null) {
                    p = create(); //为空则创建新的实例
                    if (p != null) {
                        create = true;
                    }
                }
                if (p == null) {
                    throw new NoSuchElementException("Pool exhausted");
                }
                if (!p.allocate()) {
                    p = null;
                }
            }

            if (p != null) { //不为空则将此实例激活,根据配置TestOnBorrow,TestOnCreate()决定是否进行可用性检查
                try {
                    factory.activateObject(p); //激活时会再次确认所处的db是否与要求相同
                } catch (Exception e) {
                    try {
                        destroy(p);
                    } catch (Exception e1) {
                        // Ignore - activation failure is more important
                    }
                    p = null;
                    if (create) {
                        NoSuchElementException nsee = new NoSuchElementException(
                                "Unable to activate object");
                        nsee.initCause(e);
                        throw nsee;
                    }
                }
                if (p != null && (getTestOnBorrow() || create && getTestOnCreate())) {
                    boolean validate = false;
                    Throwable validationThrowable = null;
                    try {
                        validate = factory.validateObject(p);
                    } catch (Throwable t) {
                        PoolUtils.checkRethrow(t);
                        validationThrowable = t;
                    }
                    if (!validate) {
                        try {
                            destroy(p);
                            destroyedByBorrowValidationCount.incrementAndGet();
                        } catch (Exception e) {
                            // Ignore - validation failure is more important
                        }
                        p = null;
                        if (create) {
                            NoSuchElementException nsee = new NoSuchElementException(
                                    "Unable to validate object");
                            nsee.initCause(validationThrowable);
                            throw nsee;
                        }
                    }
                }
            }
        }

        updateStatsBorrow(p, System.currentTimeMillis() - waitTime);

        return p.getObject();
    }

(在Pool中资源不够的时候)通过create创建实例,会调用factory.makeObject()方法

  @Override
  public PooledObject<Jedis> makeObject() throws Exception {
    final HostAndPort hostAndPort = this.hostAndPort.get();
    final Jedis jedis = new Jedis(hostAndPort.getHost(), hostAndPort.getPort(), connectionTimeout,
        soTimeout, ssl, sslSocketFactory, sslParameters, hostnameVerifier);

    try {
      jedis.connect(); //此处即创建socket通信,和Redis Sever通过Socket通信
      if (null != this.password) {
        jedis.auth(this.password);
      }
      if (database != 0) { //创建的默认db是0号,若在配置中更改了db,这边会发生一次select操作
        jedis.select(database);
      }
      if (clientName != null) {
        jedis.clientSetname(clientName);
      }
    } catch (JedisException je) {
      jedis.close();
      throw je;
    }

    return new DefaultPooledObject<Jedis>(jedis);

  }

在jedis的最底层Connection 中维护了一个底层Socket连接;
RedisOutputStream与RedisInputStream,I/O Stream是在Connection中Socket建立连接后获取并在使用时传给Protocol的
在这里插入图片描述
可以看到,Jedis与Redis之间的通信就是使用一个基本的Socket。

  1. 对取得的连接实例进行操作(此处是lpush)
  2. 归还连接时会检查其是否破损
    在这里插入图片描述
    @Override
    public void returnObject(T obj) {
        PooledObject<T> p = allObjects.get(new IdentityWrapper<T>(obj));
        
        if (p == null) {
            if (!isAbandonedConfig()) {
                throw new IllegalStateException(
                        "Returned object not currently part of this pool");
            } else {
                return; // Object was abandoned and removed
            }
        }

        synchronized(p) {
            final PooledObjectState state = p.getState();
            if (state != PooledObjectState.ALLOCATED) {
                throw new IllegalStateException(
                        "Object has already been returned to this pool or is invalid");
            } else {
                p.markReturning(); //将状态更新为成功归还 Keep from being marked abandoned
            }
        }

        long activeTime = p.getActiveTimeMillis();

        if (getTestOnReturn()) { //若配置中testOnReturn为true则在每次归还时会进行检查,销毁破损的实例
            if (!factory.validateObject(p)) {
                try {
                    destroy(p);
                } catch (Exception e) {
                    swallowException(e);
                }
                try {
                    ensureIdle(1, false);
                } catch (Exception e) {
                    swallowException(e);
                }
                updateStatsReturn(activeTime);
                return;
            }
        }

        try {
            factory.passivateObject(p); // 钝化池对象,未实现,这里没有把实例的db改回0
        } catch (Exception e1) {
            swallowException(e1);
            try {
                destroy(p);
            } catch (Exception e) {
                swallowException(e);
            }
            try {
                ensureIdle(1, false);
            } catch (Exception e) {
                swallowException(e);
            }
            updateStatsReturn(activeTime);
            return;
        }

        if (!p.deallocate()) {
            throw new IllegalStateException(
                    "Object has already been returned to this pool or is invalid");
        }

        int maxIdleSave = getMaxIdle();
        if (isClosed() || maxIdleSave > -1 && maxIdleSave <= idleObjects.size()) { 
        //若池中空闲实例数已大于设定的maxIdle则将该实例销毁
            try {
                destroy(p);
            } catch (Exception e) {
                swallowException(e);
            }
        } else {
            if (getLifo()) {
                idleObjects.addFirst(p);
            } else {
                idleObjects.addLast(p);
            }
            if (isClosed()) {
                // Pool closed while object was being added to idle objects.
                // Make sure the returned object is destroyed rather than left
                // in the idle object pool (which would effectively be a leak)
                clear();
            }
        }
        updateStatsReturn(activeTime);
    }

比较有意思的是在passivateObject方法中作者留了个TODO,并未将db设置回默认值0
在这里插入图片描述

二.commonRedis

再来看工程中的commonRedis源码,其底层也是基于jedisPool实现的,只是在表面做了封装,更好的适应复杂对象在redis中的存储。

  1. 用过redisson的都很熟悉下面的这种写法,直接将key绑定到对应的数据结构上
    在这里插入图片描述

这样写的好处是相当于做了分类,相关的逻辑只在这个分类中处理,而不是在全局,操作更简单方便,更好的贯彻了面向对象的思这样写的好处是相当于做了分类,相关的逻辑只在这个分类中处理,而不是在全局,操作更简单方便,更好的贯彻了面向对象的思想

  1. 继续看leftPush方法,先将key和value都通过序列化方法转化成Byte字节流(一开始我以为这部分影响性能比较严重,后来和导师确认了其实并不是,只是最简单的序列化方法)

在这里插入图片描述
在这里插入图片描述

  1. 后面的过程同Jedis类似(因其底层是基于Jedis实现),不同的地方在于最后归还实例时,其会将db置为默认的0号,也就是说当初始的db不是0号时,commonRedis每次(无论是创建实例还是从pool中获取)都会有两次的select db操作,borrow时select至目标db,return时会再select回去0号,而原生Jedis只会在新建client对象时进行一次select操作,同时从pool中获取已存在对象时也不会再进行select操作,至此成功找到了最大的性能差距。

推荐阅读:
Jedis源码解析(一)-------Jedis与JedisPool
jedis 源码阅读二——jedisPool

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值