更新了原先寫的JDBC查詢緩存

前段時間因爲實際需要,寫了一個簡單的JDBC查詢緩存,發表在這裏:

http://topic.csdn.net/u/20091209/18/e366812c-5cc6-47b2-83d6-f78350206781.html?1092065064


經過一段時間的使用,發現很嚴重的問題:緩存的CachedRowSet因爲是通過遊標訪問記錄的,所以如果多線程同時操作一個CachedRowSet,遊標就會衝突,爲了解決這個問題,我在上面補了很多代碼,非常非常難看!


今天重拾舊題,也多虧了“火龍果@菜菜寶寶”的點撥,使用了Apache Commons DbUtil的List格式的結果集代替了CachedRowSet,這樣遊標衝突就避免了(多線程肯定是擁有獨立的迭代器的,而不像遊標只有一個),然後再使用Apache Commons Collections中的LRUMap以Collections.synchronizedMap()方法包裝成線程安全的Map作爲緩存,懶得自己寫線程安全的代碼了,呵呵。最後,連接池使用的是C3P0。


廢話少說,上代碼:

import com.mchange.v2.c3p0.ComboPooledDataSource;
import java.beans.PropertyVetoException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;
import java.util.logging.Logger;
import org.apache.commons.collections.map.LRUMap;
import org.apache.commons.dbutils.DbUtils;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.MapListHandler;

/**
 * JDBC查詢緩存
 * @author shajunxing
 */
public class QueryCache {

    private static final Logger logger = Logger.getLogger(QueryCache.class.getName());
    private Map<String, List<Map<String, Object>>> cache = null;
    private String driver;
    private String url;
    private String user;
    private String password;
    private ComboPooledDataSource cpds;
    private AtomicLong visitedCount = new AtomicLong(0);
    private AtomicLong hitCount = new AtomicLong(0);

    /**
     * 默認構造函數
     * @param driver JDBC驅動類
     * @param url 連接字符串
     * @param user 用戶名
     * @param password 口令
     * @param maxSize 查詢緩存大小
     */
    public QueryCache(String driver, String url, String user, String password, int maxSize) {
        this.driver = driver;
        this.url = url;
        this.user = user;
        this.password = password;
        cache = Collections.synchronizedMap(new LRUMap(maxSize));
        cpds = new ComboPooledDataSource();
        cpds.setJdbcUrl(url);
        cpds.setUser(user);
        cpds.setPassword(password);
        try {
            Class.forName(driver);
            cpds.setDriverClass(driver);
        } catch (ClassNotFoundException ex) {
            logger.severe(ex.toString());
        } catch (PropertyVetoException ex) {
            logger.severe(ex.toString());
        }
    }

    public void clear() {
        cache.clear();
    }

    /**
     * 非緩存查詢
     * @param sql SQL語句
     * @param pooled 是否使用連接池
     * @return 查詢結果
     */
    public List<Map<String, Object>> query(String sql, boolean pooled) {
        Connection conn = null;
        try {
            if (pooled) {
                conn = cpds.getConnection();
            } else {
                conn = DriverManager.getConnection(url, user, password);
            }
            return new QueryRunner().query(conn, sql, new MapListHandler());
        } catch (SQLException ex) {
            logger.severe(ex.toString());
            return null;
        } finally {
            DbUtils.closeQuietly(conn);
        }
    }

    /**
     * 緩存查詢
     * @param sql SQL語句
     * @param pooled 是否使用連接池
     * @return 查詢結果
     */
    public List<Map<String, Object>> cachedQuery(String sql, boolean pooled) {
        visitedCount.incrementAndGet();
        if (cache.containsKey(sql)) {
            hitCount.incrementAndGet();
            return cache.get(sql);
        } else {
            List<Map<String, Object>> result = query(sql, pooled);
            cache.put(sql, result);
            return result;
        }
    }

    public long getVisitedCount() {
        return visitedCount.get();
    }

    public long getHitCount() {
        return hitCount.get();
    }

    public int getSize() {
        return cache.size();
    }

    public Set<String> getKeySet() {
        return cache.keySet();
    }
}


進行了兩個測試:


其一測試了下面7種情形:

A單線程,無連接池、無查詢緩存

