一步步完善的过程
1. 环境搭建
1. 1 目录结构
1.2 sql
create table product(
id int(12) not null auto_increment comment 'id',
product_name varchar(60) not null comment '商品名称',
stock int(10) not null comment '库存',
price decimal(16,2) not null comment '单价',
version int(10) not null default 0 comment '版本号,乐观锁',
primary key (id)
);
-- 购买记录表
create table purchase_record(
id int(12) not null auto_increment comment ,
user_id int(12) not null comment '用户编号',
product_id int(12) not null comment '商品编号',
price decimal(16,2) not null comment '价格',
quantity int(12) not null comment '数量',
sum decimal(16,2) not null comment '总价',
purchase_date timestamp not null default now() comment '购买日期',
primary key (id)
);
service:
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public void seckillbyId(Integer id) {
Product product = productMapper.getById(id);
if (product .getStock() > 0) {
productMapper.decreaseStock(id);
} else {
System.out.println("库存不足");
}
}
mapper:
@Update("update product set stock=stock-1 where id = #{id}")
public int decreaseStock(@Param("id") int id);
2.理论基础
图片来自:https://www.cnblogs.com/SteadyJack/p/11228391.html
本文实现精简版
1.乐悲观锁原理了解
参见
2.压力测试JMeter基本使用
参见
3.redisTemplate基本使用
参见
4.秒杀系统设计概念
秒杀系统架构优化思路
秒杀架构参考
3.基于Mysql+Mybatis
3.1 基本情况
测试了好几组,一直没有出现超发情况,只能一直不停的修改增加线程数之类的,最后感觉一次减少一个不太行,就把代码改成了以下这样:
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public void seckillbyId(Integer id) {
Product product = productMapper.getById(id);
//增加随机数
int i = new Random().nextInt(10);
if (product .getStock() > 0) {
productMapper.decreaseStock(id, i);
} else {
System.out.println("库存不足");
}
}
@Update("update product set stock=stock-#{decrease} where id = #{id}")
public int decreaseStock(@Param("id") int id, @Param("decrease") int quantity);
原数据库数据:
超发第一次:
超发第二次:
其实都知道,直接查出来是否为0再减库存的方法肯定是不行,这样做的目的也就是测试一下测试是否可行,以及能否出现超发情况,不然一个错误的方法不能出现超发,后面也不用玩了!
3.2 基于乐观锁
乐观锁就是再修改之前看看,当前值是否和我拿到的值是否一样,如果不一样就是说明了,被别人修改了,那么我再去修改这个值可能就会出现并发修改的错误.
所以每次我要减库存了,我就来看看我改之前拿到的版本号是否被人修改了,即version
下面是修改过后的关键代码:
@Update("update product set stock=stock-#{decrease} , version = version +1 where id = #{id} and version=#{version}")
public int decreaseStockByOptimisticLock(@Param("id") int id, @Param("decrease") int quantity, @Param("version") int version);
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public void seckillbyId(Integer id) {
Product byId = productMapper.getById(id);
int i = new Random().nextInt(10);
if (byId.getStock() > 0) {
// productMapper.decreaseStock(id, i);
productMapper.decreaseStockByOptimisticLock(id,i,byId.getVersion());
} else {
System.out.println("库存不足");
}
}
然后进行测试:
然而:
没错,超发了
问题在哪?
… 认真看了的应该能够发现,没发现也可以思考一下
我就不说错误原因了:
修改后的代码如下:
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public void seckillbyId(Integer id) {
Product byId = productMapper.getById(id);
int i = new Random().nextInt(10);
if (byId.getStock() > 0 && byId.getStock() >= i) {
// productMapper.decreaseStock(id, i);
productMapper.decreaseStockByOptimisticLock(id, i, byId.getVersion());
} else {
System.out.println("库存不足");
}
}
再来进行测试:
哦nice!
但是这只是第一次,并发是有偶然性的哦!我这样提醒自己
我把每次的数据库的前后的值都贴出来
再次修改:
奥里给!!!还没出现超发
再来给个大的:
???
我不是设置的5000次吗? 没多大意义的一次测试
3.2 基于悲观锁
如何实现悲观锁呢?
最简单的方法就是加一个synchronized,在一个线程持有锁的时候,其他线程是无法获取到锁的
代码如下:
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public synchronized void seckillbyId(Integer id) {
Product byId = productMapper.getById(id);
int i = new Random().nextInt(10);
if (byId.getStock() > 0 && byId.getStock() >= i) {
productMapper.decreaseStock(id, i);
// productMapper.decreaseStockByOptimisticLock(id, i, byId.getVersion());
} else {
System.out.println("库存不足");
}
}
jconsole表示如下:
第一次:
堆内存:
线程:
跑回去测试了下乐观锁,乐观锁的jconsole表现如下:
4.基于Redis
由上面实现的乐观锁,悲观锁的方式,我们可以看出:
1.乐观锁其实把压力还是给到了数据库,一查一尝试写
2.悲观锁对数据库的压力只有一查一准确写
可见,使用悲观锁对数据库的压力是要小于乐观锁的,但是使用悲观锁,其实就是使减库存操作变成了串行,在性能方面其实是是否堪忧的,而在秒杀环境下,一下子进来那么多请求,完成一次秒杀应该是在很短的时间内的,这样了,我们才想到了,如何能够分摊数据库压力,提高响应速度,增加吞吐量?
当然这一切都是建立在绝对不能超发的情况下的;
众所周知,内存的读写速度是要远高于磁盘速度的
那就用redis做一层缓存呗
如何优化?
- 读数据库的剩余库存是否可以转移到redis
- 版本号是否可以转移到redis
很明显,全部全部全部都可以放到redis缓存里;
其实就是把对mysql的操作放到redis里面,等最后结束秒杀了,就同步回数据库
当然以上以上都是我个人的想法;
等过一段时间我再来看看,更加完善这一部分的代码书写;