JAVA 多种减库存方案的效率问题

这里只做了一个减库存的操作,没有生成订单之类的操作,操作数据库使用的是JPA。
方案为别是:

  1. synchronized 同步方法
  2. 乐观锁
  3. for update 数据库排他锁
  4. redis+lua

方案一

@RestController
public class GoodsController {
    @Autowired
    GoodsDao goodsDao;
    
    @Transactional
    @GetMapping("/syn_stock")
    public synchronized String synStock(Integer id) {
        Optional<Goods> goodsOptional = goodsDao.findById(id);
        Goods goods = goodsOptional.get();
        int stock = goods.getStock();
        if (stock <= 0) {
            return "卖完了";
        }
        System.out.println("库存=" + stock);

        goods.setStock(stock - 1);
        goods = goodsDao.save(goods);
        System.out.println("库存成功减 1 数据库的库存为" + goods.getStock());

    }
 }

方案二

@RestController
public class GoodsController {
    @Autowired
    GoodsDao goodsDao;
    @Autowired
    private ApplicationContext applicationContext;

    /**
     * 减库存
     *
     * @param id
     * @return
     */
    @GetMapping("/stock")
    public String stock(Integer id) {
        ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
        GoodsController goodsController =(GoodsController) applicationContext.getBean("goodsController");

        Optional<Goods> goodsOptional = goodsDao.findById(id);
        int stock = goodsOptional.get().getStock();

        if (stock <= 0) {
            return "卖完了";
        }
        System.out.println("库存=" + stock);

        //不能直接调用updateStock方法,否则会使得事务失效,导致回滚失败
        Integer result = goodsController.updateStock(goodsOptional.get().getId(), stock);
        if (result == 0) {
            stock(id);
        }
        return "成功";
    }


    @Transactional
    public Integer updateStock(Integer id, int stock) {
        Integer result = goodsDao.updateStockById(stock - 1, stock, id);

        if (result == 1) {
            System.out.println("成功减 1 ,库存 = " + stock + "减完后库存 = " + (stock - 1));
        }
        
        return result;
    }
  }
--------------------------------------------------------
DAO 代码:
public interface GoodsDao  extends JpaRepository<Goods,Integer>, JpaSpecificationExecutor<Goods> {
    @Transactional
    @Query(value = "update goods  set stock=?1  where stock=?2 and id=?3 ", nativeQuery = true)
    //必须清掉缓存,否则无限循环,因为默认JPA一级缓存问题。
    @Modifying(clearAutomatically = true)
    Integer updateStockById(Integer stock,Integer old_stock , Integer id);
}

方案三

@RestController
public class GoodsController {
    @Autowired
    GoodsDao goodsDao;

    @Transactional
    @GetMapping("/for_update_stock")
    public void forUpdateStock(Integer id) {
        Goods goods = goodsDao.selectStocktById(id);
        Integer stock = goods.getStock();
        if (stock <= 0) {
            return;
        }
        System.out.println("库存=" + stock);

        goods.setStock(goods.getStock() - 1);
        goods = goodsDao.save(goods);
        System.out.println("库存成功减 1 数据库的库存为" + goods.getStock());

    }
 }
 -----------------------------------------------
    Dao代码:
   
    public interface GoodsDao  extends JpaRepository<Goods,Integer>, JpaSpecificationExecutor<Goods> {
    @Transactional
    @Query(value = "select *  from goods  where  id=?1 for update ", nativeQuery = true)
    Goods selectStockById(Integer id);
  }
  

方案四

首先依赖需要改动一下

    <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>io.lettuce</groupId>
                    <artifactId>lettuce-core</artifactId>
                </exclusion>
            </exclusions>
    </dependency>

代码:

@RestController
public class RedisGoodsController {
    @Autowired
    GoodsDao goodsDao;
    @Autowired
    RedisTemplate redisTemplate;
    String redisStockKey = "STOCK_";


    @GetMapping("redis_stock")
    public String redisStock(Integer id) {

        String key = redisStockKey + id;
        Long result = stock(key, 1);
        if (result > 0) {
            return "成功";
        } else if (result == -1) {
            return "卖完了";
        } else if (result == -2) {
            initStock(id, key, 30);
            redisStock(id);
        }

        return "成功";
    }