B單線程,有連接池、無查詢緩存

C單線程,無連接池、有查詢緩存

D單線程,有連接池、有查詢緩存

E多線程,有連接池、無查詢緩存

F多線程,無連接池、有查詢緩存

G多線程,有連接池、有查詢緩存

注:多線程,無連接池、無查詢緩存的情況因爲數據庫最大連接數的限制,不做測試。

import java.util.logging.Logger;

/**
 * 性能測試1
 * @author shajunxing
 */
public class PerformanceTest {

    private static final Logger logger = Logger.getLogger(PerformanceTest.class.getName());
    private static final String driver = "XXX";
    private static final String url = "XXX";
    private static final String user = "XXX";
    private static final String password = "XXX";
    private static final String sql = "XXX";
    private static final int loopCount = 1000;
    private static QueryCache cache = new QueryCache(driver, url, user, password, 1000000);

    private static void testQuery() {
        for (int i = 0; i < loopCount; i++) {
            cache.query(sql, false);
        }
    }

    private static void testPooledQuery() {
        for (int i = 0; i < loopCount; i++) {
            cache.query(sql, true);
        }
    }

    private static void testCachedQuery() {
        for (int i = 0; i < loopCount; i++) {
            cache.cachedQuery(sql, false);
        }
    }

    private static void testCachedPooledQuery() {
        for (int i = 0; i < loopCount; i++) {
            cache.cachedQuery(sql, true);
        }
    }

    private static void testPooledQueryMS() {
        Thread[] threads = new Thread[loopCount];
        for (int i = 0; i < loopCount; i++) {
            Thread thread = new Thread(new Runnable() {

                public void run() {
                    cache.query(sql, true);
                }
            });
            thread.start();
            threads[i] = thread;
        }
        for (Thread thread : threads) {
            try {
                thread.join();
            } catch (InterruptedException ex) {
                logger.severe(ex.toString());
            }
        }
    }

    private static void testCachedQueryMS() {
        Thread[] threads = new Thread[loopCount];
        for (int i = 0; i < loopCount; i++) {
            Thread thread = new Thread(new Runnable() {

                public void run() {
                    cache.cachedQuery(sql, false);
                }
            });
            thread.start();
            threads[i] = thread;
        }
        for (Thread thread : threads) {
            try {
                thread.join();
            } catch (InterruptedException ex) {
                logger.severe(ex.toString());
            }
        }
    }

    private static void testCachedPooledQueryMS() {
        Thread[] threads = new Thread[loopCount];
        for (int i = 0; i < loopCount; i++) {
            Thread thread = new Thread(new Runnable() {

                public void run() {
                    cache.cachedQuery(sql, true);
                }
            });
            thread.start();
            threads[i] = thread;
        }
        for (Thread thread : threads) {
            try {
                thread.join();
            } catch (InterruptedException ex) {
                logger.severe(ex.toString());
            }
        }
    }

    public static void main(String[] args) {
        testQuery();
        testPooledQuery();
        testCachedQuery();
        testCachedPooledQuery();
        testPooledQueryMS();
        testCachedQueryMS();
        testCachedPooledQueryMS();
    }
}


使用NetBeans Profiler做三次測試,結果如下:


 

 

從中可以得出結論:

單線程的四種方式時間比大約爲A : B : C : D = 15000 : 2000 : 15 : 1;
多線程三種方式耗時差不多,估計在線程的創建以及調度上花費了很多時間。

 

其二測試了緩存在模擬的實際情況下面的增長以及命中率等數值。

 

方法是首先通過JDBC元數據操作獲取數據庫中的所有表名和各個表的字段名,然後將字段名和表名隨機組合構造SQL語句,測試10000次(單線程,在我們的項目中多線程同時訪問的情況很少),每100次統計一下緩存的命中率等信息,代碼如下:

 

 

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Calendar;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.logging.Logger;
import org.apache.commons.dbutils.DbUtils;

/**
 * 性能測試2
 * @author shajunxing
 */
public class PerformanceTest2 {

