电商秒杀方法
电商秒杀方法
一、秒杀业务分析
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"); } }