秒杀系统的设计与压测

环境准备

数据库

完成demo至少需要两个数据表,一个customer表示秒杀的用户,一个sec_product表示被秒杀的商品。

sql 复制代码
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脚本实现:

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测试

  1. 新建一个测试计划
  2. 在测试计划中添加一个线程组,代表并发用户数,可以设置循环次数
  3. 在线程组下添加HTTP请求
  4. 导入数据库中的customer
  5. 添加聚合报告,用于查看

数据库乐观锁的方式实现

java 复制代码
@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锁整个逻辑

java 复制代码
    @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锁整个逻辑

java 复制代码
    @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异步的方式

java 复制代码
    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:

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

可能由于虚拟机配置较低的原因,提升效果并不明显。

相关推荐
萧曵 丶4 分钟前
Spring @TransactionalEventListener
java·数据库·spring·事务·transactional·异步
胡斌附体8 分钟前
mobaxterm终端sqlplus乱码问题解决
数据库·乱码·sqlplus·字符集设置
ladymorgana10 分钟前
【Docker】如何设置 `wiredTigerCacheSizeGB` 和 `resources.limits.memory`
运维·docker·容器
再见晴天*_*16 分钟前
logback 日志不打印
java·服务器·logback
moon66sun18 分钟前
开源项目XYZ.ESB:数据库到数据库(DB->DB)集成
数据库·esb
TDengine (老段)36 分钟前
使用 StatsD 向 TDengine 写入
java·大数据·数据库·时序数据库·iot·tdengine·涛思数据
DarkAthena42 分钟前
【GaussDB】深度解析:创建存储过程卡死且无法Kill会话的疑难排查
数据库·gaussdb
Gauss松鼠会1 小时前
GaussDB权限管理:从RBAC到精细化控制的企业级安全实践
大数据·数据库·安全·database·gaussdb
myskybeyond1 小时前
时序数据库TDEngine安装和使用
服务器·时序数据库·tdengine
时序数据说1 小时前
时序数据库IoTDB用户自定义函数(UDF)使用指南
大数据·数据库·物联网·开源·时序数据库·iotdb