redis高并发导致读写变慢(redis多线程)

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)。这个方法主要两个作用

  1. 比较AtomicBoolean和expect的值,如果一致,执行方法内的语句。其实就是一个if语句
  2. 把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();
        }
    }
}

猜你喜欢

转载自blog.csdn.net/u012723183/article/details/103690139