redis 高并发读写变慢
最近在最redis + MQ高并发的一个功能,压测时发现redis读写性能突然降低很多,而redis已经启用一年多,一直没问题,走了点弯路后发现是因为对 JedisPool的高并发处理上存在效率
问题,如下为分析:
压测:此功能线上一小时6万的访问量,压测时同一比数据100各并发扫5分钟就开始报问题
效果:一开始读写毫秒级,1分钟后逐渐变慢,全部扫描完平均读写速度9s
分析原因
- 写的redis工具类RedisUtil.java开启JedisPool的逻辑效率有问题,如下段代码
- 每次操作
getJedis()
时都会通过synchronized
控制多线程并发,并且每次操作都会初始化,当高并发时会持续等待
Java中堆和栈——关键字volatile
每个线程都有自己的栈内存,用于存储本地变量,方法参数和栈调用,一个线程中存储的变量对其它线程是不可见的。而堆是所有线程共享的一片公用内存区域。对象都在堆里创建,为了提升效率线程会从堆中弄一个缓存到自己的栈,如果多个线程使用该变量就可能引发问题,这时
volatile
变量就可以发挥作用了,它要求线程从主存中读取变量的值。
//哨兵模式
private static JedisSentinelPool jedisPool = null;
//单机模式
private static JedisPool jedisPoolS = null;
public static Jedis getJedis() {
Jedis jedis = null;
if (jedisPoolS == null) {
poolInit();
}
}
/*
* 在多线程环境同步初始化, 这里效率有问题
*/
private synchronized static void poolInit() {
if (jedisPool == null) {
initialPool();
}
}
/*
* 多线程并发时这里会持续初始化,再加上synchronized的所会导致持续等待
*/
private static void initialPool(){
try {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(MAX_ACTIVE);
config.setMaxIdle(MAX_IDLE);
config.setMaxWaitMillis(MAX_WAIT);
config.setTestOnBorrow(TEST_ON_BORROW);
String[] redisIPList = redisIP.split(",");
Set<String> sentinels=new HashSet<String>();
for(int i=0;i<redisIPList.length;i++) {
sentinels.add(redisIPList[i]);
}
//使用redis服务器类型:1-测试(单机模式),2-生产(哨兵模式)
if("1".equals(redisType)) {
jedisPoolS = new JedisPool(config, redisIP, redisPort, 100000, AUTH);
}else {
jedisPool = new JedisSentinelPool(master, sentinels, config, TIMEOUT, AUTH);
}
} catch (Exception e) {
e.printStackTrace();
logger.error("First create JedisPool error : "+e);
}
}
解决方案
Java中堆和栈——关键字volatile
每个线程都有自己的栈内存,用于存储本地变量,方法参数和栈调用,一个线程中存储的变量对其它线程是不可见的。而堆是所有线程共享的一片公用内存区域。对象都在堆里创建,为了提升效率线程会从堆中弄一个缓存到自己的栈,如果多个线程使用该变量就可能引发问题,这时
volatile
变量就可以发挥作用了,它要求线程从主存中读取变量的值。
AtomicBoolean
AtomicBoolean是java.util.concurrent.atomic包下的原子变量,这个包里面提供了一组原子类。其基本的特性就是在多线程环境下,当有多个线程同时执行这些类的实例包含的方法时,具有排他性,即当某个线程进入方法,执行其中的指令时,不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成,才由JVM从等待队列中选择一个另一个线程进入,这只是一种逻辑上的理解。实际上是借助硬件的相关指令来实现的,
不会阻塞线程
(或者说只是在硬件级别上阻塞了)。
例如AtomicBoolean,在这个Boolean值的变化的时候不允许在之间插入,保持操作的原子性。方法和举例:compareAndSet(boolean expect, boolean update)。这个方法主要两个作用
- 比较AtomicBoolean和expect的值,如果一致,执行方法内的语句。其实就是一个if语句
- 把AtomicBoolean的值设成update,比较最要的是这两件事是一气呵成的,这两个动作之间不会被打断,任何内部或者外部的语句都不可能在两个动作之间运行。为多线程的控制提供了解决的方案。
原文链接:百度资料
- 将Jedispool初始化到
堆内存
中 - 通过
volatile
变量使线程从主存中读取变量的值 - 利用AtomicBoolean进行
CAS方法加锁
,保证单一初始化
//主要代码段
private volatile static JedisSentinelPool jedisPool = null;
private volatile static JedisPool jedisPoolS = null;
private static AtomicBoolean initFlag = new AtomicBoolean(false);
if(initFlag.compareAndSet(false, true)) {
...
}
调整后示例
package com.sinosoft.prpall.pubfun.redis;
import java.util.HashSet;
import java.util.Set;
import org.apache.log4j.Logger;
import com.sinosoft.sysframework.reference.AppConfig;
import com.sinosoft.utility.string.Str;
import edu.emory.mathcs.backport.java.util.concurrent.atomic.AtomicBoolean;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.JedisSentinelPool;
/**
* Redis 工具类
* @author caspar
* https://blog.csdn.net/tuposky
*/
public class RedisUtil {
protected static Logger logger = Logger.getLogger(RedisUtil.class);
//redis服务器地址
private static String redisIP = AppConfig.get("Redis.SentinelServiceIP");
//redis服务器端口
private static int redisPort = Integer.parseInt(AppConfig.get("Redis.Port"));
//主服务器名
private static String master = AppConfig.get("Redis.SentinelMasterName");
//访问密码
private static String AUTH = AppConfig.get("Redis.Auth");
//连接的DB序号
private static int dbSerialNo = Integer.parseInt(Str.chgStrZero(AppConfig.get("Redis.DBSerialNo")));
//可用连接实例的最大数目,默认值为8;
//如果赋值为-1,则表示不限制;如果pool已经分配了maxActive个jedis实例,则此时pool的状态为exhausted(耗尽)。
private static int MAX_ACTIVE = Integer.parseInt(Str.chgStrZero(AppConfig.get("Redis.MaxActive")));
//控制一个pool最多有多少个状态为idle(空闲的)的jedis实例,默认值也是8。
private static int MAX_IDLE = Integer.parseInt(Str.chgStrZero(AppConfig.get("Redis.MaxIdle")));
//等待可用连接的最大时间,单位毫秒,默认值为-1,表示永不超时。如果超过等待时间,则直接抛出JedisConnectionException;
private static int MAX_WAIT = Integer.parseInt(Str.chgStrZero(AppConfig.get("Redis.WaitTime")));
//超时时间
private static int TIMEOUT = Integer.parseInt(Str.chgStrZero(AppConfig.get("Redis.TimeOut")));
//使用redis服务器类型:1-测试(单机模式),2-生产(哨兵模式)
private static String redisType = AppConfig.get("Redis.RedisType");
//在borrow一个jedis实例时,是否提前进行validate操作;如果为true,则得到的jedis实例均是可用的;
private static boolean TEST_ON_BORROW = true;
//将Jedispool初始化到 堆内存 中通过 volatile 变量使线程从主存中读取变量的值
private volatile static JedisSentinelPool jedisPool = null;
private volatile static JedisPool jedisPoolS = null;
//利用AtomicBoolean进行 CAS方法加锁 ,保证单一初始化
private static AtomicBoolean initFlag = new AtomicBoolean(false);
/**
* redis过期时间,以秒为单位
*/
public final static int EXRP_HOUR = 60*60; //一小时
public final static int EXRP_DAY = 60*60*24; //一天
public final static int EXRP_MONTH = 60*60*24*30; //一个月
/**
* 初始化Redis连接池
*/
static{
// 利用CAS方法加锁,保证单一初始化
if(initFlag.compareAndSet(false, true)) {
try {
logger.info(Thread.currentThread().getId() + " init pool");
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(MAX_ACTIVE);
config.setMaxIdle(MAX_IDLE);
config.setMaxWaitMillis(MAX_WAIT);
config.setTestOnBorrow(TEST_ON_BORROW);
String[] redisIPList = redisIP.split(",");
Set<String> sentinels=new HashSet<String>();
for(int i=0;i<redisIPList.length;i++) {
sentinels.add(redisIPList[i]);
}
//使用redis服务器类型:1-测试(单机模式),2-生产(哨兵模式)
if("1".equals(redisType)) {
jedisPoolS = new JedisPool(config, redisIP, redisPort, 100000, AUTH);
}else {
jedisPool = new JedisSentinelPool(master, sentinels, config, TIMEOUT, AUTH);
}
} catch (Exception e) {
// 初始化失败,放开标志让其它线程尝试初始化池
initFlag.set(false);
e.printStackTrace();
logger.error("First create JedisPool error : "+e);
}
}else {
// 等待第一个进入的线程初始化完毕
while(jedisPoolS == null && jedisPool == null) {
logger.info(Thread.currentThread().getId() + " 等待其它线程完成连接池初始化 ...");
}
}
}
/**
* 同步获取Jedis实例
* @return Jedis
*/
public static Jedis getJedis() {
Jedis jedis = null;
try {
//使用redis服务器类型:1-测试(单机模式),2-生产(哨兵模式)
if("1".equals(redisType)) {
if (jedisPoolS != null) {
jedis = jedisPoolS.getResource();
jedis.select(dbSerialNo);
}
}else {
if (jedisPool != null) {
jedis = jedisPool.getResource();
jedis.select(dbSerialNo);
}
}
} catch (Exception e) {
e.printStackTrace();
logger.error("Get jedis error : ", e);
throw e;
}
return jedis;
}
public void returnJedisResource(Jedis jedis){
if(jedis != null){
jedis.close();
}
}
}