1 秒杀系统模拟基础实现,使用DB实现

本文介绍了一个基于Spring Boot和MySQL的秒杀系统设计方案,通过合理的业务、系统和数据隔离策略,结合乐观锁机制确保高并发场景下的商品库存准确无误。

本文根据动脑学院的一节类似的课程,改编实现。分别使用DB和redis来完成。



隔离的解释

业务隔离:将秒杀业务独立出来,尽量不与其他业务关联,以减少对其他业务的依赖性。譬如秒杀业务只保留用户id,商品id,数量等重要属性,通过中间件发送给业务系统,完成后续的处理。

系统隔离:将秒杀业务单独部署,以减少对其他业务服务器的压力。

数据隔离:由于秒杀对DB的压力很大,将DB单独部署,不与其他业务DB放一起,避免对DB的压力。



本篇讲使用DB完成秒杀系统。下一篇使用redis完成持久层。

一 初始化项目

以Springboot,mysql,jpa为技术方案。

新建Springboot项目,pom如下

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<groupId>com.tianyalei</groupId>
	<artifactId>common</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>jar</packaging>

	<name>common</name>
	<description>Demo project for Spring Boot</description>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>1.5.2.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
		<java.version>1.8</java.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<optional>true</optional><!-- optional=true,依赖不会传递,该项目依赖devtools;之后依赖myboot项目的项目如果想要使用devtools,需要重新引入 -->
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-redis</artifactId>
		</dependency>
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>druid</artifactId>
			<version>1.0.18</version>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>


</project>

javaBean

package com.tianyalei.model;

import javax.persistence.*;

/**
 * Created by wuwf on 17/7/5.
 */
@Entity
public class GoodInfo {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Integer id;
    //数量
    private int amount;
    //商品编码
    @Column(unique = true)
    private String code;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public int getAmount() {
        return amount;
    }

    public void setAmount(int amount) {
        this.amount = amount;
    }

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }
}
dao层,注意一下sql语句,where条件中的amount - count >= 0是关键,该语句能严格保证不超卖。

package com.tianyalei.repository;

import com.tianyalei.model.GoodInfo;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;

/**
 * Created by admin on 17/7/5.
 */
public interface GoodInfoRepository extends CrudRepository<GoodInfo, Integer> {
    @Query("update GoodInfo set amount = amount - ?2 where code = ?1 and amount - ?2 >= 0")
    @Modifying
    int updateAmount(String code, int count);
}

service接口

package com.tianyalei.service;

import com.tianyalei.model.GoodInfo;

/**
 * Created by wuwf on 17/7/5.
 */
public interface GoodInfoService {
    void add(GoodInfo goodInfo);

    void delete(GoodInfo goodInfo);

    int update(String code, int count);
}

Service实现类

package com.tianyalei.service;

import com.tianyalei.model.GoodInfo;
import com.tianyalei.repository.GoodInfoRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * Created by wuwf on 17/7/5.
 */
@Service("db")
public class GoodInfoDbService implements GoodInfoService {

    @Autowired
    private GoodInfoRepository goodInfoRepository;

    @Transactional
    public int update(String code, int count) {
        return goodInfoRepository.updateAmount(code, count);
    }

    public void add(GoodInfo goodInfo) {
        goodInfoRepository.save(goodInfo);
    }

    public void delete(GoodInfo goodInfo) {
        goodInfoRepository.deleteAll();
    }

}
yml配置文件
spring:
  jpa:
    database: mysql
    show-sql: true
    hibernate:
      ddl-auto: update
  datasource:
      type: com.alibaba.druid.pool.DruidDataSource
      driver-class-name: com.mysql.jdbc.Driver
      url: jdbc:mysql://localhost:3306/test
      username: root
      password:
  redis:
        host: localhost
        port: 6379
        password:
        pool:
          max-active: 8
          max-idle: 8
          min-idle: 0
          max-wait: 10000
  profiles:
    active: dev
server:
  port: 8080
以上即是基本配置。

二 模拟并发访问抢购

新建junit测试类
package com.tianyalei;

import com.tianyalei.model.GoodInfo;
import com.tianyalei.service.GoodInfoService;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;

/**
 * Created by wuwf on 17/7/5.
 */
@RunWith(SpringRunner.class)
@SpringBootTest
public class MyTest {

    @Resource(name = "db")
    private GoodInfoService service;

    private String goodCode = "iphone7";
    /**
     * 机器总数量
     */
    private int goodAmount = 100;
    /**
     * 并发量
     */
    private int threadNum = 200;

    //销售量
    private int goodSale = 0;

