Dict.CN 在线词典, 英语学习, 在线翻译 ------------- MyGitee 朱秋贵内科诊所 My腾云code

电商秒杀方法

电商秒杀方法

 

一、秒杀业务分析

1.正常电子商务流程 (1)查询商品;(2)创建订单;(3)扣减库存;(4)更新订单;(5)付款;(6)卖家发货

2.秒杀业务特性流程 ( 1)低廉价格;(2)大幅推广;(3)瞬时售空;(4)一般是定时上架;(5)时间短、瞬时并发量高;

3.秒杀实现技术挑战

(1)秒杀技术挑战 假设某网站秒杀活动只推出一件商品,预计会吸引1万人参加活动,也就说最大并发请求数是10000,秒杀系统需要面对的技术挑战有:

          对现有网站业务造成冲击 秒杀活动只是网站营销的一个附加活动,这个活动具有时间短,并发访问量大的特点,如果和网站原有应用部署在一起,必然会对现有业务造成冲击,

         稍有不慎可能导致整个网站瘫痪。 解决方案:将秒杀系统独立部署,甚至使用独立域名,使其与网站完全隔离。

二、秒杀抢购修改库存如何减少数据库IO操作

在高并发情况下,如果突然有10万个不同用户的请求进行秒杀,但是商品的库存数量只有100个,那么这时候可能会出现10个请求执行修改秒杀库存sql语句,这时候可能会出现数据库访问压力承受不了?

-秒杀抢购修改库存如何减少数据库IO操作 数据库分表分库、读写分离、使用redis缓存减去数据库访问压力

非常靠谱的秒杀方案 基于MQ+库存令牌桶实现 同时有10万个请求实现秒杀、商品库存只有100个 实现只需要修改库存100次就可以了

方案实现流程:提前对应的商品库存生成好对应令牌(100个令牌),在10万个请求中,只要谁能够获取到令牌谁就能够秒杀成功, 获取到秒杀令牌后,在使用mq异步实现修改减去库存。

三、使用数据库乐观锁实现防止超卖问题

1、数据库表结构

