这里只做了一个减库存的操作,没有生成订单之类的操作,操作数据库使用的是JPA。
方案为别是:
@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