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

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: 上一篇文章我们介绍秒杀案例的基本实现 Redis(二十二)-秒杀案例的基本实现以及用ab工具模拟并发。但是在文章的最后留了两个问题没有解决,一个是高并发情况下的超卖问题,以及jedis客户端请求超时问题。这篇文章就是来解决这两篇问题。

简介

上一篇文章我们介绍秒杀案例的基本实现 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);
        }
    }
}

其中连接池参数:

MaxTotal :控制一个pool可以分配多少个jedis实例,通过pool.getResource()来获取,如果赋值为-1,则表示不限制,如果pool已经分配了MaxTotal个jedis实例,则此时pool的状态为exhausted。

MaxIdle:控制一个pool最多有多少个状态为idle(空闲)的jedis实例;

MaxWaitMillis:表示当borrow一个jedis实例时,最大的等待毫秒数,如果超过等待时间,则直接抛JedisConnectionException;

再将原来的 Jedis jedis = new Jedis("127.0.0.1", 6379); 替换成 Jedis jedis = JedisPoolUtil.getJedisPool().getResource(); 表示从连接池中获取Jedis连接实例。

再次测试一下:

768ec8bb754b92015e8197b79f54f00a_a56d29584dc345848b8afe25e64f92e6.png

超卖问题

超卖问题的原因

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

// 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;
        }

这里通过watch方法监视库存的键 sk:1010:qt。然后将 减库存decr方法以及增加用户sadd方法放在事务队列,最后在通过exec方法依次执行事务队列中的命令。最终的运行结果如下:


这里给商品qt设置了30个库存,最终执行完成之后还剩下8个库存。虽然超卖的问题解决了但是还有一个新的问题:库存遗留问题。下一篇文章会接着来说下如何处理库存遗留问题。

总结

本文详细介绍了如何解决秒杀案例中的超卖以及超时问题。希望对读者朋友们有所帮助。


相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
8月前
|
监控 NoSQL Java
Redis之高并发超卖问题解决方案
在高并发的秒杀抢购场景中,常常会面临一个称为“超卖”(Over-Selling)的问题。超卖指的是同一件商品被售出的数量超过了实际库存数量,导致库存出现负数。这是由于多个用户同时发起抢购请求,而系统未能有效地控制库存的并发访问。
528 0
|
3月前
|
NoSQL Java API
美团面试:Redis锁如何续期?Redis锁超时,任务没完怎么办?
在40岁老架构师尼恩的读者交流群中,近期有小伙伴在面试一线互联网企业时遇到了关于Redis分布式锁过期及自动续期的问题。尼恩对此进行了系统化的梳理,介绍了两种核心解决方案:一是通过增加版本号实现乐观锁,二是利用watch dog自动续期机制。后者通过后台线程定期检查锁的状态并在必要时延长锁的过期时间,确保锁不会因超时而意外释放。尼恩还分享了详细的代码实现和原理分析,帮助读者深入理解并掌握这些技术点,以便在面试中自信应对相关问题。更多技术细节和面试准备资料可在尼恩的技术文章和《尼恩Java面试宝典》中获取。
美团面试:Redis锁如何续期?Redis锁超时,任务没完怎么办?
|
4月前
|
NoSQL Linux Redis
linux安装单机版redis详细步骤,及python连接redis案例
这篇文章提供了在Linux系统中安装单机版Redis的详细步骤,并展示了如何配置Redis为systemctl启动,以及使用Python连接Redis进行数据操作的案例。
100 2
|
3月前
|
消息中间件 NoSQL Kafka
大数据-116 - Flink DataStream Sink 原理、概念、常见Sink类型 配置与使用 附带案例1:消费Kafka写到Redis
大数据-116 - Flink DataStream Sink 原理、概念、常见Sink类型 配置与使用 附带案例1:消费Kafka写到Redis
214 0
|
5月前
|
NoSQL 网络协议 Linux
【Azure Redis】Lettuce客户端遇见连接Azure Redis长达15分钟的超时
【Azure Redis】Lettuce客户端遇见连接Azure Redis长达15分钟的超时
|
5月前
|
NoSQL 网络协议 Linux
【Azure Redis】Redis客户端出现15分钟的超时异常
【Azure Redis】Redis客户端出现15分钟的超时异常
|
5月前
|
缓存 监控 NoSQL
【Azure Redis 缓存】Azure Redis出现了超时问题后,记录一步一步的排查出异常的客户端连接和所执行命令的步骤
【Azure Redis 缓存】Azure Redis出现了超时问题后,记录一步一步的排查出异常的客户端连接和所执行命令的步骤
|
7月前
|
JSON NoSQL Redis
|
7月前
|
NoSQL Java Redis
数据管理DMS产品使用合集之在使用AWS DMS与ElastiCache for Redis进行通信时遇到Java超时错误,该怎么办
阿里云数据管理DMS提供了全面的数据管理、数据库运维、数据安全、数据迁移与同步等功能,助力企业高效、安全地进行数据库管理和运维工作。以下是DMS产品使用合集的详细介绍。
62 0
|
7月前
|
NoSQL Redis
Redis系列学习文章分享---第五篇(Redis实战篇--优惠券秒杀,全局唯一id 添加优惠券 实现秒杀下单 库存超卖问题分析 乐观锁解决超卖 实现一人一单功能 集群下的线程并发安全问题)
Redis系列学习文章分享---第五篇(Redis实战篇--优惠券秒杀,全局唯一id 添加优惠券 实现秒杀下单 库存超卖问题分析 乐观锁解决超卖 实现一人一单功能 集群下的线程并发安全问题)
148 0