    //买成功的数量
    private int accountNum = 0;
    //买成功的人的ID集合
    private List<Integer> successUsers = new ArrayList<>();

    private GoodInfo goodInfo;

    /*当创建 CountDownLatch 对象时,对象使用构造函数的参数来初始化内部计数器。每次调用 countDown() 方法,
     CountDownLatch 对象内部计数器减一。当内部计数器达到0时, CountDownLatch 对象唤醒全部使用 await() 方法睡眠的线程们。*/
    private CountDownLatch countDownLatch = new CountDownLatch(threadNum);

    @Test
    public void contextLoads() {
        for (int i = 0; i < threadNum; i++) {
            new Thread(new UserRequest(goodCode, 7, i)).start();
            countDownLatch.countDown();
        }

        //让主线程等待200个线程执行完,休息2秒,不休息的话200条线程还没执行完,就打印了
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("-----------购买成功的用户数量----------为" + accountNum);
        System.out.println("-----------销售量--------------------为" + goodSale);
        System.out.println("-----------剩余数量------------------为" + (goodAmount - goodSale));
        System.out.println(successUsers);
    }

    private class UserRequest implements Runnable {

        private String code;
        private int buyCount;
        private int userId;

        public UserRequest(String code, int buyCount, int userId) {
            this.code = code;
            this.buyCount = buyCount;
            this.userId = userId;
        }

        @Override
        public void run() {
            try {
                //让线程等待,等200个线程创建完一起执行
                countDownLatch.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //如果更新数据库成功,也就代表购买成功了
            if (service.update(code, buyCount) > 0) {
                //对service加锁,因为很多线程在访问同一个service对象,不加锁将导致购买成功的人数少于预期,且数量不对,可自行测试
                synchronized (service) {
                    //销售量
                    goodSale += buyCount;
                    accountNum++;
                    //收录购买成功的人
                    successUsers.add(userId);
                }
            }
        }
    }


    @Before
    public void add() {
        goodInfo = new GoodInfo();
        goodInfo.setCode(goodCode);
        goodInfo.setAmount(goodAmount);
        service.add(goodInfo);
    }

