1. 程式人生 > >面試官:你是如何使用JDK來實現自己的快取(支援高併發)?

面試官:你是如何使用JDK來實現自己的快取(支援高併發)?

需求分析

專案中經常會遇到這種場景:一份資料需要在多處共享,有些資料還有時效性,過期自動失效。比如手機驗證碼,傳送之後需要快取起來,然後處於安全性考慮,一般還要設定有效期,到期自動失效。我們怎麼實現這樣的功能呢?

解決方案

使用現有的快取技術框架,比如redis,ehcache。優點:成熟,穩定,功能強大;缺點,專案需要引入對應的框架,不夠輕量。

如果不考慮分散式,只是在單執行緒或者多執行緒間作資料快取,其實完全可以自己手寫一個快取工具。下面就來簡單實現一個這樣的工具。

先上程式碼:

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.*;

/**
 * @Author: lixk
 * @Date: 2018/5/9 15:03
 * @Description: 簡單的記憶體快取工具類
 */
public class Cache {
    //鍵值對集合
    private final static Map<String, Entity> map = new HashMap<>();
    //定時器執行緒池,用於清除過期快取
    private final static ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();

    /**
     * 新增快取
     *
     * @param key  鍵
     * @param data 值
     */
    public synchronized static void put(String key, Object data) {
        Cache.put(key, data, 0);
    }

    /**
     * 新增快取
     *
     * @param key    鍵
     * @param data   值
     * @param expire 過期時間,單位:毫秒, 0表示無限長
     */
    public synchronized static void put(String key, Object data, long expire) {
        //清除原鍵值對
        Cache.remove(key);
        //設定過期時間
        if (expire > 0) {
            Future future = executor.schedule(new Runnable() {
                @Override
                public void run() {
                    //過期後清除該鍵值對
                    synchronized (Cache.class) {
                        map.remove(key);
                    }
                }
            }, expire, TimeUnit.MILLISECONDS);
            map.put(key, new Entity(data, future));
        } else {
            //不設定過期時間
            map.put(key, new Entity(data, null));
        }
    }

    /**
     * 讀取快取
     *
     * @param key 鍵
     * @return
     */
    public synchronized static Object get(String key) {
        Entity entity = map.get(key);
        return entity == null ? null : entity.getValue();
    }

    /**
     * 讀取快取
     *
     * @param key 鍵
     *            * @param clazz 值型別
     * @return
     */
    public synchronized static <T> T get(String key, Class<T> clazz) {
        return clazz.cast(Cache.get(key));
    }

    /**
     * 清除快取
     *
     * @param key
     * @return
     */
    public synchronized static Object remove(String key) {
        //清除原快取資料
        Entity entity = map.remove(key);
        if (entity == null) return null;
        //清除原鍵值對定時器
        Future future = entity.getFuture();
        if (future != null) future.cancel(true);
        return entity.getValue();
    }

    /**
     * 查詢當前快取的鍵值對數量
     *
     * @return
     */
    public synchronized static int size() {
        return map.size();
    }

    /**
     * 快取實體類
     */
    private static class Entity {
        //鍵值對的value
        private Object value;
        //定時器Future 
        private Future future;

        public Entity(Object value, Future future) {
            this.value = value;
            this.future = future;
        }

        /**
         * 獲取值
         *
         * @return
         */
        public Object getValue() {
            return value;
        }

        /**
         * 獲取Future物件
         *
         * @return
         */
        public Future getFuture() {
            return future;
        }
    }
}

本工具類主要採用 HashMap+定時器執行緒池 實現,map 用於儲存鍵值對資料,map的value是 Cache 的內部類物件 Entity,Entity 包含 value 和該鍵值對的生命週期定時器 Future。Cache 類對外只提供了 put(key, value), put(key, value, expire), get(key), get(key, class), remove(key), size()幾個同步方法。

當新增鍵值對資料的時候,首先會呼叫remove()方法,清除掉原來相同 key 的資料,並取消對應的定時清除任務,然後新增新資料到 map 中,並且,如果設定了有效時間,則新增對應的定時清除任務到定時器執行緒池。

測試

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

/**
 * @Author: lixk
 * @Date: 2018/5/9 16:40
 * @Description: 快取工具類測試
 */
public class CacheTest {

    /**
     * 測試
     *
     * @param args
     */
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        String key = "id";
        //不設定過期時間
        System.out.println("***********不設定過期時間**********");
        Cache.put(key, 123);
        System.out.println("key:" + key + ", value:" + Cache.get(key));
        System.out.println("key:" + key + ", value:" + Cache.remove(key));
        System.out.println("key:" + key + ", value:" + Cache.get(key));

        //設定過期時間
        System.out.println("\n***********設定過期時間**********");
        Cache.put(key, "123456", 1000);
        System.out.println("key:" + key + ", value:" + Cache.get(key));
        Thread.sleep(2000);
        System.out.println("key:" + key + ", value:" + Cache.get(key));

