秒杀系统的设计与压测

环境准备

数据库

完成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

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

相关推荐
倔强的石头_18 小时前
《Kingbase护城河》——数据库存储空间全景探测与精细化瘦身实战
数据库
冬奇Lab1 天前
每日一个开源项目(第134篇):Zvec - 阿里开源的嵌入式向量数据库,向量搜索界的 SQLite
数据库·人工智能·llm
ClouGence2 天前
Oracle CDC 架构优化:从主库直连到 DataGuard 备库同步
数据库·后端·oracle
无响应de神2 天前
三、用户与权限管理
数据库·mysql
大树882 天前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠2 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
霸道流氓气质2 天前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
小宇宙Zz2 天前
Maven依赖冲突
java·服务器·maven
Inhand陈工3 天前
基于台达PLC与映翰通IG502的智慧水产养殖精准投喂与远程运维解决方案
运维·人工智能·物联网·阿里云·信息与通信
酣大智3 天前
ARP代理--工作原理
运维·网络·arp·arp代理