Java有什么項目推薦的嗎?電商功能設計
項目經歷第三期。。。。。。項目經歷第三期。。。。。。項目經歷第三期。。。。。。
大家好,我是南哥。
一個Java學習與進階的領路人,跟著南哥我們一起Java成長。
文章目錄
- 電商功能設計
- 商品表設計
- 商品列表
- 商品詳情
- 商品下單
- 重點:秒殺搶購
好久之前就想寫這么一篇商品功能設計,這幾天得空把坑給填了,給南友們多一個 "項目亮點" 的參考。
1. 電商功能設計
1.1 商品表設計
面試官:數據庫表你怎么設計的?
南哥先給出電商業(yè)務最基礎的幾個表設計。隨著用戶量的激增,肯定的是業(yè)務復雜性會逐日遞增,你會發(fā)現(xiàn)簡簡單單的一個表,不知不覺多出了很多奇奇怪怪的字段。
(1)商品表
CREATE TABLE products (
product_id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT,
price DECIMAL(10, 2) NOT NULL,
stock INT DEFAULT 0,
category_id INT,
status ENUM('active', 'inactive', 'deleted') DEFAULT 'active',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (category_id) REFERENCES product_categories(category_id)
);
(2)商品分類表
CREATE TABLE product_categories (
category_id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT,
parent_id INT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (parent_id) REFERENCES product_categories(category_id)
);
(3)用戶購物車表
CREATE TABLE shopping_carts (
cart_id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
product_id INT NOT NULL,
quantity INT DEFAULT 1,
added_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(user_id),
FOREIGN KEY (product_id) REFERENCES products(product_id)
);
(4)訂單表
CREATE TABLE orders (
order_id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
total_price DECIMAL(10, 2) NOT NULL,
status ENUM('pending', 'completed', 'cancelled') DEFAULT 'pending',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(user_id)
);
1.2 商品列表
面試官:那商品列表接口怎么保證可用性?
商品列表在電商APP有多種形式,例如:熱門商品列表、查詢條件商品列表、用戶推薦商品列表。
(1)熱門商品列表
特別針對第一種形式,熱門商品列表要重點加上緩存,畢竟該列表所有用戶打開APP都需要顯示出來,可以把該接口歸類為高并發(fā)設計接口。
熱門商品有一個特點,商品的更新速度快,可能某個商品半小時還在熱門,下一秒突然不見。
這里我們采用Redis分布式緩存,后臺配置熱門商品時更新分布式緩存,而熱門商品列表接口直接查詢Redis,不把壓力落到數據庫。
// 后臺配置熱門商品時更新分布式緩存,而熱門商品列表接口直接查詢Redis
public List<Product> getHotProductList() {
// 從Redis緩存獲取熱門商品列表
List<Product> hotProducts = redisTemplate.opsForList().range("hot_products", 0, -1);
if (hotProducts == null || hotProducts.isEmpty()) {
// 如果緩存為空,從數據庫查詢,并更新緩存
hotProducts = productService.fetchHotProductsFromDB();
redisTemplate.opsForList().rightPushAll("hot_products", hotProducts);
}
return hotProducts;
}
另外需要把熱門商品列表緩存到APP端,不至于每次返回主頁面就調用一次接口查詢。APP端緩存接口設置短些,例如1 分鐘,畢竟上文有提到熱門商品更新速度是比較快的!
(2)查詢條件商品列表
用戶的查詢條件多種多樣,我們可以把用戶查詢關鍵詞通過埋點記錄下來,要求運營給出熱度最高的商品查詢關鍵詞。
針對熱門關鍵詞查詢,把查詢結果進行緩存。當然整個查詢結果會很大,我們設置對前幾頁進行緩存。
緩存放在哪?
這里我們仍然放在Redis分布式緩存。有人可能會說放到后端本地緩存?MyBatis一級、二級緩存的坑或許他還沒遇到,MyBatis一級緩存作用于SqlSession對象,二級緩存作用于Mapper對象。這造成了各個后端服務的本地緩存不同,每次查詢的結果都不相同。
當然有些業(yè)務可以用到,例如閱讀量這些用戶不太在意的的數據可以用本地緩存。
// 查詢條件商品列表
public List<Product> getProductsByQuery(String query, int page) {
String cacheKey = "query_products:" + query + ":page" + page;
List<Product> products = redisTemplate.opsForList().range(cacheKey, 0, -1);
if (products == null || products.isEmpty()) {
// 如果緩存為空,從數據庫查詢,并更新緩存
products = productService.fetchProductsByQueryFromDB(query, page);
redisTemplate.opsForList().rightPushAll(cacheKey, products);
// 緩存有效期為10分鐘
redisTemplate.expire(cacheKey, 10, TimeUnit.MINUTES);
}
return products;
}
另一個問題,查詢結果變化怎么辦?
這里我們設置一個定時任務,每隔一段時間更新 "查詢條件商品列表" 的緩存結果。
// 定時任務更新緩存
@Scheduled(fixedRate = 600000)
public void updateProductsCache() {
// 重新從數據庫獲取數據并更新緩存
List<String> hotQueries = analyticsService.getHotQueries();
for (String query : hotQueries) {
List<Product> products = productService.fetchProductsByQueryFromDB(query, 1); // 僅示例:更新第一頁數據
String cacheKey = "query_products:" + query + ":page1";
redisTemplate.delete(cacheKey);
redisTemplate.opsForList().rightPushAll(cacheKey, products);
// 重設緩存有效期
redisTemplate.expire(cacheKey, 10, TimeUnit.MINUTES);
}
}
1.3 商品詳情
面試官:商品詳情為什么要加緩存?
商品詳情的特點是更新頻率慢,另外用戶的操作習慣是:會不斷退出重進,反復瀏覽某個商品的詳情頁。
猜猜他們在干嘛,用戶在反復對比不同商品,勸說自己究竟要買哪一個,畢竟強迫癥大家都有的。
基于以上的用戶行為、商品詳情特點,我們可以把商品詳情緩存到APP端。
1.4 商品下單
面試官:下單邏輯怎么保證安全性?
電商業(yè)務的訂單記錄表、商品下單接口是最重要的核心模塊,畢竟這一塊涉及到了業(yè)務賺錢的核心。
(1)校驗功能
用戶從APP端點擊下單按鈕,后端服務要走一套怎么樣的流程?首先我們需要先進行校驗。
- 用戶身份校驗
- 用戶余額校驗
- 商品校驗
- 商品庫存校驗
(2)防重復提交
再者,對于下單接口需要添加防重復提交限制,這里可以有多種方案。舉個例子,采用Redis分布式鎖方案,Redis分布式鎖的key設置與用戶、商品id相關。
# Redis分布式鎖的key
lock:order:{uid}:{product_id}
用戶下單某一個商品,會獲取Redis分布式鎖。對于同一個商品,在前一個商品的邏輯沒有處理完成時,不能進行下一次下單請求。
防重復提交的作用主要是防止用戶誤觸,或者同一時間多個重復下單請求造成的數據異常。
(3)事務控制
對于整個下單的流程,包括庫存的減少、用戶扣費、訂單表的創(chuàng)建都應該包含在同一個MySQL事務中,一旦流程中的任何一個邏輯出錯,則進行回滾。
(4)異步處理
對于下單成功后的其他操作,例如下單成功信息通知用戶等,可以使用任務隊列的形式異步去執(zhí)行,減少下單接口的耗時。
// 用戶下單接口
public Order placeOrder(int userId, int productId, int quantity) throws Exception {
// 獲取分布式鎖
String lockKey = "lock:order:" + userId + ":" + productId;
if (!redisTemplate.opsForValue().setIfAbsent(lockKey, "locked", 10, TimeUnit.SECONDS)) {
throw new Exception("下單過于頻繁,請稍后再試");
}
try {
// 檢查用戶、商品及庫存
userService.verifyUser(userId);
Product product = productService.verifyProduct(productId);
inventoryService.checkInventory(productId, quantity);
// 開始事務
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
// 減庫存,扣費,生成訂單
inventoryService.decreaseInventory(productId, quantity);
userService.debitUserAccount(userId, product.getPrice().multiply(new BigDecimal(quantity)));
Order order = orderService.createOrder(userId, productId, product.getPrice(), quantity);
transactionManager.commit(status); // 提交事務
return order;
} catch (Exception e) {
transactionManager.rollback(status); // 回滾事務
throw e;
}
} finally {
redisTemplate.delete(lockKey); // 釋放鎖
}
}
1.5 重點:秒殺搶購
面試官:你會怎么設計秒殺搶購功能?
我們可以把秒殺搶購看成是商品下單的特殊場景。秒殺搶購的并發(fā)量高,庫存有限,且秒殺商品的頁面會獨立出來,不會和其他商品頁面耦合在一起。
基于以上簡單的梳理,我們可以這么設計來保證秒殺場景的穩(wěn)定性。
(1)秒殺頁面靜態(tài)化
把秒殺商品頁面設置為靜態(tài)化,當用戶刷新頁面時,只需要從服務器獲取基礎后端數據進行填充。另外當用戶點擊秒殺按鈕后,前端把按鈕進行置灰,減少用戶的請求。
(2)下單限制
很多程序員的初始設計會把所有請求都進入下單接口流程,完全沒必要!??!
如果秒殺庫存只有10,在下單接口前面,我們可以設置一個過濾攔截,只有前50個用戶才會進入下單流程,拒絕其他用戶的下單請求,其他用戶甚至不需要進行下單的流程。
后續(xù)在由這50個用戶搶奪這10個商品庫存。
// 決定是否讓用戶進入搶購流程
public class SeckillController {
@Autowired
private KafkaTemplate<String, SeckillOrderRequest> kafkaTemplate;
public ResponseEntity<String> placeSeckillOrder(int userId, int productId) {
String queueName = "seckill_orders";
String lockKey = "seckill:availability:" + productId;
// 檢查是否還有秒殺資格
Long rank = redisTemplate.opsForValue().increment(lockKey);
if (rank == null || rank > 50) {
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body("抱歉,秒殺名額已滿。");
}
// 創(chuàng)建秒殺請求
SeckillOrderRequest request = new SeckillOrderRequest(userId, productId);
// 發(fā)送到Kafka隊列
kafkaTemplate.send(queueName, request);
return ResponseEntity.ok("您的秒殺請求已接收,正在處理中,請耐心等待結果。");
}
}
(3)下單請求任務化
把每一個下單請求都抽象為一個Kafka隊列任務,任務一個個執(zhí)行,減少系統(tǒng)的瞬時壓力。
// 出來下單隊列任務
@Service
public class SeckillOrderConsumer {
@Autowired
private OrderService orderService;
@Autowired
private ProductService productService;
@Autowired
private InventoryService inventoryService;
@KafkaListener(topics = "seckill_orders", groupId = "seckill_group")
public void consume(SeckillOrderRequest request) {
try {
// 檢查庫存
if (!inventoryService.checkInventory(request.getProductId(), 1)) {
throw new Exception("庫存不足");
}
// 下單處理
Order order = orderService.createSeckillOrder(request.getUserId(), request.getProductId(), 1);
// 其他邏輯處理
notifyUser(order);
} catch (Exception e) {
// 處理失敗邏輯
System.out.println("秒殺處理失敗:" + e.getMessage());
}
}
private void notifyUser(Order order) {
// 通知用戶秒殺結果
}
}
#java##秋招##面試##項目##實習#創(chuàng)作不易,不妨點贊、收藏、關注支持一下,各位的支持就是我創(chuàng)作的最大動力????