实习的第一个项目是实现一个秒杀系统,一开始没用工程中已封装好的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 |
---|---|---|
jedis | 4021.27 | 4610.01 |
redisson | 3703.79 | 4415.19 |
commonRedis | 3248.28 | 3813.51 |
后将此结果给导师看时,被告知select db处可能也有影响,又采用默认的0号db进行测试,如下:
0号DB | -c 1000 | 性能增幅 | -c 100 | 性能增幅 |
---|---|---|---|---|
jedis | 4031.13 | 0.24% | 4646.98 | 0.80% |
redisson | 3804.07 | 2.70% | 4786.89 | 8.41% |
commonRedis | 3704.69 | 14.05% | 4487.56 | 17.67% |
明显可见去掉切换db操作后,commonRedis的性能增幅远大于其余二者。
源码分析
一.jedisPool
- 调用代码,以lpush操作为例:
public long lpush(String key, String strings) {
Jedis jedis = jedisPool.getResource();
long result = jedis.lpush(key, strings);
jedis.close();
return result;
}
- 首先从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。
- 对取得的连接实例进行操作(此处是lpush)
- 归还连接时会检查其是否破损
@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中的存储。
- 用过redisson的都很熟悉下面的这种写法,直接将key绑定到对应的数据结构上
这样写的好处是相当于做了分类,相关的逻辑只在这个分类中处理,而不是在全局,操作更简单方便,更好的贯彻了面向对象的思这样写的好处是相当于做了分类,相关的逻辑只在这个分类中处理,而不是在全局,操作更简单方便,更好的贯彻了面向对象的思想
- 继续看leftPush方法,先将key和value都通过序列化方法转化成Byte字节流(一开始我以为这部分影响性能比较严重,后来和导师确认了其实并不是,只是最简单的序列化方法)
- 后面的过程同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