1. 程式人生 > >設計容錯的資料庫連線池程式設計思路--架構優化之道【JAVA核心】

設計容錯的資料庫連線池程式設計思路--架構優化之道【JAVA核心】

本專案的原始碼已經上傳,歡迎點選下載

1、資料庫連線池的產生背景?



資料庫在高併發情況下會面臨諸多問題——

  • JDBC:管道的臨時建立——非常耗用資源,而且在高併發中,多個客戶端同時建立IO管道
  • DB的承受能力如何考慮?
  • 執行SQL語句完畢後又要馬上銷燬——之後難道不要再用?
  • 最大連線管道上限也沒有——太多了無法承受
  • 響應效能在高併發連線時如何保證?

急需第三方優化框架管理資源連線,提供以下功能——

  • 管道快取  複用
  • 設定連線上限,MAX值,保證DB正常響應
  • 解決我們快取管道里面的高併發佔用問題——保證管道資料唯一性

2、什麼是資料庫連線池?

在Java中開源的資料庫連線池有以下幾種 :

(1)C3P0:是一個開放原始碼的JDBC連線池,它在lib目錄中與Hibernate一起釋出,包括了實現jdbc3和jdbc2擴充套件規範說明的Connection 和Statement 池的DataSources 物件。

(2)Druid:Druid不僅是一個數據庫連線池,還包含一個ProxyDriver、一系列內建的JDBC元件庫、一個SQL Parser。
支援所有JDBC相容的資料庫,包括Oracle、MySql、Derby、Postgresql、SQL Server、H2等。
Druid針對Oracle和MySql做了特別優化,比如:

  • Oracle的PS Cache記憶體佔用優化
  • MySql的ping檢測優化

資料庫連線池主要採用池化技術(資源池模式)——也就是快取

連線池架構:解決了非常寶貴的連線資源,不用臨時去new,事先準備一定數量的連線資源物件,且這個資源物件還可以被複用。且當執行緒正在佔用的時候其他執行緒是互斥狀態。為資料庫的連線資源進行調配,減少資源空閒的時間。

資料庫連線池:提供資料庫連線管道的連線池架構就是資料庫連線池。

3、資料庫連線池的應用架構所處的位置和功能?


4、自己手寫設計一個數據庫連線池

本專案的原始碼已經上傳,歡迎點選下載

思路圖:


4.1 專案整體架構


4.2 druid部分編碼


4.2.1 ISimplePool介面

package com.heylink.druid;

/*
 * introductions:面向介面程式設計
 * 抽取連線池架構的介面
 * created by Heylink on 2018/4/23 21:13
 */
public interface ISimplePool {

    //獲取連線
    PoolConnection getConnection();
    //建立連線
    void createConnection(int count);
}

4.2.2 SimplePoolImpl介面實現

package com.heylink.druid;

import com.heylink.util.PropertiesUtil;

import java.sql.Connection;
import java.sql.Driver;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Objects;
import java.util.Vector;

/*
 * introductions:執行緒池的實現類,實現管道的配置、初始化、建立和提供
 * created by Heylink on 2018/4/27 18:31
 */
public class SimplePoolImpl implements ISimplePool {

    //jdbc引數
    private static String jdbcDriver = null;
    private static String jdbcUrl = null;
    private static String username = null;
    private static String password = null;
    //限制連線池中的管道數量引數
    private static int initCount = 3;
    private static int stepSize = 10;
    private static int poolMaxSize = 150;
    private static Vector<PoolConnection> poolConnections = new Vector<>();

    public SimplePoolImpl() {
        init();
    }

    private void init() {
        //1.外部配置檔案的讀取
        jdbcDriver = PropertiesUtil.getProperty("jdbcDriver");
        jdbcUrl = PropertiesUtil.getProperty("jdbcUrl");
        username = PropertiesUtil.getProperty("username");
        password = PropertiesUtil.getProperty("password");
        //2.限制管道數量引數初始化
        if (Integer.valueOf(Objects.requireNonNull(PropertiesUtil.getProperty("initCount"))) > 0) {
            initCount = Integer.valueOf(PropertiesUtil.getProperty("initCount"));
        }
        if (Integer.valueOf(Objects.requireNonNull(PropertiesUtil.getProperty("stepSize"))) > 0) {
            stepSize = Integer.valueOf(PropertiesUtil.getProperty("stepSize"));
        }
        if (Integer.valueOf(Objects.requireNonNull(PropertiesUtil.getProperty("poolMaxSize"))) > 0) {
            poolMaxSize = Integer.valueOf(PropertiesUtil.getProperty("poolMaxSize"));
        }
        //3.DriverManager 管家用來註冊,驅動例項
        try {
            //獲取資料庫的驅動物件
            Driver dbDriver = (Driver) Class.forName(jdbcDriver).newInstance();
            DriverManager.registerDriver(dbDriver);
        } catch (Exception e) {
            e.printStackTrace();
        }
        //4.初始化連線池中的管道數量
        createConnection(initCount);
    }