复制代码
CREATE TABLE `meite_order` (
  `seckill_id` bigint(20) NOT NULL COMMENT '秒杀商品id',
  `user_phone` bigint(20) NOT NULL COMMENT '用户手机号',
  `state` tinyint(4) NOT NULL DEFAULT '-1' COMMENT '状态标示:-1:无效 0:成功 1:已付款 2:已发货',
  `create_time` datetime NOT NULL COMMENT '创建时间',
  KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='秒杀成功明细表';

CREATE TABLE `meite_seckill` (
  `seckill_id` bigint(20) NOT NULL COMMENT '商品库存id',
  `name` varchar(120) CHARACTER SET utf8 NOT NULL COMMENT '商品名称',
  `inventory` int(11) NOT NULL COMMENT '库存数量',
  `start_time` datetime NOT NULL COMMENT '秒杀开启时间',
  `end_time` datetime NOT NULL COMMENT '秒杀结束时间',
  `create_time` datetime NOT NULL COMMENT '创建时间',
  `version` bigint(20) NOT NULL DEFAULT '0',
  PRIMARY KEY (`seckill_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='秒杀库存表';
复制代码

2、实体类

复制代码
/**
 * 秒杀实体
 */
@Data
public class SeckillEntity {

    private long seckillId;
    //商品名称
    private String name;
    //库存数量
    private Integer inventory;
    //秒杀开启时间
    private Date startTime;
    //秒杀结束时间
    private Date endTime;
    //创建时间
    private Date createTime;
    //版本号
    private Long version;
}
/**
 * 订单实体
 */
@Data
public class OrderEntity {
    //秒杀商品ID
    private Long seckillId;
    //用户手机号
    private String userPhone;
    //状态
    private Integer state;
    //创建时间
    private Date createTime;
}
复制代码

3、工具类

复制代码
@Component
public class RedisUtil {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    // 如果key存在的话返回fasle 不存在的话返回true
    public Boolean setNx(String key, String value, Long timeout) {
        Boolean setIfAbsent = stringRedisTemplate.opsForValue().setIfAbsent(key, value);
        if (timeout != null) {
            stringRedisTemplate.expire(key, timeout, TimeUnit.SECONDS);
        }
        return setIfAbsent;
    }

    public StringRedisTemplate getStringRedisTemplate() {
        return stringRedisTemplate;
    }

    public void setList(String key, List<String> listToken) {
        stringRedisTemplate.opsForList().leftPushAll(key, listToken);
    }

    /**
     * 存放string类型
     * 
     * @param key
     *            key
     * @param data
     *            数据
     * @param timeout
     *            超时间
     */
    public void setString(String key, String data, Long timeout) {
        try {

            stringRedisTemplate.opsForValue().set(key, data);
            if (timeout != null) {
                stringRedisTemplate.expire(key, timeout, TimeUnit.SECONDS);
            }

        } catch (Exception e) {

        }

    }

    /**
     * 开启Redis 事务
     * 
     * @param isTransaction
     */
    public void begin() {
        // 开启Redis 事务权限
        stringRedisTemplate.setEnableTransactionSupport(true);
        // 开启事务
        stringRedisTemplate.multi();

    }

    /**
     * 提交事务
     * 
     * @param isTransaction
     */
    public void exec() {
        // 成功提交事务
        stringRedisTemplate.exec();
    }

    /**
     * 回滚Redis 事务
     */
    public void discard() {
        stringRedisTemplate.discard();
    }

    /**
     * 存放string类型
     * 
     * @param key
     *            key
     * @param data
     *            数据
     */
    public void setString(String key, String data) {
        setString(key, data, null);
    }

    /**
     * 根据key查询string类型
     * 
     * @param key
     * @return
     */
    public String getString(String key) {
        String value = stringRedisTemplate.opsForValue().get(key);
        return value;
    }

    /**
     * 根据对应的key删除key
     * 
     * @param key
     */
    public Boolean delKey(String key) {
        return stringRedisTemplate.delete(key);

    }
}
复制代码
复制代码
@Component
public class GenerateToken {
    @Autowired
    private RedisUtil redisUtil;

    /**
     * 生成令牌
     * 
     * @param prefix
     *            令牌key前缀
     * @param redisValue
     *            redis存放的值
     * @return 返回token
     */
    public String createToken(String keyPrefix, String redisValue) {
        return createToken(keyPrefix, redisValue, null);
    }

    /**
     * 生成令牌
     * 
     * @param prefix
     *            令牌key前缀
     * @param redisValue
     *            redis存放的值
     * @param time
     *            有效期
     * @return 返回token
     */
    public String createToken(String keyPrefix, String redisValue, Long time) {
        if (StringUtils.isEmpty(redisValue)) {
            new Exception("redisValue Not nul");
        }
        String token = keyPrefix + UUID.randomUUID().toString().replace("-", "");
        redisUtil.setString(token, redisValue, time);
        return token;
    }

    public void createListToken(String keyPrefix, String redisKey, Long tokenQuantity) {
        List<String> listToken = getListToken(keyPrefix, tokenQuantity);
        redisUtil.setList(redisKey, listToken);
    }

    public List<String> getListToken(String keyPrefix, Long tokenQuantity) {
        List<String> listToken = new ArrayList<>();
        for (int i = 0; i < tokenQuantity; i++) {
            String token = keyPrefix + UUID.randomUUID().toString().replace("-", "");
            listToken.add(token);
        }
        return listToken;

    }

    public String getListKeyToken(String key) {
        String value = redisUtil.getStringRedisTemplate().opsForList().leftPop(key);
        return value;
    }

    /**
     * 根据token获取redis中的value值
     * 
     * @param token
     * @return
     */
    public String getToken(String token) {
        if (StringUtils.isEmpty(token)) {
            return null;
        }
        String value = redisUtil.getString(token);
        return value;
    }

    /**
     * 移除token
     * 
     * @param token
     * @return
     */
    public Boolean removeToken(String token) {
        if (StringUtils.isEmpty(token)) {
            return null;
        }
        return redisUtil.delKey(token);

    }

}
复制代码

 4、mapper类

复制代码
@Mapper
public interface SeckillMapper {


    /**
     * 基于版本号形式实现乐观锁
     *
     * @param seckillId
     * @return
     */
    @Update("update meite_seckill set inventory=inventory-1 ,version=version+1 where  seckill_id=#{seckillId} and version=#{version} and inventory>0;")
    int optimisticVersionSeckill(@Param("seckillId") Long seckillId, @Param("version") Long version);


    /**
     * 查询秒杀订单
     * @param seckillId
     * @return
     */
    @Select("SELECT seckill_id AS seckillId,name as name,inventory as inventory,start_time as startTime,end_time as endTime,create_time as createTime,version as version from meite_seckill where seckill_id=#{seckillId}")
    SeckillEntity findBySeckillId(Long seckillId);

    /**
     * 插入秒杀订单
     * @param orderEntity
     * @return
     */
    @Insert("INSERT INTO `meite_order` VALUES (#{seckillId},#{userPhone}, '1', now());")
    int insertOrder(OrderEntity orderEntity);
}
复制代码

5、service类

复制代码
/**
 * 库存超卖
 */
@Service
public class SpikeCommodityService {

    @Autowired
    private SeckillMapper seckillMapper;


    @Autowired
    private RedisUtil redisUtil;

    @Transactional
    public JSONObject spike(String phone, Long seckillId) {
        JSONObject jsonObject = new JSONObject();
        // 1.验证参数
        if (StringUtils.isEmpty(phone)) {
            jsonObject.put("error","手机号码不能为空!");
            return jsonObject;
        }
        if (seckillId == null) {
            jsonObject.put("error","库存id不能为空!");
            return jsonObject;
        }
        // >>>限制用户访问频率 比如10秒中只能访问一次
        Boolean resultNx = redisUtil.setNx(phone, seckillId + "", 10l);
        if (!resultNx) {
            jsonObject.put("error","该用户操作过于频繁,请稍后重试!");
            return jsonObject;
        }
        // 2.根据库存id查询商品是否存在
        SeckillEntity seckillEntity = seckillMapper.findBySeckillId(seckillId);
        if (seckillEntity == null) {
            jsonObject.put("error","该商品信息不存在!");
            return jsonObject;
        }
        // 3.对库存的数量实现减去1
        Long version = seckillEntity.getVersion();
        int inventoryDeduction = seckillMapper.optimisticVersionSeckill(seckillId, version);
        if (inventoryDeduction<=0) {
            jsonObject.put("error","秒杀失败");
            return jsonObject;
        }
        // 4.添加秒杀成功订单
        OrderEntity orderEntity = new OrderEntity();
        orderEntity.setSeckillId(seckillId);
        orderEntity.setUserPhone(phone);
        int insertOrder = seckillMapper.insertOrder(orderEntity);
        if (insertOrder<=0) {
            jsonObject.put("success","恭喜你,秒杀成功!");
            return jsonObject;
        }
        jsonObject.put("error","秒杀失败");
        return jsonObject;
    }
}
复制代码

6、controller类

复制代码
@RestController
public class SpikeCommodityController {

    @Autowired
    private SpikeCommodityService spikeCommodityService;

    @RequestMapping("/spike")
    public JSONObject spike(String phone, Long seckillId){
        JSONObject jsonObject = spikeCommodityService.spike(phone,seckillId);
        return jsonObject;
    }
}
复制代码

7、启动类

复制代码
@SpringBootApplication
public class SpikeBootStrap {
    public static void main(String[] args) {
        SpringApplication.run(SpikeBootStrap.class);
    }
}
复制代码

8、pom文件

复制代码
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.6.RELEASE</version>
    </parent>
    <properties>
        <mybatis-spring-boot.version>1.3.1</mybatis-spring-boot.version>
        <mybatis.version>3.4.5</mybatis.version>
    </properties>
    <dependencies>
        <!-- 集成commons工具类 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>

        <!-- 集成lombok 框架 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!-- fastjson -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.30</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--mybatis起步依赖-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.2</version>
        </dependency>
        <!--Mysql-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.0.7</version>
        </dependency>
        <!-- 添加springboot对amqp的支持 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
        <!-- redis缓存 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
    </dependencies>
复制代码

9、yml文件

复制代码
server:
  port: 9800

spring:
  application:
    name: app-mayikt-spike
  redis:
    host: 127.0.0.1
   # password: 123456
    port: 6379
    pool:
      max-idle: 100
      min-idle: 1
      max-active: 1000
      max-wait: -1
###数据库相关连接      
  datasource:
    username: root
    password: root
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/meite_spike
复制代码

四、秒杀服务基于库存令牌桶实现修改商品库存

1、生产者

复制代码
/**
 * 生产者发送消息
 */
@Component
public class SpikeCommodityProducer implements RabbitTemplate.ConfirmCallback {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Transactional
    public void send(JSONObject jsonObject){
        String jsonString = jsonObject.toJSONString();
        String messAgeId = UUID.randomUUID().toString().replace("-", "");
        MessageBuilder.withBody(jsonString.getBytes())
                .setContentType(MessageProperties.CONTENT_TYPE_JSON)
                .setContentEncoding("utf-8")
                .setMessageId(messAgeId);
        //构造参数
        this.rabbitTemplate.setMandatory(true);
        this.rabbitTemplate.setConfirmCallback(this);
    }

    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        //获取id
        String messageId = correlationData.getId();
        JSONObject jsonObject = JSONObject.parseObject(messageId);
        if (ack){
            System.out.println("消费成功");
        }else{
            //重试机制调用
            send(jsonObject);
        }
    }
}
复制代码

2、service类

复制代码
/**
 * 基于mq实现库存
 */
@Component
public class SpikeCommodity {
    @Autowired
    private SeckillMapper seckillMapper;
    @Autowired
    private RedisUtil redisUtil;

    @Autowired
    private GenerateToken generateToken;
    @Autowired
    private SpikeCommodityProducer spikeCommodityProducer;

    @Transactional
    public JSONObject getOrder(String phone, Long seckillId) {
        JSONObject jsonObject = new JSONObject();
        // 1.验证参数
        if (StringUtils.isEmpty(phone)) {
            jsonObject.put("error","手机号码不能为空!");
            return jsonObject;
        }
        if (seckillId == null) {
            jsonObject.put("error","库存id不能为空!");
            return jsonObject;
        }
        // 2.从redis从获取对应的秒杀token
        String seckillToken = generateToken.getListKeyToken(seckillId + "");
        if (StringUtils.isEmpty(seckillToken)) {
            return null;
        }
        // 3.获取到秒杀token之后,异步放入mq中实现修改商品的库存
        sendSeckillMsg(seckillId, phone);
        return jsonObject;
    }

    @Async
    public void sendSeckillMsg(Long seckillId, String phone) {
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("seckillId",seckillId);
        jsonObject.put("phone",phone);
        spikeCommodityProducer.send(jsonObject);
    }
}
复制代码

3、创建token

复制代码
 // 采用redis数据库类型为 list类型 key为 商品库存id list 多个秒杀token

    public String addSpikeToken(Long seckillId, Long tokenQuantity) {
        // 1.验证参数
        if (seckillId == null) {
            return "商品库存id不能为空!";
        }
        if (tokenQuantity == null) {
            return "token数量不能为空!";
        }
        SeckillEntity seckillEntity = seckillMapper.findBySeckillId(seckillId);
        if (seckillEntity == null) {
            return "商品信息不存在!";
        }
        // 2.使用多线程异步生产令牌
        createSeckillToken(seckillId, tokenQuantity);
        return "令牌正在生成中.....";
    }

    @Async
    public void createSeckillToken(Long seckillId, Long tokenQuantity) {
        generateToken.createListToken("seckill_", seckillId + "", tokenQuantity);
    }
复制代码

4、消费者

复制代码
/**
 * 消费者
 */
@Component
public class StockConsumer {
    @Autowired
    private SeckillMapper seckillMapper;

    @RabbitListener(queues = {"modify_inventory_queue"})
    public void process(Message message, Channel channel) throws UnsupportedEncodingException {
        String messageId = message.getMessageProperties().getMessageId();
        String msg = new String(message.getBody(), "UTF-8");
        JSONObject jsonObject = JSONObject.parseObject(msg);
        // 1.获取秒杀id
        Long seckillId = jsonObject.getLong("seckillId");
        SeckillEntity seckillEntity = seckillMapper.findBySeckillId(seckillId);
        if (seckillEntity == null) {
            return;
        }
        Long version = seckillEntity.getVersion();
        int inventoryDeduction = seckillMapper.optimisticVersionSeckill(seckillId, version);
        if (!toDaoResult(inventoryDeduction)) {
            return;
        }
        // 2.添加秒杀订单
        OrderEntity orderEntity = new OrderEntity();
        String phone = jsonObject.getString("phone");
        orderEntity.setUserPhone(phone);
        orderEntity.setSeckillId(seckillId);
        orderEntity.setState((int) 1l);
        int insertOrder = seckillMapper.insertOrder(orderEntity);
        if (!toDaoResult(insertOrder)) {
            return;
        }
    }

    // 调用数据库层判断
    public Boolean toDaoResult(int result) {
        return result > 0 ? true : false;
    }

}
复制代码

5、MQ配置类

复制代码
/**
 * rabbitMq配置类
 */
@Configuration
public class RabbitMqConfig {
    // 添加修改库存队列
    public static final String MODIFY_INVENTORY_QUEUE = "modify_inventory_queue";
    // 交换机名称
    private static final String MODIFY_EXCHANGE_NAME = "modify_exchange_name";

    // 1.添加交换机队列
    @Bean
    public Queue directModifyInventoryQueue() {
        return new Queue(MODIFY_INVENTORY_QUEUE);
    }

    // 2.定义交换机
    @Bean
    DirectExchange directModifyExchange() {
        return new DirectExchange(MODIFY_EXCHANGE_NAME);
    }

    // 3.修改库存队列绑定交换机
    @Bean
    Binding bindingExchangeintegralDicQueue() {
        return BindingBuilder.bind(directModifyInventoryQueue()).to(directModifyExchange()).with("modifyRoutingKey");
    }

}
复制代码

 

 

posted @ 2021-03-10 15:50  cn2024  阅读(389)  评论(0编辑  收藏  举报