code学习

Redis(二十三)-秒杀案例之超卖和超时问题解决

​作者:码农飞哥,

感谢您阅读本文,欢迎一键三连哦。

文章目录

  • ​​简介​​
  • ​​超时问题​​
  • ​​超时问题的现象​​
  • ​​超时问题的原因​​
  • ​​超卖问题​​
  • ​​超卖问题的原因​​
  • ​​超卖问题解决​​
  • ​​总结​​

简介

上一篇文章我们介绍秒杀案例的基本实现 Redis(二十二)-秒杀案例的基本实现以及用ab工具模拟并发。但是在文章的最后留了两个问题没有解决,一个是高并发情况下的超卖问题,以及jedis客户端请求超时问题。这篇文章就是来解决这两篇问题。

超时问题

超时问题的现象

java.net.SocketTimeoutException: connect timed out
  at java.net.DualStackPlainSocketImpl.waitForConnect(Native Method) ~[na:1.8.0_60]
  at java.net.DualStackPlainSocketImpl.socketConnect(DualStackPlainSocketImpl.java:85) ~[na:1.8.0_60]
  at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:350) ~[na:1.8.0_60]
  at      

超时问题的原因

因为在上面的案例中是每次都会创建一个新的连接实例,而创建连接实例又是一个比较耗时的动作。故会出现超时问题。

那么,解决超时问题可以利用jedis的线程池,节省每次连接redis服务带来的消耗,把连接好的实例反复利用。

通过参数管理连接的行为,代码如下:

public class JedisPoolUtil {
    private static JedisPool jedisPool;
    private JedisPoolUtil() {

    }
    //通过单例模式来定义jedisPool连接池
    public static JedisPool getJedisPool() {
        synchronized (JedisPoolUtil.class) {
            if (jedisPool == null) {
                JedisPoolConfig poolConfig = new JedisPoolConfig();
                poolConfig.setMaxTotal(200);
                poolConfig.setMaxIdle(32);
                poolConfig.setMaxWaitMillis(100 * 1000);
                poolConfig.setBlockWhenExhausted(true);
                poolConfig.setTestOnBorrow(true);
                //指定连接池的poolConfig,redis的IP地址,端口号,已经超时时间。
                jedisPool = new JedisPool(poolConfig, "127.0.0.1", 6379,60000);
            }
        }
        return jedisPool;
    }
    //释放jedis连接
    public static void release(JedisPool jedisPool, Jedis jedis) {
        if (jedis != null) {
            jedisPool.returnResource(jedis);
        }
    }
}      

其中连接池参数:

  1. MaxTotal :控制一个pool可以分配多少个jedis实例,通过pool.getResource()来获取,如果赋值为-1,则表示不限制,如果pool已经分配了MaxTotal个jedis实例,则此时pool的状态为exhausted。
  2. MaxIdle:控制一个pool最多有多少个状态为idle(空闲)的jedis实例;
  3. MaxWaitMillis:表示当borrow一个jedis实例时,最大的等待毫秒数,如果超过等待时间,则直接抛JedisConnectionException;

再将原来的​

​ Jedis jedis = new Jedis("127.0.0.1", 6379);​

​​ 替换成​

​ Jedis jedis = JedisPoolUtil.getJedisPool().getResource();​

​​ 表示从连接池中获取Jedis连接实例。

再次测试一下:

Redis(二十三)-秒杀案例之超卖和超时问题解决

超卖问题

超卖问题的原因

之所以出现超卖的情况还是因为在并发的情况下出现了线程安全的问题,即两个线程同时需减同一个库存。主要是如下代码有问题:

// 4.获取库存,如果库存null,秒杀还没有开始
    String skValue = jedis.get(skKey);
        if (skValue == null) {
            System.out.println("秒杀还没开始,请等待");
            return false;
        }
        // 6. 判断如果商品数量,库存数量小于1,秒杀结束
        if (Integer.parseInt(skValue) <= 0) {
            System.out.println("该商品的库存不足,秒杀失败");
            return false;
        }
        // 7. 秒杀过程
        //    7.1. 库存-1
        jedis.decr(skKey);
        //    7.2. 用户加入到set集合中
        jedis.sadd(userKey, uid);      

比如说现在还剩下最后一个库存,现在两个线程同时获取到库存值为1,两者又同时去修改库存值,最终的库存值就会被减成负数。

超卖问题解决

要解决这个问题话,还是需要用到前面提到的乐观锁,用乐观锁去监视库存值,哪怕出现上述的情况,由于第一个线程修改库存值之后,版本号也被修改了。故第二个线程不能修改成功。所以就不会出现超卖的情况。核心代码如下:

String skKey = "sk:" + prodid + ":qt";
    //监视库存
        jedis.watch(skKey);
        // 4.获取库存,如果库存null,秒杀还没有开始
        String skValue = jedis.get(skKey);
        if (skValue == null) {
            System.out.println("秒杀还没开始,请等待");
            return false;
        }
  //增加事务
        Transaction multi = jedis.multi();
        //组队操作
        multi.decr(skKey);
        multi.sadd(userKey, uid);
        //执行
        List<Object> results = multi.exec();
        if (results == null || results.size() == 0) {
            System.out.println("秒杀失败了。。。");
            jedis.close();
            return false;
        }      

总结