        /******************併發效能測試************/
        System.out.println("\n***********併發效能測試************");
        //建立有10個執行緒的執行緒池,將1000000次操作分10次新增到執行緒池
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        Future[] futures = new Future[10];
        /********新增********/
        {
            long start = System.currentTimeMillis();
            for (int j = 0; j < 10; j++) {
                futures[j] = executorService.submit(() -> {
                    for (int i = 0; i < 100000; i++) {
                        Cache.put(Thread.currentThread().getId() + key + i, i, 300000);
                    }
                });
            }
            //等待全部執行緒執行完成,列印執行時間
            for (Future future : futures) {
                future.get();
            }
            System.out.printf("新增耗時:%dms\n", System.currentTimeMillis() - start);
        }

        /********查詢********/
        {
            long start = System.currentTimeMillis();
            for (int j = 0; j < 10; j++) {
                futures[j] = executorService.submit(() -> {
                    for (int i = 0; i < 100000; i++) {
                        Cache.get(Thread.currentThread().getId() + key + i);
                    }
                });
            }
            //等待全部執行緒執行完成,列印執行時間
            for (Future future : futures) {
                future.get();
            }
            System.out.printf("查詢耗時:%dms\n", System.currentTimeMillis() - start);
        }

        System.out.println("當前快取容量:" + Cache.size());
    }
}

測試結果:

***********不設定過期時間**********
key:id, value:123
key:id, value:123
key:id, value:null

***********設定過期時間**********
key:id, value:123456
key:id, value:null

***********併發效能測試************
新增耗時:2313ms
查詢耗時:335ms
當前快取容量:1000000