    /**
     * 對外提供管道
     * @author heylinlook
     * @date 2018/4/27 20:20
     * @param
     * @return
     */
    @Override
    public PoolConnection getConnection() {
        if (poolConnections.size() == 0) {
            System.out.println("連線池還未初始化,將執行初始化操作");
            createConnection(initCount);
        }
        PoolConnection connection = getRealConnection();
        //多執行緒環境中 執行緒拿到物件 引用堆當中例項 可能被其他執行緒搶走或者執行緒終止導致管道為空
        while (connection == null) {
            //管道被搶,擴容
            createConnection(stepSize);
            //再次獲取有效管道
            connection = getRealConnection();
            try {
                //個人經驗:人為的減少競爭 醒來後仍然判斷是否為空
                Thread.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //返回有效物件
        return connection;
    }

    /**
     * 獲取有效的管道 加鎖
     * 1.管道物件長期在集合中,沒有超時回收
     * 2.沒有被其他執行緒佔用
     * @author heylinlook
     * @date 2018/4/27 20:23
     * @param
     * @return
     */
    private synchronized PoolConnection getRealConnection() {
        for (PoolConnection conn : poolConnections) {
            if (!conn.isBusy()) {
                //沒有被佔用的情況
                Connection connection = conn.getConnection();
                //進一步判斷是否沒有長期存在集合中被回收
                try {
                    //如果失效
                    if (!connection.isValid(2000)) {
                        //替換 保證最大效率(不走create方法)
                        connection = DriverManager.getConnection(jdbcUrl, username, password);
                    }
                } catch (SQLException e) {
                    e.printStackTrace();
                }
                //當前執行緒佔用該管道
                conn.setBusy(true);
                return conn;
            }
        }
        //如果遍歷完都沒有有效的管道,返回空
        return null;
    }

    @Override
    public void createConnection(int count) {
        //判斷是否還可以再增加管道數量
        if (poolMaxSize <= 0 || poolMaxSize < (poolConnections.size() + count)) {
            return;
        }
        //新增count個管道
        for (int i = 0; i < count; i++) {
            try {
                Connection connection = DriverManager.getConnection(jdbcUrl, username, password);
                //封裝管道
                PoolConnection poolConnection = new PoolConnection(false, connection);
                //新增到vector
                poolConnections.add(poolConnection);
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
}

4.2.3 PoolConnection類

package com.heylink.druid;

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

/*
 * introductions:自定義連線資料管道Bean
 * 原生的Connection上面沒有複用的標記
 * 自己封裝設計isBusy標記位來解決執行緒被佔用的問題
 * 總結:這個思想就是封裝思想,也就是擴充套件功能
 * created by Heylink on 2018/4/27 18:32
 */
public class PoolConnection {
    //表示繁忙的標誌,複用的標誌,執行緒安全
    private boolean isBusy = false;
    //真正的管道 用來操作資料的java.sql.Connection
    private Connection connection;

    public PoolConnection(boolean isBusy, Connection connection) {
        this.isBusy = isBusy;
        this.connection = connection;
    }

    public boolean isBusy() {
        return isBusy;
    }

    public void setBusy(boolean busy) {
        isBusy = busy;
    }

    public Connection getConnection() {
        return connection;
    }

    public void setConnection(Connection connection) {
        this.connection = connection;
    }

    //自定義釋放方法
    public void close() {
        this.isBusy = false;
    }

    //自定義查詢功能
    public ResultSet queryBySql(String sql) {
        Statement sm = null;
        ResultSet rs = null;
        try {
            sm = connection.createStatement();
            rs = sm.executeQuery(sql);
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return rs;
    }
}

4.2.4 PoolManager類

package com.heylink.druid;

/*
 * introductions:內部類單例模式來對外提供連線池物件
 * 嚴格保證執行緒安全機制
 * 注意:
 * 簡單的單例模式還不能完全防禦記憶體當中在執行時期有多個物件
 * 只是在返回時合併為一個結果
 * 會給系統帶來額外開銷
 * created by Heylink on 2018/4/27 18:32
 */
public class PoolManager {

    /**
     * 內部類
     * @author heylinlook
     * @date 2018/4/27 20:44
     * @param
     * @return
     */
    private static class createPool {
        private static SimplePoolImpl simplePool = new SimplePoolImpl();
    }

    /**
     * JVM中類載入器位元組碼是一個嚴格互斥理論模型
     * 類載入器原理
     * 完美的實現執行緒安全
     * @author heylinlook
     * @date 2018/4/27 20:45
     * @param
     * @return
     */
    public static SimplePoolImpl getInstance() {
        return createPool.simplePool;
    }
}

4.3 util部分編碼


package com.heylink.util;

import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Properties;

public class PropertiesUtil {

    private static Properties props;

    static {
        String fileName = "jdbcPool.properties";
        props = new Properties();
        try {
            props.load(new InputStreamReader(PropertiesUtil.class.getClassLoader().getResourceAsStream(fileName),"UTF-8"));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static String getProperty(String key) {
        String value = props.getProperty(key.trim());
        if(value == null || value.trim().isEmpty()) {
            return null;
        }
        return value.trim();
    }

    public static String getProperty(String key, String defaultValue) {
        String value = props.getProperty(key.trim());
        if(value == null || value.trim().isEmpty()) {
            value = defaultValue;
        }
        return value.trim();
    }
}

4.4 resource部分編碼


4.4.1 jdbcPool.properties屬性檔案

jdbcDriver=com.mysql.jdbc.Driver
#url
jdbcUrl=jdbc:mysql://localhost:3306/test
#資料賬戶名
username=root
password=admin123
#規範連線池的引數 規範初始化的時候產生多少執行緒的連結管道
initCount=10
#步進量 開發500 架構5000
stepSize=4
#資料庫管道數量限制
poolMaxSize=150

4.4.2 test.sql測試資料庫

-- MySQL dump 10.13  Distrib 5.7.12, for Win64 (x86_64)
--
-- Host: localhost    Database: test
-- ------------------------------------------------------
-- Server version	5.6.34-log

--
-- Table structure for table `items`
--

DROP TABLE IF EXISTS `items`;
/*!40101 SET @saved_cs_client     = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `items` (
  `ID` int(11) NOT NULL,
  `NAME` varchar(45) DEFAULT NULL,
  `PRICE` int(11) DEFAULT NULL,
  PRIMARY KEY (`ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */;

--
-- Dumping data for table `items`
--

LOCK TABLES `items` WRITE;
/*!40000 ALTER TABLE `items` DISABLE KEYS */;
INSERT INTO `items` VALUES (1,'heylink',100),(2,'chicken',200),(3,'dog',234);
/*!40000 ALTER TABLE `items` ENABLE KEYS */;
UNLOCK TABLES;

4.5 test部分編碼


import com.heylink.druid.PoolConnection;
import com.heylink.druid.PoolManager;
import com.heylink.druid.SimplePoolImpl;

import java.sql.ResultSet;
import java.sql.SQLException;
/*
 * introductions:
 * created by Heylink on 2018/4/27 18:31
 */
public class SimplePoolTest {
    //拿到連線池物件
    private static SimplePoolImpl simplePool = PoolManager.getInstance();

    /**
     * 單個執行緒使用連線池物件做查詢業務
     * @author heylinlook
     * @date 2018/4/27 21:01
     * @param
     * @return
     */
    public synchronized static void selectData() {
        PoolConnection connection = simplePool.getConnection();
        //資料庫中存放了測試表
        ResultSet rs = connection.queryBySql("SELECT * FROM items");
        System.out.println("執行緒名稱: " + Thread.currentThread().getName());
        try {
            while (rs.next()) {
                System.out.print(rs.getString("ID") + "\t\t");
                System.out.print(rs.getString("NAME") + "\t\t");
                System.out.println(rs.getString("PRICE") + "\t\t");
            }
            rs.close();
            //業務做完後,我們就釋放   ——連線池中的管道複用機制
            connection.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        //建立1500個執行緒,列印輸出資料庫內容
        for (int i = 0; i < 1500; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    selectData();
                }
            }).start();
        }
    }

}

4.6 程式輸出

輸出結果正常

4.7 注意事項

本專案按照設計容錯的資料庫連線池程式設計思路進行編碼
測試用的SQL語句在resources資料夾下,執行一遍即可
如果報錯No suitable driver found for jdbc:mysql://127.0.0.1:3306/
請先確認將mysql-connector-java-*.jar包放在
JDK安裝路徑下,如
C:\Program Files\Java\jdk\jre\lib\ext

4.8專案原始碼

本專案的原始碼已經上傳到我的GitHub主頁,有問題歡迎留言,歡迎點選下載