    /**
     * @return -1:库存不足 -2:库存未初始化 大于等于0:扣除后的库存
     */
    private String getLua() {
        StringBuilder sb = new StringBuilder();
        sb.append("if (redis.call('exists', KEYS[1]) == 1) then");
        sb.append("    local stock = tonumber(redis.call('get', KEYS[1]));");
        sb.append("    local num = tonumber(ARGV[1]);");
        sb.append("    if (stock >= num) then");
        sb.append("        return redis.call('DECRBY', KEYS[1], num);");
        sb.append("    end;");
        sb.append("    return -1;");
        sb.append("end;");
        sb.append("return -2;");
        return sb.toString();
    }


    /**
     * 库存初始化
     *
     * @param id
     * @param key
     * @param expire 过期时间 单位分钟
     * @return
     */
     //分布式请使用分布式锁
    public synchronized void initStock(Integer id, String key, long expire) {
        //双重检测
        Integer stock = (Integer) redisTemplate.opsForValue().get(key);
        if (stock == null) {
            Optional<Goods> goodsOptional = goodsDao.findById(id);
            stock = goodsOptional.get().getStock();
            
            redisTemplate.opsForValue().set(key, stock, expire, TimeUnit.MINUTES);
        }
    }


    /**
     * 执行lua
     *
     * @param key
     * @param num
     * @return -1:库存不足; -2:库存未初始化 大于等于0:扣除后的库存
     */
    private Long stock(String key, int num) {
        // 脚本里的KEYS参数
        List<String> keys = new ArrayList<>();
        keys.add(key);
        // 脚本里的ARGV参数
        List<String> args = new ArrayList<>();
        args.add(Integer.toString(num));
        String lua = getLua();


        Long result = (long) redisTemplate.execute(new RedisCallback<Long>() {
            @Override
            public Long doInRedis(RedisConnection connection) throws DataAccessException {
                Object nativeConnection = connection.getNativeConnection();
                return (Long) ((Jedis) nativeConnection).eval(lua, keys, args);
            }
        });
        return result;
    }
}

效率和总结:

方案一:同步方法,效率最慢, 实现最简单。
一个商品,500线程,测是7次,时间(单位:毫秒)分别为27886,24703,21650,22158,24004,25679,26051
去掉最大最小值 求平均为:24519

3个商品,各200线程,测试5次,时间分别为28967,25900,25906,21598,20664
去掉最大最小值 求平均为:24468

方案二:乐观锁,效率一般,高并发,多写的时候,乐观锁效率并不可观,实现较简单,注意几个坑就好,我以为单个商品 会比同步方法慢,结果,快了不少。

一个商品,500线程,测是7次,时间(单位:毫秒)分别为11989,10711,11351,11032,11783,11094,10723
去掉最大最小值 求平均为:11197

3个商品,各200线程,测试10次,时间分别为10094,7368,7289,7971,8640,9595,8817,8237,7873,7273
去掉最大最小值 求平均为:8224

方案三:数据库排他锁,效率比前面两种快,不试不知道,一试吓一跳,竟然还快了不少,实现也非常简单,

一个商品,500线程,测是7次,时间(单位:毫秒)分别为5924,6413,5916,5874,7009,5667,5196
去掉最大最小值 求平均为:5959

3个商品,各200线程,测试10次,时间分别为7224,6040,6236,7150,6617,5997,5555,5126,5720,6870
去掉最大最小值 求平均为:6273

方案四 redis 减库存,是真的太快了,实现起来比较麻烦,还有后面减数据库存,增加库存,生成订单,抛异常回滚了等比较复杂。

一个商品,500线程,测是7次,时间(单位:毫秒)分别为2026,479,499,421,384,379,361
去掉最大最小值 求平均为:432

3个商品,各200线程,测试10次,时间分别为1546,678,804,560,664,656,2263,663,775,672
去掉最大最小值 求平均为:807

你可能感兴趣的:(JAVA 多种减库存方案的效率问题)