    private static final Logger logger = Logger.getLogger(PerformanceTest2.class.getName());
    private static final String driver = "XXX";
    private static final String url = "XXX";
    private static final String user = "XXX";
    private static final String password = "XXX";
    private static QueryCache cache = new QueryCache(driver, url, user, password, 1000000);
    private static Map<String, List<String>> tables = new HashMap<String, List<String>>();
    private static List<String> tableNames = new LinkedList<String>();
    private static Random rand = new Random(Calendar.getInstance().getTimeInMillis());

    private static void getTables() {
        Connection conn = null;
        Statement stmt = null;
        ResultSet rs = null;
        DbUtils.loadDriver(driver);

        try {
            conn = DriverManager.getConnection(url, user, password);
            // 獲取所有表名稱
            logger.info("獲取所有表名稱");
            try {
                DatabaseMetaData dbMeta = conn.getMetaData();
                rs = dbMeta.getTables(null, null, null, new String[]{"TABLE"});
                if (rs != null) {
                    while (rs.next()) {
                        tables.put(rs.getString("TABLE_NAME"), new LinkedList<String>());
                    }
                }
            } catch (SQLException ex) {
                logger.severe(ex.toString());
            } finally {
                DbUtils.closeQuietly(rs);
            }

            // 獲取所有表的字段名稱
            Set<String> illegalTables = new HashSet<String>();
            for (String tableName : tables.keySet()) {
                logger.info(String.format("獲取表%s的字段", tableName));
                List<String> tableColumns = tables.get(tableName);
                try {
                    stmt = conn.createStatement();
                    rs = stmt.executeQuery(String.format("select * from %s where rownum=1", tableName));
                    ResultSetMetaData rsMeta = rs.getMetaData();
                    for (int i = 1; i <= rsMeta.getColumnCount(); i++) {
                        String columnName = rsMeta.getColumnName(i);
                        tableColumns.add(columnName);
                    }
                } catch (SQLException ex) {
                    logger.severe(String.format("獲取表%s的字段失敗:%s", tableName, ex.toString()));
                    illegalTables.add(tableName);
                } finally {
                    DbUtils.closeQuietly(rs);
                    DbUtils.closeQuietly(stmt);
                }
            }

            // 刪除非法的表
            for (String illegal : illegalTables) {
                tables.remove(illegal);
            }

            // 表名列表
            for (String tableName : tables.keySet()) {
                tableNames.add(tableName);
            }

        } catch (SQLException ex) {
            logger.severe(ex.toString());
        } finally {
            DbUtils.closeQuietly(conn);
        }
    }

    public static void main(String[] args) {
        // 獲取所有的表和字段名
        getTables();

        // 打印所有的表和字段名
        for (String tableName : tables.keySet()) {
            System.out.println(tableName);
            for (String columnName : tables.get(tableName)) {
                System.out.println("    " + columnName);
            }
            System.out.println();
        }

        // 用隨機組合的SQL語句測試
        long lastVisitedCount = 0;
        long lastHitCount = 0;
        long lastTime = Calendar.getInstance().getTimeInMillis();
        for (int i = 0; i < 100; i++) {
            // 每100次循環統計一次
            for (int j = 0; j < 100; j++) {
                String tableName = tableNames.get(rand.nextInt(tableNames.size()));
                List<String> columnNames = tables.get(tableName);
                String columnName = columnNames.get(rand.nextInt(columnNames.size()));
                String sql = String.format("select /"%s/" from /"%s/"", columnName, tableName);
                cache.cachedQuery(sql, true);
            }
            long visitedCount = cache.getVisitedCount();
            long hitCount = cache.getHitCount();
            long time = Calendar.getInstance().getTimeInMillis();
            System.out.println(String.format("緩存總訪問數:%d,總命中數:%d,當前大小:%d,命中率:%.2f%%,耗時:%d毫秒",
                    visitedCount,
                    hitCount,
                    cache.getSize(),
                    100.0 * (hitCount - lastHitCount) / (visitedCount - lastVisitedCount),
                    time - lastTime));
            lastVisitedCount = visitedCount;
            lastHitCount = hitCount;
            lastTime = time;
        }

        // 按任意鍵繼續
        System.out.println("按任意鍵繼續...");
        try {
            new BufferedReader(new InputStreamReader(System.in)).readLine();
        } catch (IOException ex) {
            logger.severe(ex.toString());
        }

        // 打印緩存中的鍵
        for (String key : cache.getKeySet()) {
            System.out.println(key);
        }
    }
}

 

