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"); } }