Java-秒殺系統的設計
阿新 • • 發佈:2018-11-19
Java-秒殺系統的設計
1 緣起
經常看到 某寶, 某東, 還有各種平臺的秒殺 活動, 覺得很想學習一下秒殺技術,也順便學習在 在高併發下系統的設計,於是學習了慕課網的秒殺教程。 這裡寫部落格記錄一下。
2 思路 & 實現
2.1 資料庫
因為秒殺商品的經常變動所以設計了
- 秒殺商品表
CREATE TABLE `miaosha_goods` (
`id` bigint(20) NOT NULL,
`goods_id` bigint(20) NOT NULL,
`miaosha_price` decimal(10,2) NOT NULL,
`stock_count` int(11) NOT NULL,
`start_date` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP,
`end_date` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
- 秒殺訂單表
CREATE TABLE `miaosha_order` (
`id` bigint(20) NOT NULL,
`user_id` bigint(20) NOT NULL,
`order_id` bigint(20) NOT NULL,
`goods_id` bigint(20) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
2.2 前端
2.2.1 前後端分離
這裡不推薦後端模板引擎的原因是因為 模板需要後端渲染 , 生成頁面,即使有快取服務端的壓力也過大。
2.2.2 儘量的快取前端 頁面,壓縮js
主要手段:1 cdn , 2 nginx 的快取,3 使用 壓縮後的js ,4 開啟 g-zip
2.3 服務端介面
2.3.1 物件快取
通過 redis 快取秒殺商品列表頁, 和詳情頁的資料 ,查詢出資料後交給前端模板引擎渲染
2.3.2 redis 預讀庫存 (重要)
- 1 啟動啟動的時候 將需要秒殺商品的庫存讀入 redis 快取,並且放入 秒殺是否結束的標誌
1 實現 InitializingBean 介面 重寫 afterPropertiesSet 方法(在預設構造方法執行完之後執行)
/**
* 系統初始化
* */
@Override
public void afterPropertiesSet() throws Exception {
List<GoodsVo> goodsList = goodsService.listGoodsVo();
if(goodsList == null) {
return;
}
for(GoodsVo goods : goodsList) {
redisService.set(GoodsKey.getMiaoshaGoodsStock, ""+goods.getId(), goods.getStockCount());
// 如果是分散式系統 可以交給 redis 去做
localOverMap.put(goods.getId(), false);
}
}
- 2 使用者下單的時候 decr 減少 redis 中的庫存(原子性操作) , 如果庫存不足,則返回秒殺失敗
2.3.3 使用 rabbitMq 進行非同步下單
1 構建秒殺訊息並且通過 mq 傳送, 然後同步返回 排隊中
@Autowired
MQSender sender;
... 省略業務程式碼
MiaoshaMessage mm = new MiaoshaMessage();
mm.setUser(user);
mm.setGoodsId(goodsId);
sender.sendMiaoshaMessage(mm);
return Result.success(0);//排隊中
1.1 傳送者的簡單實現
@Service
public class MQSender {
@Autowired
AmqpTemplate amqpTemplate ;
public void sendMiaoshaMessage(MiaoshaMessage mm) {
String msg = RedisService.beanToString(mm);
log.info("send message:"+msg);
amqpTemplate.convertAndSend(MQConfig.MIAOSHA_QUEUE, msg);
}
}
2 使用 定義 reciver 處理訊息(監聽指定佇列, 接收訊息)
@RabbitListener(queues=MQConfig.MIAOSHA_QUEUE)
public void receive(String message) {
log.info("receive message:"+message);
MiaoshaMessage mm = RedisService.stringToBean(message, MiaoshaMessage.class);
MiaoshaUser user = mm.getUser();
long goodsId = mm.getGoodsId();
GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId);
int stock = goods.getStockCount();
if(stock <= 0) {
return;
}
//判斷是否已經秒殺到了
MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId);
if(order != null) {
return;
}
//減庫存 下訂單 寫入秒殺訂單
miaoshaService.miaosha(user, goods);
}
3 使用 事物 保證 秒殺操作的資料一致性
這裡預設的隔離級別,使用了 行鎖(獨佔鎖) 保證庫存不會賣超
@Transactional
public OrderInfo miaosha(MiaoshaUser user, GoodsVo goods) {
//減庫存 下訂單 寫入秒殺訂單,
boolean success = goodsService.reduceStock(goods);
if(success) {
//order_info maiosha_order
return orderService.createOrder(user, goods);
}else {
setGoodsOver(goods.getId());
return null;
}
}
4 客戶端的處理
客戶端 接收到秒殺介面的返回後,判斷是否成功, 如果失敗 直接提示給使用者 秒殺失敗 如果返回排隊中, 則呼叫查詢介面查詢秒殺結果
public long getMiaoshaResult(Long userId, long goodsId) {
// 通過 redis 查詢 該使用者是否秒殺了指定產品
MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(userId, goodsId);
if(order != null) {//秒殺成功
return order.getOrderId();
}else {
// 獲取該商品是否秒殺完的記憶體標記, 建議使用 redis
boolean isOver = getGoodsOver(goodsId);
if(isOver) {
return -1;
}else {
// 返回處理中 客戶端隔一段時間以後繼續發起查詢
return 0;
}
}
}
2.4 其他優化手段
2.4.1 秒殺驗證碼
通過驗證碼 可以有效分散使用者請求,大大降低系統瞬間的併發, 大概思路就是 建立一個驗證碼圖片,寫給客戶端,並且在服務端儲存結果
@RequestMapping(value="/verifyCode", method=RequestMethod.GET)
@ResponseBody
public Result<String> getMiaoshaVerifyCod(HttpServletResponse response,MiaoshaUser user,
@RequestParam("goodsId")long goodsId) {
if(user == null) {
return Result.error(CodeMsg.SESSION_ERROR);
}
try {
BufferedImage image = miaoshaService.createVerifyCode(user, goodsId);
OutputStream out = response.getOutputStream();
ImageIO.write(image, "JPEG", out);
out.flush();
out.close();
return null;
}catch(Exception e) {
e.printStackTrace();
return Result.error(CodeMsg.MIAOSHA_FAIL);
}
}
2.4.2 隱藏秒殺地址
通過動態的秒殺地址,並且在商品開始秒殺之前 無法獲取, 增加別人的破解難度
@AccessLimit(seconds=5, maxCount=5, needLogin=true)
@RequestMapping(value="/path", method=RequestMethod.GET)
@ResponseBody
public Result<String> getMiaoshaPath(HttpServletRequest request, MiaoshaUser user,
@RequestParam("goodsId")long goodsId,
@RequestParam(value="verifyCode", defaultValue="0")int verifyCode
) {
if(user == null) {
return Result.error(CodeMsg.SESSION_ERROR);
}
boolean check = miaoshaService.checkVerifyCode(user, goodsId, verifyCode);
if(!check) {
return Result.error(CodeMsg.REQUEST_ILLEGAL);
}
String path =miaoshaService.createMiaoshaPath(user, goodsId);
return Result.success(path);
}
2.4.3 通過自定義註解限流
- 1 定義註解 @AccessLimit(seconds=5, maxCount=5, needLogin=true)
@Retention(RUNTIME)
@Target(METHOD)
public @interface AccessLimit {
int seconds();
int maxCount();
boolean needLogin() default true;
}
-2 通過 ThreadLocal 來保證每一個執行緒 持有一個秒殺物件
public class UserContext {
private static ThreadLocal<MiaoshaUser> userHolder = new ThreadLocal<MiaoshaUser>();
public static void setUser(MiaoshaUser user) {
userHolder.set(user);
}
public static MiaoshaUser getUser() {
return userHolder.get();
}
}
- 3 通過 HandlerInterceptorAdapter 實現介面的限流
大概思路 通過方法攔截,獲取方法上面的自定義註解, 然後 根據業務邏輯 自定義限流規則
@Service
public class AccessInterceptor extends HandlerInterceptorAdapter{
@Autowired
MiaoshaUserService userService;
@Autowired
RedisService redisService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
if(handler instanceof HandlerMethod) {
MiaoshaUser user = getUser(request, response);
UserContext.setUser(user);
HandlerMethod hm = (HandlerMethod)handler;
AccessLimit accessLimit = hm.getMethodAnnotation(AccessLimit.class);
if(accessLimit == null) {
return true;
}
int seconds = accessLimit.seconds();
int maxCount = accessLimit.maxCount();
boolean needLogin = accessLimit.needLogin();
String key = request.getRequestURI();
if(needLogin) {
if(user == null) {
render(response, CodeMsg.SESSION_ERROR);
return false;
}
key += "_" + user.getId();
}else {
//do nothing
}
AccessKey ak = AccessKey.withExpire(seconds);
Integer count = redisService.get(ak, key, Integer.class);
if(count == null) {
redisService.set(ak, key, 1);
}else if(count < maxCount) {
redisService.incr(ak, key);
}else {
render(response, CodeMsg.ACCESS_LIMIT_REACHED);
return false;
}
}
return true;
}
private void render(HttpServletResponse response, CodeMsg cm)throws Exception {
response.setContentType("application/json;charset=UTF-8");
OutputStream out = response.getOutputStream();
String str = JSON.toJSONString(Result.error(cm));
out.write(str.getBytes("UTF-8"));
out.flush();
out.close();
}
private MiaoshaUser getUser(HttpServletRequest request, HttpServletResponse response) {
String paramToken = request.getParameter(MiaoshaUserService.COOKI_NAME_TOKEN);
String cookieToken = getCookieValue(request, MiaoshaUserService.COOKI_NAME_TOKEN);
if(StringUtils.isEmpty(cookieToken) && StringUtils.isEmpty(paramToken)) {
return null;
}
String token = StringUtils.isEmpty(paramToken)?cookieToken:paramToken;
return userService.getByToken(response, token);
}
private String getCookieValue(HttpServletRequest request, String cookiName) {
Cookie[] cookies = request.getCookies();
if(cookies == null || cookies.length <= 0){
return null;
}
for(Cookie cookie : cookies) {
if(cookie.getName().equals(cookiName)) {
return cookie.getValue();
}
}
return null;
}
}
- 4 新增引數解析者
@Service
public class UserArgumentResolver implements HandlerMethodArgumentResolver {
@Autowired
MiaoshaUserService userService;
public boolean supportsParameter(MethodParameter parameter) {
Class<?> clazz = parameter.getParameterType();
return clazz==MiaoshaUser.class;
}
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
return UserContext.getUser();
}
}
- 5 註冊攔截器和引數解析者
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter{
@Autowired
UserArgumentResolver userArgumentResolver;
@Autowired
AccessInterceptor accessInterceptor;
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(userArgumentResolver);
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(accessInterceptor);
}
}
3 結束
搞完收工,如果你喜歡博主的文章的話麻煩點一下關注,如果發現博主文章的錯誤的話 麻煩指出, 謝謝大家。