java使用mysql和redis如何解决“商品超卖”
2018-01-20 00:00
603 查看
摘要: 商品超卖、超发
用java来模拟并发下的库存超卖:
测试多次,最终订单数几乎都超过1000 , 库存出现负数,比如:
情况b:线程获取库存的同时,其它线程正准备或正在修改库存。当库存充足时也不会出现问题,但其他线程将库存数减到了小于当前要去更新库存的线程数时,比如线程T1和T2获取到库存值为3,几乎同时2个其它线程将库存减到了1,然后T1和T2再去减库存的话,最终就可能超卖1个。
情况c:情况a和b一起,比如5个线程拿到的库存数都为3,几乎同时库存数被别的线程减到了1,等它们再去更新库存时,最终就可能超卖4个。
加锁怎么样?比如java的synchronized。但是并发场景下,加锁是件很影响效率的事,且粒度难以控制。
对于上面的代码,我们额外加了一些逻辑判断,似乎可以解决问题,如下:
测试多次,都不会出现超卖:(多线程下请忽略打印顺序)
代码的核心控制逻辑就是,在更新库存之后,再去实时判断一下库存数,如果超卖了就及时回退。这个判断逻辑得益于updateStockNum() 可以实时返回库存数,而不用再去getStockNum() 查一下,只要去查,就会存在耗时,并发下就会存在不准确性。
实际开发中,库存变量这种业务数据不会像上面的demo那样一直存在本地内存中,我们会使用mysql或者结合redis等数据库。它们各自的更新操作,都可以返回一些实时的操作信息,比如mysql update 操作可以实时返回受影响的行数,借助这些特性和上面代码中的思路,我们可以很方便地控制超卖。
减库存语句:
上面的sql语句,num>=1 这个条件至关重要,保证了只有库存为正值时才执行update操作,我们就可以利用实时返回的 “受影响的行数” 来判断减库存是否成功。
Java实现:
如果实时返回的“受影响的行数” reply==0,则表示num>=1不成立,即商品已售罄。在并发下,可能reply==0 这个代码分支会进来多次,无外乎“浪费了”几次数据库请求,但不需要进行库存回退操作,因为当前更新没有执行。
基于redis的库存控制,下面列举2种方法:
2.1)使用redis的incrby
实现逻辑和最上面的AtomicInteger几乎一样:
2.2)使用redis的列表
比如库存10,则lpush10个值,然后依次lpop,当lpop返回空nil,说明没有库存了:
Java实现:
比较2.1,好处是不存在回退库存的操作了,但如果库存数较大,比如在list放一万个1,会不会很臃肿?
什么是超卖?
商品超卖,简单理解就是仓库只有1000个商品,用户却成功下单1000个以上。这种超卖现象,不局限于电商的库存数,还包括其它场景,比如抢红包的预算,抽奖的奖品数等等。用java来模拟并发下的库存超卖:
//库存数(AtomicInteger原子操作) public static AtomicInteger stockNum = new AtomicInteger(1000); //订单数 public static AtomicInteger orderNum = new AtomicInteger(0); //获取库存 public static int getStockNum() { try { //模拟运行耗时 Thread.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } return stockNum.get(); } //更新库存+i,i<0时表示减库存 public static int updateStockNum(int i) { try { //模拟运行耗时 Thread.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } //返回当前库存数 return stockNum.addAndGet(i); } //添加订单+1 public static int insertOrder() { try { //模拟运行耗时 Thread.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } return orderNum.addAndGet(1); } public static void main(String[] args) throws InterruptedException { //简单模拟1100个人并发抢购 for (int i = 0; i < 1100; i++) { new Thread(() -> { //查询库存 int stockNum = getStockNum(); if (stockNum <= 0) { System.out.println("售罄!!!"); } else { //减库存 updateStockNum(-1); //添加订单 insertOrder(); } }).start(); } Thread.sleep(5 * 1000); System.out.println("最终下单数:" + orderNum.get()+" , 库存数:"+stockNum.get()); }
测试多次,最终订单数几乎都超过1000 , 库存出现负数,比如:
... ... 售罄!!! 售罄!!! 售罄!!! 最终下单数:1045 , 库存数:-45
问题出在哪?
情况a:多个线程几乎同时去获取库存,拿到的值一样,当库存充足时不会出现问题,但当线程数 > 库存数,比如5个线程拿到的库存数都为3,然后各自去减库存,最终就可能超卖2个。情况b:线程获取库存的同时,其它线程正准备或正在修改库存。当库存充足时也不会出现问题,但其他线程将库存数减到了小于当前要去更新库存的线程数时,比如线程T1和T2获取到库存值为3,几乎同时2个其它线程将库存减到了1,然后T1和T2再去减库存的话,最终就可能超卖1个。
情况c:情况a和b一起,比如5个线程拿到的库存数都为3,几乎同时库存数被别的线程减到了1,等它们再去更新库存时,最终就可能超卖4个。
该怎么处理?
库存变量虽然使用了AtomicInteger,但它只保证“int值更新”的原子性(N个线程同时去+1,结果始终是+N),类似于redis的incr/decr操作,它们都不能直接保证外层”整个业务操作”的原子性。加锁怎么样?比如java的synchronized。但是并发场景下,加锁是件很影响效率的事,且粒度难以控制。
对于上面的代码,我们额外加了一些逻辑判断,似乎可以解决问题,如下:
测试多次,都不会出现超卖:(多线程下请忽略打印顺序)
... 超卖,请回滚!!! ... 售罄!!! 最终下单数:1000 , 库存数:0
代码的核心控制逻辑就是,在更新库存之后,再去实时判断一下库存数,如果超卖了就及时回退。这个判断逻辑得益于updateStockNum() 可以实时返回库存数,而不用再去getStockNum() 查一下,只要去查,就会存在耗时,并发下就会存在不准确性。
实际开发中,库存变量这种业务数据不会像上面的demo那样一直存在本地内存中,我们会使用mysql或者结合redis等数据库。它们各自的更新操作,都可以返回一些实时的操作信息,比如mysql update 操作可以实时返回受影响的行数,借助这些特性和上面代码中的思路,我们可以很方便地控制超卖。
1 ) mysql
商品的库存信息,一般对应于表中的一行记录DROP TABLE IF EXISTS stock; CREATE TABLE stock ( id INT PRIMARY KEY, num INT NOT NULL ) ENGINE = INNODB; INSERT INTO stock VALUES (1, 1000);
减库存语句:
UPDATE stock SET num = num - 1 WHERE num >= 1 -- 保证库存为0时不执行update AND id = 1;
上面的sql语句,num>=1 这个条件至关重要,保证了只有库存为正值时才执行update操作,我们就可以利用实时返回的 “受影响的行数” 来判断减库存是否成功。
Java实现:
//减库存,返回mysql update受影响行数 public static int minusStockNum() throws SQLException { PreparedStatement ps = null;//略 int reply=ps.executeUpdate(" UPDATE stock SET num = num - 1 WHERE num >= 1 AND id = 1 "); return reply; } public static void main(String[] args) throws InterruptedException { //简单模拟1100个人并发抢单 for(int i=0;i<1100;i++){ new Thread(()->{ int stockNum=getStockNum(); if(stockNum<=0){ System.out.println("售罄!!!"); }else{ //减库存,拿到mysql update 受影响行数 int reply=minusStockNum(); //判断是否为0 if(reply==0){ System.out.println("售罄!!!"); }else{ //减库存成功,可以下单 insertOrder(); } } }).start(); } }
如果实时返回的“受影响的行数” reply==0,则表示num>=1不成立,即商品已售罄。在并发下,可能reply==0 这个代码分支会进来多次,无外乎“浪费了”几次数据库请求,但不需要进行库存回退操作,因为当前更新没有执行。
2 ) redis
redis读写操作的原子性,效果类似于java.util.concurrent.atomic下面的原子类,前者是单线程模式,天生具有原子性,后者则利用了CAS算法。基于redis的库存控制,下面列举2种方法:
2.1)使用redis的incrby
实现逻辑和最上面的AtomicInteger几乎一样:
//redis池 private static JedisPool jedisPool = null; //订单数 public static AtomicInteger orderNum = new AtomicInteger(0); //初始化库存池 public static void initStockNum(int num) { Jedis jedis = jedisPool.getResource(); try { jedis.set("stockNum", String.valueOf(num)); } catch (JedisException e) { e.printStackTrace(); } finally { jedis.close(); } } //获取库存 public static long getStockNum() { Jedis jedis = jedisPool.getResource(); long currentNum = 0; try { currentNum = Long.valueOf(jedis.get("stockNum")); } catch (JedisException e) { e.printStackTrace(); } finally { jedis.close(); } return currentNum; } //更新库存+i,i<0时表示减库存 public static long updateStockNum(int i) { Jedis jedis = jedisPool.getResource(); long currentNum = 0; try { currentNum = jedis.incrBy("stockNum", i); } catch (JedisException e) { e.printStackTrace(); } finally { jedis.close(); } return currentNum; } //添加订单 public static int insertOrder() { try { //模拟运行耗时 Thread.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } return orderNum.addAndGet(1); } public static void main(String[] args) throws InterruptedException { //连接池 jedisPool = new JedisPool(new GenericObjectPoolConfig(), "192.168.1.2", 6379, 2000, "password"); //初始化库存 initStockNum(1000); //简单模拟1100个人并发抢购 for (int i = 0; i < 1100; i++) { new Thread(() -> { //查询库存 long stockNum = getStockNum(); if (stockNum <= 0) { System.out.println("售罄!!!"); } else { /** * 减库存操作,实时拿到现在的库存数,如果<0,则说明超卖了,需要回滚 */ long currentNum = updateStockNum(-1); if (currentNum < 0) { System.out.println("超卖,请回滚!!!"); //回退库存 updateStockNum(1); } else { //添加订单 insertOrder(); } } }).start(); } Thread.sleep(5 * 1000); System.out.println("最终下单数:" + orderNum.get() + " , 库存数:" + getStockNum()); }
2.2)使用redis的列表
比如库存10,则lpush10个值,然后依次lpop,当lpop返回空nil,说明没有库存了:
lpush stockList 1 1 1 1 1 1 1 1 1 1 lpop stockList 1 //第1个人返回1 lpop stockList 1 //第2个人返回1 ... lpop stockList 1 //第11个人返回nil lpop stockList 1 //第12个人返回nil
Java实现:
//redis池 private static JedisPool jedisPool = null; //订单数 public static AtomicInteger orderNum = new AtomicInteger(0); //初始化库存池 public static void initStockNum(int num) { Jedis jedis = jedisPool.getResource(); try { String[] flags = new String[num]; for (int i = 0; i < num; i++) { flags[i] = String.valueOf(i); } jedis.del("stockList"); jedis.lpush("stockList", flags); } catch (JedisException e) { e.printStackTrace(); } finally { jedis.close(); } } //获取库存 public static long getStockNum() { Jedis jedis = jedisPool.getResource(); long currentNum = 0; try { currentNum = jedis.llen("stockList"); } catch (JedisException e) { e.printStackTrace(); } finally { jedis.close(); } return currentNum; } //库存加减1,返回受影响的个数 public static int updateOneStockNum(int i) { Jedis jedis = jedisPool.getResource(); int reply = 0; try { if (i == 1) { System.out.println("库存+1"); jedis.lpush("stockList", "1"); reply=1; } else if (i == -1) { System.out.println("库存-1"); if(jedis.lpop("stockList")!=null){ reply=1; } } } catch (JedisException e) { e.printStackTrace(); } finally { jedis.close(); } return reply; } //添加订单 public static int insertOrder() { try { //模拟运行耗时 Thread.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } return orderNum.addAndGet(1); } public static void main(String[] args) throws InterruptedException { //连接池 jedisPool = new JedisPool(new GenericObjectPoolConfig(), "192.168.1.2", 6379, 2000, "password"); //初始化库存 initStockNum(1000); //简单模拟1100个人并发抢购 for (int i = 0; i < 1100; i++) { new Thread(() -> { //查询库存 long stockNum = getStockNum(); if (stockNum <= 0) { System.out.println("售罄!!!"); } else { /** * 减库存操作,拿到redis lpop受影响的个数,如果==0,则说明售罄,这个和mysql类似不需要回退 */ long reply= updateOneStockNum(-1); if (reply == 0) { System.out.println("售罄!!!"); } else { //添加订单 insertOrder(); } } }).start(); } Thread.sleep(5 * 1000); System.out.println("最终下单数:" + orderNum.get() + " , 库存数:" + getStockNum()); }
比较2.1,好处是不存在回退库存的操作了,但如果库存数较大,比如在list放一万个1,会不会很臃肿?
相关文章推荐
- 使用Eclipse运行Java代码调用JDBC读写MySQL中文变成问号的终极解决办法
- 使用MySQL和Hibernate时,出现java.lang.UnsupportedOperationException: Update queries only supported through HQL异常的解决方法
- Mysql第一次使用-如何解决Mysql "发生系统错误2,找不到指定的文件" 的问题(第一次安装使用)
- MySQl使用-------如何修改root密码&&解决本地无法登录问题
- 使用Eclipse开发工具如何解决Java Compiler中Annotation Processin不出现的问题
- 如何解决使用远程工具登陆mysql,缺乏权限的问题
- 使用Eclipse运行Java代码调用JDBC读写MySQL中文变成问号的终极解决办法
- 安装mysql, 如何解决在centos上面用yum不能安装redis
- 如何使用redis做mysql的缓存
- Java中如何使用Redis做缓存
- 如何解决PHP使用mysql_query查询超大结果集超内存问题
- 如何使用java向mysql存取二进制图片
- mysql , java 中文乱码如何解决
- 解决Java程序使用MySQL时返回参数为乱码的示例教程
- java使用hibernate访问mysql 如何配置hibernate.cfg.xml
- mysql压缩包如何使用及PoolableConnectionFactory 和Access denied for user 'testdb'@'localhost'问题的解决
- 使用java语言操作,如何来实现MySQL中Blob字段的存取
- 解决Java程序使用MySQL时返回参数为乱码的示例教程
- 如何解决java.io.FileNotFoundException: mysql.ini (系统找不到指定的文件。)
- 正确使用MySQL JDBC setFetchSize() setMaxRows()方法解决JDBC处理大结果集 java.lang.OutOfMemoryError: Java heap space