任何解决方案的引入,都是为了解决我们所遇到的业务问题。分布式锁可以说是解决各种业务问题的一把利刃,本篇笔记将从具体的业务场景开始,一步步的来说分布式锁的使用。
- 拿秒杀扣除库存的业务场景来说明,比如秒杀百事可乐,业务方提供的秒杀库存只有 100 件商品,那么如何防止超卖
- 超卖就是你计划卖 100 件商品,结果你卖出去了 101 件。这多出来的 1 件商品对于业务方来说就是资损
1. 超卖并发请求模型
2. 基于内存数据模拟
- 商品对象
public class Goods implements Serializable {
//商品总库存
private Integer count = 5000;
public Integer getCount() {
return count;
}
public void setCount(Integer count) {
this.count = count;
}
}
- service 层代码
@Service
public class GoodsServiceImpl implements GoodsService {
private static final Logger log = LoggerFactory.getLogger(GoodsService.class);
private Goods goods = new Goods();
public void deductStock() {
goods.setCount(goods.getCount() - 1);
log.info("商品库存余量 :{}", goods.getCount());
}
}
- 请求入口
@RestController
@RequestMapping("/lock")
public class GoodsController {
@Resource
private GoodsService goodsService;
@GetMapping("/issue")
public Object deduct() {
goodsService.deductStock();
return "success";
}
}
2.1 测试结果
20 秒内启动 100 个线程分别循环 51 次
并发请求后导致商品超卖的情况,库存被扣减成负数:
2.2 解决方案
- 面对单机应用,此时我们能想到的最快的解决方案就是 JDK 的同步工具
synchronized
或ReentrantLock
public void deductStock() {
synchronized (GoodsServiceImpl.class) {
goods.setCount(goods.getCount() - 1);
log.info("商品库存余量 :{}", goods.getCount());
}
}
3. 基于mysql数据来模拟
- 在基于内存数据模拟时,我们使用了
synchronized
和ReentrantLock
,那么在使用 mysql 后这种方式还能解决问题吗 ???
使用 spring boot + tk.mybatis 框架完成这段代码
- 商品对象
package com.example.distributed.lock.seckill.bean;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
import java.io.Serializable;
@Getter
@Setter
@Table(name = "order.goods")
public class Goods implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// 商品编码
@Column(name = "code")
private String code;
//商品总库存
@Column(name = "count")
private Integer count;
}
- service 层代码
@Service
public class GoodsServiceImpl implements GoodsService {
private static final Logger log = LoggerFactory.getLogger(GoodsService.class);
@Resource
private GoodsMapper goodsMapper;
public void deduct() {
Example query = Example.builder(Goods.class).build();
query.createCriteria().andEqualTo("code", "1001");
Goods goods = goodsMapper.selectOneByExample(query);
if (null != goods && goods.getCount() > 0) {
int count = goods.getCount() - 1;
log.info("商品库存余量 :{}", count);
goods.setCount(count);
goodsMapper.updateByPrimaryKeySelective(goods);
}
}
}
- dao 层代码
import tk.mybatis.mapper.common.Mapper;
import tk.mybatis.mapper.common.MySqlMapper;
public interface SqlMapper<T> extends Mapper<T>, MySqlMapper<T> {
}
package com.example.distributed.lock.seckill.repository.mybatis;
import com.example.distributed.lock.seckill.bean.Goods;
import com.example.distributed.lock.seckill.repository.SqlMapper;
public interface GoodsMapper extends SqlMapper<Goods> {
}
注意 :tk.mybatis 中你自己定义的 SqlMapper
路径不能和 GoodsMapper
在同一目录下,否则启动会报错!!!GoodsMapper
所在的目录就是 @MapperScan
的扫描路径
@MapperScan(basePackages = {"com.example.distributed.lock.seckill.repository.mybatis"})
- 请求入口
@RestController
@RequestMapping("/lock")
public class GoodsController {
@Resource
private GoodsService goodsService;
@GetMapping("/issue")
public Object deduct() {
goodsService.deduct();
return "success";
}
}
3.1 测试结果
3.1.1 当业务方法没有开启事务
- 数据库里面有库存商品 100 个
- 立即启动 10 个线程分别循环 10 次
public void deduct() {
Example query = Example.builder(Goods.class).build();
query.createCriteria().andEqualTo("code", "1001");
synchronized (GoodsServiceImpl.class) {
Goods goods = goodsMapper.selectOneByExample(query);
if (null != goods && goods.getCount() > 0) {
int count = goods.getCount() - 1;
log.info("商品库存余量 :{}", count);
goods.setCount(count);
goodsMapper.updateByPrimaryKeySelective(goods);
}
}
}
结论:未开启事务,未发生多扣除商品的情况,synchronized
方案生效
3.1.2 当业务方法开启事务
- 数据库里面有库存商品 1000 个
- 20秒内启动 100 个线程分别循环 10 次
结论:开启事务,使用 synchronized
,发现有重复更新库存数量的现象
4. 总结
通过上面两个实验发现,使用同步工具 synchronized
或 ReentrantLock
无法根本解决秒杀业务场景的重复扣减库存的问题!原因如下:
- MySQL的事务会导致
synchronized
失效 - 集群部署也会导致
synchronized
失效