1.
效率最低(同步关键字,低并发可以,高并发不行,10000个线程,甚至十几秒钟才扣减一个库存)
for update 也是悲观锁,会锁住当前行,影响性能
1.乐观锁机制(加version版本号)
3.用mybatis自带的行锁,同一时间只能有一个线程修改某一行数据
直接上代码:
mapper代码:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.example.mapper.ProductDao"> <!--FOR UPDATE 悲观锁没用--> <select id="getProductAmountById" resultType="com.example.entity.Product"> SELECT id ,amount,version FROM product WHERE id=1 FOR UPDATE; </select> <update id="subtracStock" > UPDATE product set amount = amount - #{buyAmount}, version = version+1 WHERE id=1 </update> <update id="subtracStockWithOptimistic" > UPDATE product set amount = amount - #{buyAmount}, version = version+1 WHERE id=1 and version = #{version} </update> <!--不用乐观锁,用数据库提供的行锁来实现(强推)--> <update id="subtracStockNoOptimistic" > UPDATE product set amount = amount - #{buyAmount} WHERE id=1 and amount >= #{buyAmount}+0 </update>
dao:
import com.example.entity.Product; import org.apache.ibatis.annotations.Param; public interface ProductDao { int subtracStock(@Param("buyAmount") int buyAmoun); int subtracStockWithOptimistic(@Param("buyAmount") int buyAmount,@Param("version") int version); Product getProductAmountById(); int subtracStockNoOptimistic(@Param("buyAmount") int buyAmoun); }
service:
import org.apache.ibatis.annotations.Param; public interface ProductService { int subtracStock(Integer id, Integer buyAmout); int subtracStockOptimistic(Integer id, Integer buyAmout); int subtracStockNoOptimistic(int buyAmoun); }
service 实现
package com.example.service; import com.example.CustomException.NoStockException; import com.example.entity.Product; import com.example.mapper.ProductDao; import com.example.redis.RedisUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.concurrent.TimeUnit; @Service public class ProductServiceImp implements ProductService { @Autowired public ProductDao productDao; @Autowired private RedisUtils redisUtils; //性能太差,并发不高的话可以使用(否则贼慢 10000个线程就不行了,库存减不动了) @Override public int subtracStock(Integer productId,Integer buyAmount) { synchronized (ProductServiceImp.class){ //判断库存 Product product = productDao.getProductAmountById(); Integer amount = product.getAmount(); if(amount<=0){ throw new NoStockException("no stock",406,"no stock"); } if(amount>=buyAmount){ productDao.subtracStock(buyAmount); System.out.println("--------"); }else{ throw new NoStockException("no stock",406,"less stock"); } } return 0; } @Override public int subtracStockOptimistic(Integer id, Integer buyAmount) { //判断库存 Product product = productDao.getProductAmountById(); if(product.getAmount()<=0){ throw new NoStockException("no stock",406,"no stock"); } if(product.getAmount()>=buyAmount){ //错误:获取最新版本号,一样则进行修改(这个习惯错误) //正确:将 商品id,购物数量,自己的version 交给数据库执行,判断交给数据库(高效,不容易犯错) int result = productDao.subtracStockWithOptimistic(buyAmount,product.getVersion()); if(result == 1){ return 1; }else{ //进入重试阶段(为的是刚好有200的并发,200的库存,将库存都卖完,而不是200 线程进来,因为版本号不一致,放弃了扣减库存的机会) subtracStockOptimistic( id, buyAmount); } }else{ throw new NoStockException("no stock",406,"less stock"); } return 0; } @Override public int subtracStockNoOptimistic(int buyAmoun) { //一分钟之内没有秒杀过的用户可以秒杀,否则不让秒杀 Long start = System.currentTimeMillis(); Object isExist = redisUtils.get("hasGetStock"); if(null == isExist){ //不需要重试也可以200个请求都把100 的库存减去 int result = productDao.subtracStockNoOptimistic(buyAmoun); //秒杀成功,将秒杀结果存到redis,防止同一个用户多次秒杀(薅羊毛) if(result ==0){ redisUtils.set("hasGetStock",1,1L,TimeUnit.MINUTES); return 1; } }else{ // redis 的ttl还可以做倒计时功能 System.out.println("一分钟之内不可以多次秒杀"); return 0; } return 0; } }
controller:
//减库存 @GetMapping("/submitOrder") public String submitOrder(Integer amount) { try { // 悲观锁并发低可以,并发量大的话,就严重影响性能(10000并发 扣库存很慢,甚至十几秒扣一个库存) //int a = productService.subtracStock(1,amount); //乐观锁(推荐) //productService.subtracStockOptimistic(1,amount); //不用乐观锁(强推) productService.subtracStockNoOptimistic(amount); //问题:如何防止被恶意用户薅羊毛??(限制用户抢的次数) //成功后,由mq来生成订单(mq的好处:异步,可以提高用户响应时间) } catch(NoStockException nse) { throw new NoStockException("no stock",406,"no stock"); } catch(Exception e) { throw new RuntimeException("Failed!!"); } // 告知微信(支付宝)已经成功处理,不需要再发送回调 return "success"; }
总结:
第三种方法性能最好,实现也简单