前期准备:
设计表结构:
商品表

购买记录表

dao设计
<!--获取产品-->
<select id="getProduct" parameterType="long" resultType="product">
select id,product_name as productName,
stock,price,version,note from t_product
where id=#{id}
</select>
<!--减少库存-->
<update id="decreaseProduct">
update t_product set stock = stock - #{quantity}
where id = #{id}
</update>
<!--插入购买记录-->
<insert id="insertPurchaseRecord" parameterType="purchaseRecord">
insert into t_purchase_record(
user_id,product_id,price,quantity,sum,purchase_date,note)
values(#{userId},#{productId},#{price},#{quantity},
#{sum},now(),#{note})
</insert>
service层设计
@Transactional
public boolean purchase(Long userId, Long productId, int quantity) {
// 获取产品
ProductPo product = productMapper.getProduct(productId);
// 比较库存与购买量
if (product.getStock()<quantity){
return false;
}
// 扣减库存
productMapper.decreaseProduct(productId,quantity);
// 创建购买记录
PurchaseRecordPo pr = this.initPurchaseRecord(userId,product,quantity);
// 插入购买记录
purchaseRecordMapper.insertPurchaseRecord(pr);
return true;
}
private PurchaseRecordPo initPurchaseRecord(Long userId, ProductPo product, int quantity) {
PurchaseRecordPo pr = new PurchaseRecordPo();
pr.setNote("购买日志,时间:" + System.currentTimeMillis());
pr.setProductId(product.getId());
pr.setPrice(product.getPrice());
pr.setQuantity(quantity);
double sum = product.getPrice() * quantity;
pr.setSum(sum);
pr.setUserId(userId);
return pr;
}
controller层设计
@GetMapping("/purchase")
public String purchase(){
return "purchase";
}
@PostMapping("/purchase")
@ResponseBody
public Result purchase(Long userId,Long productId,Integer quantity){
boolean success = purchaseService.purchase(userId,productId,quantity);
String message = success? "抢购成功":"抢购失败";
Result result = new Result(success,message);
return result;
}
class Result{
private boolean success;
private String message;
public Result() {
}
public Result(boolean success, String message) {
this.success = success;
this.message = message;
}
public boolean isSuccess() {
return success;
}
public void setSuccess(boolean success) {
this.success = success;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
前端页面调用
<script type="text/javascript" src="/js/jquery-1.8.3.js"></script>
<script type="text/javascript">
for(var i=0;i<50000;i++){
var params = {
userId:1,
productId:1,
quantity:1
};
$.post("/purchase/purchase",params,function (result) {
//alert(result.message);
})
}
</script>
五万人抢两万件商品结果如下

可看出出现超发现象,卖出去了20003件商品,库存变为-3
那么这样的问题应该如何来解决呢?当前企业中提出了悲观锁、乐观锁、redis等解决方案
一、悲观锁
<!--获取产品-->
<select id="getProduct" parameterType="long" resultType="product">
select id,product_name as productName,
stock,price,version,note from t_product
where id=#{id} for update
</select>
数据库事务执行的过程中 就会锁定查询出来的数据 其他的事务将不能再对其进行读写,这样就避免了数据的不 单个请求直至数据 事务完成,才会释放这个锁,其他的请求才能重新得 这个锁

结果正确,但第一次我耗时们耗时51秒,这次我们一共花费了一分零5秒,花费了更多的时间,那如何减少需要花费的时间呢?
二、乐观锁
<!--减少库存-->
<update id="decreaseProduct">
update t_product set stock = stock - #{quantity},
version = version + 1
where id = #{id} and version = #{version}
</update>
别忘记去掉for update,并同步修改dao减少库存接口加上第三个参数
@Transactional
public boolean purchase(Long userId, Long productId, int quantity) {
// 获取产品
ProductPo product = productMapper.getProduct(productId);
// 比较库存与购买量
if (product.getStock()<quantity){
return false;
}
// 获取当前版本号
int version = product.getVersion();
// 尝试扣减库存
int result = productMapper.decreaseProduct(productId, quantity, version);
if (result == 0) {
return false;
}
// 创建购买记录
PurchaseRecordPo pr = this.initPurchaseRecord(userId,product,quantity);
// 插入购买记录
purchaseRecordMapper.insertPurchaseRecord(pr);
return true;
}
结果如下:卖出5447,剩余14553件产品
这次时间是快了,但是却剩余了大量的产品,为什么呢,因为并发操作时好多操作都被判定失败了。
改进方法:
使用时间戳限制重入的乐观锁
public boolean purchase(Long userId, Long productId, int quantity) {
// 当前时间
long start = System.currentTimeMillis();
// 循环尝试直到成功
while(true){
// 循环时间
long end = System.currentTimeMillis();
if (end - start>100){
return false;
}
// 获取产品
ProductPo product = productMapper.getProduct(productId);
// 比较库存与购买量
if (product.getStock()<quantity){
return false;
}
// 获取当前版本号
int version = product.getVersion();
// 尝试扣减库存
int result = productMapper.decreaseProduct(productId, quantity, version);
if (result == 0) {
continue;
}
// 创建购买记录
PurchaseRecordPo pr = this.initPurchaseRecord(userId,product,quantity);
// 插入购买记录
purchaseRecordMapper.insertPurchaseRecord(pr);
return true;
}
}
结果如下: 吐血,可能由于本人电脑问题,剩余商品反而更多了,剩余18698,卖出1302,耗时1分27秒
使用限定次数重入的乐观锁
@Transactional
public boolean purchase(Long userId, Long productId, int quantity) {
for(int i=0;i<3;i++){
// 获取产品
ProductPo product = productMapper.getProduct(productId);
// 比较库存与购买量
if (product.getStock()<quantity){
return false;
}
// 获取当前版本号
int version = product.getVersion();
// 尝试扣减库存
int result = productMapper.decreaseProduct(productId, quantity, version);
if (result == 0) {
continue;
}
// 创建购买记录
PurchaseRecordPo pr = this.initPurchaseRecord(userId,product,quantity);
// 插入购买记录
purchaseRecordMapper.insertPurchaseRecord(pr);
return true;
}
return false;
}
结果如下:这次还好,剩余6550,卖出13450,耗时1分16秒
三、那有没有更好的方法呢,有的那就是使用redis
采用lua脚本在内存中操作数据进行抢购,再通过定时任务的方式将其写入到数据库中,总用时15秒,非常快。