    @After
    public void delete() {
        service.delete(goodInfo);
    }

}

注意,由于是模拟并发,需要保证200个线程同时启动去访问数据库,所以使用了CountDownLatch类,在调用UserRequest线程的start方法后,会先进入await状态,等待200个线程创建完毕后,一起执行。

注意,由于是多线程操作service,必然导致数据不同步,所以需要对service加synchronize锁,来保证service的update方法能够正确执行。如果不加,可以自行测试,会导致少卖。
运行该测试类,看打印的结果。



可以多次运行,并修改每个人的购买数量、总商品数量、线程数,看看结果是否正确。

如修改为每人购买8个

mysql支持的并发访问量有限,倘若并发量较小,可以采用上面的update的sql就能控制住,倘若量大,可以考虑使用nosql。

下一篇讲一下redis模拟的方式。

java实现秒杀系统@Controller @RequestMapping("seckill")//url:/模块/资源/{id}/细分 /seckill/list public class SeckillController { private final Logger logger = LoggerFactory.getLogger(this.getClass()); @Autowired private SeckillService seckillService; @RequestMapping(value="/list",method = RequestMethod.GET) public String list(Model model){ //获取列表页 List list=seckillService.getSeckillList(); model.addAttribute("list",list); //list.jsp+model = ModelAndView return "list";//WEB-INF/jsp/"list".jsp } @RequestMapping(value = "/{seckillId}/detail",method = RequestMethod.GET) public String detail(@PathVariable("seckillId") Long seckillId, Model model){ if (seckillId == null){ return "redirect:/seckill/list"; } Seckill seckill = seckillService.getById(seckillId); if (seckill == null){ return "forward:/seckill/list"; } model.addAttribute("seckill",seckill); return "detail"; } //ajax json @RequestMapping(value = "/{seckillId}/exposer", method = RequestMethod.POST, produces = {"application/json;charset=UTF-8"}) @ResponseBody public SeckillResult exposer(@PathVariable("seckillId") Long seckillId){ SeckillResult result; try { Exposer exposer =seckillService.exportSeckillUrl(seckillId); result = new SeckillResult(true,exposer); } catch (Exception e) { logger.error(e.getMessage(),e); result = new SeckillResult(false,e.getMessage()); } return result; } @RequestMapping(value = "/{seckillId}/{md5}/execution", method = RequestMethod.POST, produces = {"application/json;charset=UTF-8"} ) @ResponseBody public SeckillResult execute(@PathVariable("seckillId")Long seckillId,
<think>好的,用户想了解Redis如何实现队列应对秒杀的具体代码。首先,我需要确认秒杀场景的核心问题:高并发下的超卖和系统崩溃。Redis的队列机制,特别是利用List结构,可以有效地处理这些挑战。 用户可能已经了解秒杀的基本概念,但具体实现细节可能不清楚。我需要分步骤解释,确保逻辑清晰。首先,用List存储商品库存,使用lpush和rpop命令模拟队列。预加载库存是关键,避免在秒杀时频繁操作数据库。 然后,用户可能关心如何避免超卖。Redis的单线程特性确保了原子性操作,但每个rpop需要对应一个真实的库存减少。这里可能需要Lua脚本保证原子性,或者在代码中处理。需要提到使用rpop的阻塞方式,或者brpop来处理并发请求。 接下来,用户可能需要异步处理订单,比如将成功抢购的用户存入另一个队列,然后后台处理。这部分需要展示如何将用户ID和商品ID存入另一个List,以及后台Worker的使用。 此外,用户可能没有考虑库存预热和超时处理。比如,预先生成库存到队列,设置订单的有效期,防止库存被占用但未支付的情况。需要提到定时任务检查超时订单,并恢复库存。 还要考虑分布式环境下的问题,比如多个消费者实例的情况,Redis的可靠性,以及是否需要持久化。不过为了简化,可能先不深入这些细节,保持代码示例简单明了。 最后,需要提醒用户这只是基础实现,实际生产环境需要更多的优化,比如连接池、错误处理、日志记录,以及可能的限流和降级措施。可能用户需要进一步学习更高级的技术,比如Redis事务或者分布式锁,但当前示例应保持简洁。 总结回答结构:先介绍基本原理,分步骤说明代码实现,包括库存预热、秒杀逻辑、订单处理,然后给出注意事项和优化建议。确保代码示例正确,使用中文注释,方便用户理解。</think>Redis 使用 List 结构实现队列应对秒杀场景的核心思路如下(Python示例): ```python import redis # 连接Redis r = redis.Redis(host='localhost', port=6379, db=0) # 商品库存预热(提前执行) def init_stock(item_id, total): key = f"item_{item_id}_stock" r.delete(key) # 清空旧数据 # 用数字字符串模拟商品ID(实际可存JSON) for i in range(total): r.lpush(key, f"prod_{i}") # 左进 # 秒杀请求处理 def seckill(user_id, item_id): stock_key = f"item_{item_id}_stock" order_key = f"item_{item_id}_orders" # 从队列右侧弹出商品(原子操作) product = r.rpop(stock_key) if not product: return {"status": "fail", "msg": "已售罄"} # 记录订单(右进保持顺序) r.rpush(order_key, f"{user_id}:{product.decode()}") return {"status": "success", "data": product.decode()} # ----------- 使用示例 ----------- if __name__ == "__main__": # 初始化100个库存(实际应提前执行) init_stock("1001", 100) # 模拟并发请求(实际应使用异步框架) print(seckill("user_9527", "1001")) # 成功 print(seckill("user_1122", "1001")) # 成功 # ... 当第101次请求时返回已售罄 ``` 核心实现要点: 1. **库存预热** - 使用 `lpush` 预加载商品ID到List - 每个元素代表一个库存单位(避免超卖) 2. **原子化操作** - `rpop` 是原子性操作,确保并发安全 - 生产环境建议使用Lua脚本保证组合操作原子性: ```lua local stock = redis.call('RPOP', KEYS[1]) if stock then redis.call('RPUSH', KEYS[2], ARGV[1]..":"..stock) end return stock ``` 3. **订单处理** - 成功获取库存后记录订单信息 - 实际应异步处理订单(支付、库存扣减等) 4. **注意事项** ```python # 需要处理的问题: # 1. 超时订单回滚(需配合定时任务) def revert_stock(item_id, order_info): stock_key = f"item_{item_id}_stock" r.lpush(stock_key, order_info.split(":")[1]) # 左端回退库存 # 2. 分布式锁(极端情况保障) # 3. 队列监控(llen获取剩余库存) ``` 优化方向: 1. 使用 `brpop` 实现阻塞队列,降低轮询压力 2. 拆分订单处理队列和工作线程(多消费者模式) 3. 结合令牌桶算法进行限流(防止系统过载) 4. 使用Redis集群提升吞吐量(分片存储不同商品队列) 实际生产环境还需配合: - 服务端限流(Nginx层) - 前端防重复提交(按钮置灰) - 库存预热验证(保证队列数量正确) - 失败重试机制(网络异常处理)
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

天涯泪小武

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值