1.正常电子商务流程 (1)查询商品;(2)建立订单;(3)扣减库存;(4)更新订单;(5)付款;(6)卖家发货 java
2.秒杀业务特性流程 ( 1)低廉价格;(2)大幅推广;(3)瞬时售空;(4)通常是定时上架;(5)时间短、瞬时并发量高;mysql
3.秒杀实现技术挑战web
(1)秒杀技术挑战 假设某网站秒杀活动只推出一件商品,预计会吸引1万人参加活动,也就说最大并发请求数是10000,秒杀系统须要面对的技术挑战有: redis
对现有网站业务形成冲击 秒杀活动只是网站营销的一个附加活动,这个活动具备时间短,并发访问量大的特色,若是和网站原有应用部署在一块儿,必然会对现有业务形成冲击,spring
稍有不慎可能致使整个网站瘫痪。 解决方案:将秒杀系统独立部署,甚至使用独立域名,使其与网站彻底隔离。sql
在高并发状况下,若是忽然有10万个不一样用户的请求进行秒杀,可是商品的库存数量只有100个,那么这时候可能会出现10个请求执行修改秒杀库存sql语句,这时候可能会出现数据库访问压力承受不了?数据库
-秒杀抢购修改库存如何减小数据库IO操做 数据库分表分库、读写分离、使用redis缓存减去数据库访问压力 apache
很是靠谱的秒杀方案 基于MQ+库存令牌桶实现 同时有10万个请求实现秒杀、商品库存只有100个 实现只须要修改库存100次就能够了 json
方案实现流程:提早对应的商品库存生成好对应令牌(100个令牌),在10万个请求中,只要谁可以获取到令牌谁就可以秒杀成功, 获取到秒杀令牌后,在使用mq异步实现修改减去库存。缓存
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='秒杀库存表';
/** * 秒杀实体 */ @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; }
@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); } }
@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); }
/** * 库存超卖 */ @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; } }
@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; } }
@SpringBootApplication public class SpikeBootStrap { public static void main(String[] args) { SpringApplication.run(SpikeBootStrap.class); } }
<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>
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
/** * 生产者发送消息 */ @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); } } }
/** * 基于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); } }
// 采用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); }
/** * 消费者 */ @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; } }
/** * 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"); } }