結果是:

 

緩存總訪問數:100,總命中數:2,當前大小:98,命中率:2.00%,耗時:4157毫秒
緩存總訪問數:200,總命中數:16,當前大小:184,命中率:14.00%,耗時:2062毫秒
緩存總訪問數:300,總命中數:40,當前大小:260,命中率:24.00%,耗時:1860毫秒
緩存總訪問數:400,總命中數:74,當前大小:326,命中率:34.00%,耗時:578毫秒
緩存總訪問數:500,總命中數:111,當前大小:389,命中率:37.00%,耗時:1281毫秒
緩存總訪問數:600,總命中數:154,當前大小:446,命中率:43.00%,耗時:3391毫秒
緩存總訪問數:700,總命中數:188,當前大小:512,命中率:34.00%,耗時:953毫秒
緩存總訪問數:800,總命中數:237,當前大小:563,命中率:49.00%,耗時:1515毫秒
緩存總訪問數:900,總命中數:283,當前大小:617,命中率:46.00%,耗時:2094毫秒
緩存總訪問數:1000,總命中數:350,當前大小:650,命中率:67.00%,耗時:297毫秒
緩存總訪問數:1100,總命中數:415,當前大小:685,命中率:65.00%,耗時:531毫秒
緩存總訪問數:1200,總命中數:477,當前大小:723,命中率:62.00%,耗時:953毫秒
緩存總訪問數:1300,總命中數:538,當前大小:762,命中率:61.00%,耗時:328毫秒
緩存總訪問數:1400,總命中數:604,當前大小:796,命中率:66.00%,耗時:1172毫秒
緩存總訪問數:1500,總命中數:667,當前大小:833,命中率:63.00%,耗時:2000毫秒
緩存總訪問數:1600,總命中數:739,當前大小:861,命中率:72.00%,耗時:2000毫秒
緩存總訪問數:1700,總命中數:813,當前大小:887,命中率:74.00%,耗時:188毫秒
緩存總訪問數:1800,總命中數:893,當前大小:907,命中率:80.00%,耗時:1265毫秒
緩存總訪問數:1900,總命中數:973,當前大小:927,命中率:80.00%,耗時:516毫秒
緩存總訪問數:2000,總命中數:1055,當前大小:945,命中率:82.00%,耗時:422毫秒
緩存總訪問數:2100,總命中數:1134,當前大小:966,命中率:79.00%,耗時:516毫秒
緩存總訪問數:2200,總命中數:1212,當前大小:988,命中率:78.00%,耗時:1015毫秒
緩存總訪問數:2300,總命中數:1287,當前大小:1013,命中率:75.00%,耗時:438毫秒
緩存總訪問數:2400,總命中數:1371,當前大小:1029,命中率:84.00%,耗時:937毫秒
緩存總訪問數:2500,總命中數:1457,當前大小:1043,命中率:86.00%,耗時:750毫秒
緩存總訪問數:2600,總命中數:1540,當前大小:1060,命中率:83.00%,耗時:235毫秒
緩存總訪問數:2700,總命中數:1627,當前大小:1073,命中率:87.00%,耗時:140毫秒
緩存總訪問數:2800,總命中數:1719,當前大小:1081,命中率:92.00%,耗時:1235毫秒
緩存總訪問數:2900,總命中數:1806,當前大小:1094,命中率:87.00%,耗時:125毫秒
緩存總訪問數:3000,總命中數:1889,當前大小:1111,命中率:83.00%,耗時:312毫秒
緩存總訪問數:3100,總命中數:1981,當前大小:1119,命中率:92.00%,耗時:63毫秒
緩存總訪問數:3200,總命中數:2071,當前大小:1129,命中率:90.00%,耗時:125毫秒
緩存總訪問數:3300,總命中數:2161,當前大小:1139,命中率:90.00%,耗時:62毫秒
緩存總訪問數:3400,總命中數:2254,當前大小:1146,命中率:93.00%,耗時:172毫秒
緩存總訪問數:3500,總命中數:2344,當前大小:1156,命中率:90.00%,耗時:156毫秒
緩存總訪問數:3600,總命中數:2436,當前大小:1164,命中率:92.00%,耗時:141毫秒
緩存總訪問數:3700,總命中數:2529,當前大小:1171,命中率:93.00%,耗時:125毫秒
緩存總訪問數:3800,總命中數:2624,當前大小:1176,命中率:95.00%,耗時:78毫秒
緩存總訪問數:3900,總命中數:2717,當前大小:1183,命中率:93.00%,耗時:16毫秒
緩存總訪問數:4000,總命中數:2811,當前大小:1189,命中率:94.00%,耗時:93毫秒
緩存總訪問數:4100,總命中數:2906,當前大小:1194,命中率:95.00%,耗時:16毫秒
緩存總訪問數:4200,總命中數:3003,當前大小:1197,命中率:97.00%,耗時:47毫秒
緩存總訪問數:4300,總命中數:3095,當前大小:1205,命中率:92.00%,耗時:47毫秒
緩存總訪問數:4400,總命中數:3190,當前大小:1210,命中率:95.00%,耗時:47毫秒
緩存總訪問數:4500,總命中數:3285,當前大小:1215,命中率:95.00%,耗時:15毫秒
緩存總訪問數:4600,總命中數:3372,當前大小:1228,命中率:87.00%,耗時:500毫秒
緩存總訪問數:4700,總命中數:3471,當前大小:1229,命中率:99.00%,耗時:16毫秒
緩存總訪問數:4800,總命中數:3563,當前大小:1237,命中率:92.00%,耗時:15毫秒
緩存總訪問數:4900,總命中數:3658,當前大小:1242,命中率:95.00%,耗時:0毫秒
緩存總訪問數:5000,總命中數:3751,當前大小:1249,命中率:93.00%,耗時:32毫秒
緩存總訪問數:5100,總命中數:3846,當前大小:1254,命中率:95.00%,耗時:15毫秒
緩存總訪問數:5200,總命中數:3937,當前大小:1263,命中率:91.00%,耗時:219毫秒
緩存總訪問數:5300,總命中數:4034,當前大小:1266,命中率:97.00%,耗時:0毫秒
緩存總訪問數:5400,總命中數:4131,當前大小:1269,命中率:97.00%,耗時:16毫秒
緩存總訪問數:5500,總命中數:4230,當前大小:1270,命中率:99.00%,耗時:0毫秒
緩存總訪問數:5600,總命中數:4327,當前大小:1273,命中率:97.00%,耗時:0毫秒
緩存總訪問數:5700,總命中數:4423,當前大小:1277,命中率:96.00%,耗時:297毫秒
緩存總訪問數:5800,總命中數:4520,當前大小:1280,命中率:97.00%,耗時:703毫秒
緩存總訪問數:5900,總命中數:4618,當前大小:1282,命中率:98.00%,耗時:0毫秒
緩存總訪問數:6000,總命中數:4715,當前大小:1285,命中率:97.00%,耗時:47毫秒
緩存總訪問數:6100,總命中數:4811,當前大小:1289,命中率:96.00%,耗時:125毫秒
緩存總訪問數:6200,總命中數:4907,當前大小:1293,命中率:96.00%,耗時:31毫秒
緩存總訪問數:6300,總命中數:5005,當前大小:1295,命中率:98.00%,耗時:62毫秒
緩存總訪問數:6400,總命中數:5103,當前大小:1297,命中率:98.00%,耗時:16毫秒
緩存總訪問數:6500,總命中數:5201,當前大小:1299,命中率:98.00%,耗時:16毫秒
緩存總訪問數:6600,總命中數:5299,當前大小:1301,命中率:98.00%,耗時:0毫秒
緩存總訪問數:6700,總命中數:5397,當前大小:1303,命中率:98.00%,耗時:31毫秒
緩存總訪問數:6800,總命中數:5496,當前大小:1304,命中率:99.00%,耗時:15毫秒
緩存總訪問數:6900,總命中數:5593,當前大小:1307,命中率:97.00%,耗時:0毫秒
緩存總訪問數:7000,總命中數:5691,當前大小:1309,命中率:98.00%,耗時:0毫秒
緩存總訪問數:7100,總命中數:5791,當前大小:1309,命中率:100.00%,耗時:0毫秒
緩存總訪問數:7200,總命中數:5889,當前大小:1311,命中率:98.00%,耗時:16毫秒
緩存總訪問數:7300,總命中數:5988,當前大小:1312,命中率:99.00%,耗時:16毫秒
緩存總訪問數:7400,總命中數:6088,當前大小:1312,命中率:100.00%,耗時:0毫秒
緩存總訪問數:7500,總命中數:6185,當前大小:1315,命中率:97.00%,耗時:0毫秒
緩存總訪問數:7600,總命中數:6281,當前大小:1319,命中率:96.00%,耗時:62毫秒
緩存總訪問數:7700,總命中數:6381,當前大小:1319,命中率:100.00%,耗時:0毫秒
緩存總訪問數:7800,總命中數:6480,當前大小:1320,命中率:99.00%,耗時:31毫秒
緩存總訪問數:7900,總命中數:6580,當前大小:1320,命中率:100.00%,耗時:0毫秒
緩存總訪問數:8000,總命中數:6679,當前大小:1321,命中率:99.00%,耗時:0毫秒
緩存總訪問數:8100,總命中數:6777,當前大小:1323,命中率:98.00%,耗時:0毫秒
緩存總訪問數:8200,總命中數:6877,當前大小:1323,命中率:100.00%,耗時:0毫秒
緩存總訪問數:8300,總命中數:6975,當前大小:1325,命中率:98.00%,耗時:16毫秒
緩存總訪問數:8400,總命中數:7074,當前大小:1326,命中率:99.00%,耗時:0毫秒
緩存總訪問數:8500,總命中數:7172,當前大小:1328,命中率:98.00%,耗時:0毫秒
緩存總訪問數:8600,總命中數:7270,當前大小:1330,命中率:98.00%,耗時:16毫秒
緩存總訪問數:8700,總命中數:7368,當前大小:1332,命中率:98.00%,耗時:0毫秒
緩存總訪問數:8800,總命中數:7465,當前大小:1335,命中率:97.00%,耗時:0毫秒
緩存總訪問數:8900,總命中數:7563,當前大小:1337,命中率:98.00%,耗時:15毫秒
緩存總訪問數:9000,總命中數:7663,當前大小:1337,命中率:100.00%,耗時:0毫秒
緩存總訪問數:9100,總命中數:7763,當前大小:1337,命中率:100.00%,耗時:0毫秒
緩存總訪問數:9200,總命中數:7862,當前大小:1338,命中率:99.00%,耗時:0毫秒
緩存總訪問數:9300,總命中數:7960,當前大小:1340,命中率:98.00%,耗時:0毫秒
緩存總訪問數:9400,總命中數:8060,當前大小:1340,命中率:100.00%,耗時:0毫秒
緩存總訪問數:9500,總命中數:8158,當前大小:1342,命中率:98.00%,耗時:16毫秒
緩存總訪問數:9600,總命中數:8258,當前大小:1342,命中率:100.00%,耗時:0毫秒
緩存總訪問數:9700,總命中數:8356,當前大小:1344,命中率:98.00%,耗時:0毫秒
緩存總訪問數:9800,總命中數:8455,當前大小:1345,命中率:99.00%,耗時:0毫秒
緩存總訪問數:9900,總命中數:8554,當前大小:1346,命中率:99.00%,耗時:0毫秒
緩存總訪問數:10000,總命中數:8654,當前大小:1346,命中率:100.00%,耗時:0毫秒

 

從中大致可以看出緩存的增長趨勢以及性能不斷改進的趨勢。

 

最後的內存佔用率如下圖所示:

 

 

當然了,這也是不得已而爲之的技術,呵呵,如果項目中能用到Hibernate等高級技術的話(Hibernate內置查詢緩存了,甚至很多數據庫也內置了),還是儘量高級的吧。

 

總結這個查詢緩存應用的場合:

1、SQL語句的可能性不能無限多(例如如果語句中包含可變的時間日期就不行);
2、讀操作遠遠多於寫操作。

例如,在權限管理(登陸註銷、操作鑑權...)中,還是很合適的。

發佈了58 篇原創文章 · 獲贊 2 · 訪問量 6萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章