环境准备
数据库
完成demo至少需要两个数据表,一个customer表示秒杀的用户,一个sec_product表示被秒杀的商品。
create database sec_kill;
use sec_kill;
create table customer(
id int primary key auto_increment not null,
name varchar(20),
phone varchar(20)
);
create table product(
id int primary key,
name varchar(20),
stock int
);
create table product_order
(
id int auto_increment primary key,
product_id int null,
customer_id int null
);
在customer中添加5000个用户,用于秒杀。用SQL脚本实现:
-- 插入5000个customer记录
delimiter $$
create procedure insert_customers()
BEGIN
declare i int default 1;
declare max int default 5000;
while i <= max do
-- 生成name字段,格式为customer_xxxx,xxxx为编号
SET @name = concat('customer_', lpad(i, 4, '0'));
-- 生成phone字段,格式为1300000xxxx,xxxx为编号
SET @phone = concat('1300000', lpad(i, 4, '0'));
insert into customer (name, phone) values (@name, @phone);
set i = i + 1;
end while ;
END $$
delimiter ;
-- 调用存储过程
call insert_customers();
JMeter测试
- 新建一个测试计划
- 在测试计划中添加一个线程组,代表并发用户数,可以设置循环次数
- 在线程组下添加HTTP请求
- 导入数据库中的customer
- 添加聚合报告,用于查看
数据库乐观锁的方式实现
@Service
public class SecKillServiceImpl extends ServiceImpl<ProductMapper, Product> implements SecKillService{
@Resource
private ProductOrderService productOrderService;
@Override
public String sec_kill(int customer_id, int product_id) {
boolean result = this.update().setSql("stock = stock - 1")
.eq("id", product_id)
.gt("stock", 0)
.update(); // where id = ? and stock > 0
if (!result) {
return "秒杀失败";
}
ProductOrder order = new ProductOrder();
order.setCustomerId(1);
order.setProductId(customer_id);
productOrderService.save(order);
return "抢购成功";
}
}
吞吐量大概是500/s
用reentrantLock锁整个逻辑
@Override
public String sec_kill(int customer_id, int product_id) {
ReentrantLock lock = new ReentrantLock();
lock.lock();
boolean result = this.update().setSql("stock = stock - 1")
.eq("id", product_id)
.gt("stock", 0)
.update();
if (!result) {
return "秒杀失败";
}
ProductOrder order = new ProductOrder();
order.setCustomerId(1);
order.setProductId(customer_id);
productOrderService.save(order);
lock.unlock();
return "抢购成功";
}
用synchronized锁整个逻辑
@Override
public synchronized String sec_kill(int customer_id, int product_id) {
boolean result = this.update().setSql("stock = stock - 1")
.eq("id", product_id)
.gt("stock", 0)
.update();
if (!result) {
return "秒杀失败";
}
ProductOrder order = new ProductOrder();
order.setCustomerId(1);
order.setProductId(customer_id);
productOrderService.save(order);
return "抢购成功";
}
用redis异步的方式
private static final DefaultRedisScript<Long> SECKILL_SCRIPT;
static {
SECKILL_SCRIPT = new DefaultRedisScript<>();
SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));
SECKILL_SCRIPT.setResultType(Long.class);
}
@Autowired
private StringRedisTemplate redisTemplate;
@Override
public String sec_kill(int customer_id, int product_id) {
Long result = redisTemplate.execute(SECKILL_SCRIPT, Collections.emptyList(), product_id + "", customer_id + "");
int r = result.intValue();
if (r != 0) {
return "秒杀失败";
}
return "抢购成功";
}
seckill.lua:
-- 1.参数列表
-- 1.1.优惠券id
local voucherId = ARGV[1]
-- 1.2.用户id
local userId = ARGV[2]
-- 2.数据key
-- 2.1.库存key
local stockKey = 'seckill:stock:' .. voucherId
-- 2.2.订单key
local orderKey = 'seckill:order:' .. voucherId
-- 3.脚本业务
-- 3.1.判断库存是否充足 get stockKey
if(tonumber(redis.call('get', stockKey)) <= 0) then
-- 3.2.库存不足,返回1
return 1
end
-- 3.2.判断用户是否下单 SISMEMBER orderKey userId
if(redis.call('sismember', orderKey, userId) == 1) then
-- 3.3.存在,说明是重复下单,返回2
return 2
end
-- 3.4.扣库存 incrby stockKey -1
redis.call('incrby', stockKey, -1)
-- 3.5.下单(保存用户)sadd orderKey userId
redis.call('sadd', orderKey, userId)
-- 3.6.发送消息到队列中, XADD stream.orders * k1 v1 k2 v2 ...
redis.call('xadd', 'stream.orders', '*', 'userId', userId, 'voucherId', voucherId)
return 0
可能由于虚拟机配置较低的原因,提升效果并不明显。