首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫

Tomcat + MySQL 構建動態橫幅廣告系統實戰:從環境搭建到 Servlet 核心架構全解析

     編輯:MySQL 資料庫相關知識

專案背景與技術棧

  • 目標:自建開源免費的輪播橫幅廣告系統,替代 DoubleClick 等商業服務。
  • 棧:Apache Tomcat 4.1/9.0 + MySQL 4.1/8.0 + MM MySQL JDBC / Connector/J + Java Servlet 2.3/4.0。
  • 核心流程:頁面加載 → `` → Servlet 隨機選取廣告 → 返回圖片 → 用戶點擊 → `` → Servlet 重定向至目標 URL。

環境搭建標準化流程 (現代版本)

1. Tomcat 9/10 安裝與部署結構

# 下載解壓
tar -xzf apache-tomcat-9.0.x.tar.gz -C /opt/
ln -s /opt/apache-tomcat-9.0.x /opt/tomcat

# Web 應用目錄結構 (Servlet 3.0+ 無需 web.xml)
$CATALINA_HOME/webapps/banner/
├── WEB-INF/
│   ├── classes/          # 編譯後 .class 文件
│   ├── lib/              # mysql-connector-j-8.x.jar
│   └── web.xml (可選)    # 只有需配置 Filter/Listener 時才需要
├── images/               # 靜態圖片資源
└── index.jsp             # 測試頁面

2. MySQL 8.0 資料庫設計

CREATE DATABASE banner_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE banner_db;