測試程式使用有10個執行緒的執行緒池來模擬併發,總共執行一百萬次新增和查詢操作,時間大約都在兩秒多,`表現還不錯,每秒40萬讀寫併發應該還是可以滿足大多數高併發場景的^_^

寫在最後

  • 第一:看完點贊,感謝您的認可;
  • ...
  • 第二:隨手轉發,分享知識,讓更多人學習到;
  • ...
  • 第三:記得點關注,每天更新的!!!
  • ...

相關推薦

面試是如何使用JDK實現自己快取支援併發

需求分析 專案中經常會遇到這種場景:一份資料需要在多處共享,有些資料還有時效性,過期自動失效。比如手機驗證碼,傳送之後需要快取起來,然後處於安全性考慮,一般還要設定有效期,到期自動失效。我們怎麼實現這樣的功能呢? 解決方案 使用現有的快取技術框架,比如redis,ehcache。優點:成熟,穩定,功能強大;

面試看過Redis資料結構底層實現嗎?

面試中,redis也是很受面試官親睞的一部分。我向在這裡講的是redis的底層資料結構,而不是你理解的五大資料結構。你有沒有想過redis底層是怎樣的資料結構呢,他們和我們java中的HashMap、List、等使用的資料結構有什麼區別呢。 1. 字串處理(string) 我們都知道redis是用C語言寫

面試瞭解過Redis物件底層實現

上一章我們講了Redis的底層資料結構,不瞭解的人可能會有疑問:這個和平時用的五大物件有啥關係呢?這一章我們就主要解釋他們所建立的聯絡。 看這個文章之前,如果對ziplist、skiplist、intset等資料結構不熟悉的話,建議先回顧一下上一章節:面試官:你看過Redis資料結構底層實現嗎? 0. 五

面試精通 Docker,那詳細說說 Dockerfile 吧

接上一篇:30分鐘快速上手Docker,看這篇就對了! 一、 帶著問題學Dockerfile 1、疑問 我們都知道從遠端倉庫可以pull一個tomcat等映象下來,然後docker run啟動容器,然後docker exec -it 容器id /bin/bash進入容器,往webapps下仍我們的程式。等等這

怎麼回答面試對Spring的理解

spring呢,是pivotal公司維護的一系列開源工具的總稱,最為人所知的是spring mvc,事實上,他們都是基於spring framework,並且再其上繼續增強,為某一方面服務的java元件。最近spring framework 剛升級到5,非常不錯。比較常見的有

面試為什麼要離開之前的公司?

離職、跳槽是一件很正常的事。但是難免在面試時會被問到:“你為什麼要離開之前的公司?”首先,你要知道面試官這麼問的目的是什麼?面試官問這個問題,是想知道你離開之前公司的原因是否合理,是想知道你是否對公司忠誠、有熱情、感興趣,想考察你動機是否單純、是否只是把公司當成一個“避難所”

面試分析過SpringMVC的源碼嗎?

技術分享 rop 調用 直接 setview gate rem code 上傳 1. MVC使用 在研究源碼之前,先來回顧以下springmvc 是如何配置的,這將能使我們更容易理解源碼。 1.1 web.xml <servlet> <servle

【BAT面試題系列】面試了解樂觀鎖和悲觀鎖嗎?

次數 catch val util overflow info 基本概念 因此 問題 前言 樂觀鎖和悲觀鎖問題,是出現頻率比較高的面試題。本文將由淺入深,逐步介紹它們的基本概念、實現方式(含實例)、適用場景,以及可能遇到的面試官追問,希望能夠幫助你打動面試官。 目錄

面試有m個雞蛋,如何用最少的次數測出雞蛋會在哪一層碎?

假設你面前有一棟n層的大樓和m個雞蛋,假設將雞蛋從f層或更高的地方放扔下去,雞蛋才會碎,否則就不會。你需要設計一種策略來確定f的值,求最壞情況下扔雞蛋次數的最小值。 leetcode原題連結 乍一看這道題很抽象,可能有的人一看到這個題目從來沒做過,就懵逼了。其實不用慌張,再花裡胡哨的題目,最後都可以抽象成我們

面試連RESTful都不知道我怎麼敢要

目錄 01 前言 02 RESTful的來源 03 RESTful6大原則 1. C-S架構 2. 無狀態 3.統一的介面 4.一致的資料格式

面試系列-面試能給我解釋一下javascript中的this嗎?

一.前言   關於javascript中的this物件,可能已經被大家說爛了。   即使是這樣,我依然決定將這篇文章給水出來。畢竟全國在新型肺炎的影響下,公司沒法正常復工。   除了刷刷手機,還是要適當的學習一下。   廢柴是真不好當,勞逸結合才是王道。 二.正戲開始   面試官:你能給我解釋一下javasc

【原創】面試回去等通知吧!

這是why技術的第37篇原創文章 老規矩,先聊聊生活,上面這張圖片是我週一拍的。 週一晚上下班後發現公司樓下推著三輪車賣花的阿姨又開始買花了。整個路口只有她一個人在做生意,整條路上也沒有幾個人,大家都低著頭匆匆走著,繁花中帶著點憂傷。 於是,我去買了一把白玫瑰。 上週日把《霍亂時期的愛情》看完了,就剛好當

Java堆記憶體是執行緒共享的!面試確定嗎?

Java作為一種面向物件的,跨平臺語言,其物件、記憶體等一直是比較難的知識點,所以,即使是一個Java的初學者,也一定或多或少的對JVM有一些瞭解。可以說,關於JVM的相關知識,基本是每個Java開發者必學的知識點,也是面試的時候必考的知識點。 在JVM的記憶體結構中,比較常見的兩個區域就是堆記憶體和棧記憶

面試知道哪些事務失效的場景?

前言 宣告式事務是Spring功能中最爽之一,可是有些時候,我們在使用宣告式事務並未生效,這是為什麼呢? 文章首發於微信公眾號【碼猿技術專欄】 今天陳某帶大家來聊一聊宣告事務的幾種失效場景。本文將會從以下兩個方面來說一下事務為什麼會失效? @Transactional介紹 @Transactional失效場

面試剛說喜歡研究新技術,那麼請說說對 Blazor 的瞭解

閱讀本文大概需要 1.5 分鐘。 最近在幾個微信 .NET 交流群裡大家討論比較頻繁的話題就是這幾天自己的面試經歷。 面試官:“你剛說你喜歡研究新技術,那麼你對 Blazor 瞭解多少?”。 作為一位專注於 .NET 開發的軟體工程師,你好意思說你對 Blazor 一點也不解嗎?.NET 新技術也就是那

面試經歷過資料庫遷移麼?有哪些注意點和難點?

前言 最近寫了很多資料庫相關的文章,大家基本上對資料庫也有了很多的瞭解,資料庫本身有所瞭解了,我們是不是應該回歸業務本身呢? 大家去了解過自己企業資料庫的部署方式麼?是怎麼部署的,又是部署在哪裡的?部署過程中可能會出現的問題有哪些? 是主從?還是雙主?有沒有分庫?大的表做了分表沒?等等...部署方式大概率也都

面試說說互斥鎖、自旋鎖、讀寫鎖、悲觀鎖、樂觀鎖的應用場景

前言 生活中用到的鎖,用途都比較簡單粗暴,上鎖基本是為了防止外人進來、電動車被偷等等。 但生活中也不是沒有 BUG 的,比如加鎖的電動車在「廣西 - 竊·格瓦拉」面前,鎖就是形同虛設,只要他願意,他就可以輕輕鬆鬆地把你電動車給「順走」,不然打工怎麼會是他這輩子不可能的事情呢?牛逼之人,必有牛

面試真的瞭解Redis分散式鎖嗎?

![](https://img2020.cnblogs.com/blog/1478697/202101/1478697-20210113230123649-1602595153.jpg) ### 什麼是分散式鎖 說到Redis,我們第一想到的功能就是可以快取資料,除此之外,Redis因為單程序、效能高的

面試說說ReentrantLock和Synchronized區別

大家好!又和大家見面了。為了避免面試尷尬,今天同比較通俗語言和大家聊下ReentrantLock和Synchronized區別!   使用方式 Synchronized可以修飾例項方法,靜態方法,程式碼塊。自動釋放鎖。 ReentrantLock一般需要try catch finally語句,在t

面試問題瞭解Java記憶體模型麼Java7、8、9記憶體模型的區別

Java記憶體模型是每個java程式設計師必須掌握理解的,這是Java的核心基礎,對我們編寫程式碼特別是併發程式設計時有很大幫助。由於Java程式是交由JVM執行的,所以我們在談Java記憶體區域劃分的時候事實上是指JVM記憶體區域劃分。 首先,我們回顧一下Java程式執行