CREATE TABLE ads (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    image_file VARCHAR(255) NOT NULL COMMENT '圖片相對路徑 /images/xxx.jpg',
    target_url VARCHAR(500) NOT NULL COMMENT '點擊跳轉 URL',
    customer_name VARCHAR(100) COMMENT '廣告主',
    clicks_purchased INT UNSIGNED DEFAULT 0 COMMENT '購買點擊數',
    clicks_remaining INT UNSIGNED DEFAULT 0 COMMENT '剩餘點擊數',
    impressions INT UNSIGNED DEFAULT 0 COMMENT '展示次數',
    weight TINYINT UNSIGNED DEFAULT 10 COMMENT '權重 (1-100)',
    status ENUM('ACTIVE','PAUSED','EXPIRED') DEFAULT 'ACTIVE',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    INDEX idx_status_weight (status, weight)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 測試數據
INSERT INTO ads (image_file, target_url, customer_name, clicks_purchased, clicks_remaining, weight) VALUES
('/images/ad1.jpg', 'https://example.com', 'Client A', 1000, 1000, 30),
('/images/ad2.jpg', 'https://shop.example.com', 'Client B', 500, 500, 20),
('/images/ad3.jpg', 'https://blog.example.com', 'Client C', 200, 200, 50);

3. JDBC 驅動部署

# 下載 mysql-connector-j-8.2.0.jar (JDK 11+)
# 放入 $CATALINA_HOME/webapps/banner/WEB-INF/lib/
# 重啟 Tomcat

核心架構:四大類職責分離

類別職責關鍵方法複用性
Logger統一日誌輸出至文件logEntry(), logStart(), logEnd(), deleteLog()✅ 全專案通用
DBHandlerJDBC 封裝:連接、PreparedStatement、ResultSet 管理setQuery(), lookup(), executeUpdate(), insert(), close()✅ 任何 JDBC 專案
BannerPOJO:廣告元數據封裝 (id, imageFile, url, weight...)Getter/Setter, toString()業務專用
BannerServlet核心控制器:初始化緩存、處理 image/link 請求、權重隨機、統計更新init(), service(), getRandomBanner(), increaseImpressions(), decreaseClicks()業務專用

BannerServlet 核心邏輯深度解析

初始化階段:載入所有廣告至內存 HashMap

public void init(ServletConfig config) throws ServletException {
    super.init(config);
    _banners = new HashMap<>();
    _totalWeight = 0;

    DBHandler db = new DBHandler("com.mysql.cj.jdbc.Driver",
                                 "jdbc:mysql://localhost:3306/banner_db?useSSL=false",
                                 "banner_user", "pass", logger);
    db.setQueryString("SELECT * FROM ads WHERE status='ACTIVE'");
    ResultSet rs = db.executeQuery();

    int index = 0;
    while (rs.next()) {
        Banner b = new Banner();
        b.setId(rs.getInt("id"));
        b.setImageFile(rs.getString("image_file"));
        b.setUrl(rs.getString("target_url"));
        b.setWeight(rs.getInt("weight"));
        b.setClicksRemaining(rs.getInt("clicks_remaining"));
        // ... 其他欄位
        _banners.put(index++, b);
        _totalWeight += b.getWeight();
    }
    db.close();
}

請求分發:image vs link 兩種模式

public void service(HttpServletRequest req, HttpServletResponse resp)
        throws ServletException, IOException {
    String type = req.getParameter("type");
    HttpSession session = req.getSession(true);

    if ("image".equals(type)) {
        // 1. 加權隨機選取
        Banner banner = getRandomBanner();
        // 2. 存入 Session 供點擊時使用
        session.setAttribute("currentBanner", banner);
        // 3. 異步增加展示次數 (不阻塞響應)
        new Thread(() -> increaseImpressions(banner.getId())).start();
        // 4. 重定向至圖片資源 (或直接輸出圖片流)
        resp.sendRedirect(req.getContextPath() + banner.getImageFile());
    } else if ("link".equals(type)) {
        // 1. 從 Session 取出對應廣告
        Banner banner = (Banner) session.getAttribute("currentBanner");
        if (banner != null) {
            // 2. 異步減少剩餘點擊數
            new Thread(() -> decreaseClicksRemaining(banner.getId())).start();
            // 3. 重定向至目標 URL
            resp.sendRedirect(banner.getUrl());
        } else {
            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
        }
    }
}

加權隨機算法 (線性掃描版)

private Banner getRandomBanner() {
    int randomWeight = (int) (Math.random() * _totalWeight) + 1;
    int cumulative = 0;
    for (Banner b : _banners.values()) {
        cumulative += b.getWeight();
        if (randomWeight <= cumulative) return b;
    }
    return _banners.values().iterator().next(); // fallback
}

優化方向:預計算前綴和陣列 + 二分查找 O(log n),或使用 DiscreteDistribution (Apache Math)。

統計更新:異步 + 緩存同步

private void increaseImpressions(int bannerId) {
    DBHandler db = new DBHandler(...);
    db.setQueryString("UPDATE ads SET impressions = impressions + 1 WHERE id = ?");
    db.executeUpdate(bannerId);
    db.close();
    reloadBanners(); // 重新載入 HashMap 保證權重/剩餘點擊實時
}

private void decreaseClicksRemaining(int bannerId) {
    DBHandler db = new DBHandler(...);
    db.setQueryString("UPDATE ads SET clicks_remaining = clicks_remaining - 1 WHERE id = ? AND clicks_remaining > 0");
    db.executeUpdate(bannerId);
    db.close();
    reloadBanners();
}

前端整合範例 (JSP/HTML)

<%@ page contentType="text/html;charset=UTF-8" %>
<html>
<body>
<h2>動態橫幅廣告演示</h2>
<!-- 圖片請求 -->
<a href="<%=request.getContextPath()%>/banner/servlet/BannerServlet?type=link">
    <img src="<%=request.getContextPath()%>/banner/servlet/BannerServlet?type=image" 
         alt="Ad Banner" border="0" width="468" height="60"/>
</a>
<hr/>
<p>每次刷新頁面,橫幅圖片與鏈接均會根據權重隨機變化。</p>
</body>
</html>

生產環境強化方向

  1. 連接池化:DBHandler 內部使用 HikariCP/Druid,而非每請求新建連接。
  2. 緩存策略升級:引入 Caffeine/Redis 做二級緩存,減少 DB 壓力;定時任務同步統計數據。
  3. 點擊防刷:IP+Cookie+UserAgent 指紋去重、點擊頻率限流 (Token Bucket)。
  4. 圖片 CDN 加速:image_file 存儲 CDN 域名路徑,Servlet 僅返回 JSON {imageUrl, targetUrl, bannerId},前端 JS 渲染。
  5. 統計異步化:引入 Kafka/RocketMQ,Servlet 發送事件,消費者聚合寫入 DB,支撐萬級 QPS。
  6. 管理後台:CRUD 廣告、權重調整、報表導出、定時上下線。

避坑指南

坑點後果解決
Servlet 單例多線程共享 HashMap 非線程安全併發下數據錯亂、NPE用 ConcurrentHashMap 或 ReadWriteLock 保護 _banners
每請求 new DBHandler() 導致連接風暴資料庫連接耗盡、響應極慢引入連接池、ThreadLocal 復用、或改用 Spring JdbcTemplate
Session 依賴導致集群 Session 不共享多節點部署點擊丟失關聯Sticky Session / Redis Session / JWT 攜帶 bannerId
權重總和溢出 (int 上限 21億)隨機算法失效用 long 存儲 _totalWeight
圖片直接輸出流未設 Content-Type/Content-Length瀏覽器無法正確渲染/緩存resp.setContentType("image/jpeg"); resp.setContentLength(len);

這個經典的 Tomcat + MySQL + Servlet 專案,雖然技術棧歷經多代演進,但其**核心架構思想——職責分離、內存緩存熱數據、異步處理非關鍵寫入、加權隨機分發**——至今仍是廣告系統、推薦系統、A/B 測試平台的基因底色。掌握其演進邏輯,能讓你在現代雲原生架構中做出更有歷史厚度的技術決策。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved