主頁 > 資料庫 > DiskLruCache 的使用及原始碼決議

DiskLruCache 的使用及原始碼決議

2020-11-13 16:56:21 資料庫

目錄

        • 一、DiskLruCache 的使用
          • 1、DiskLruCache 的創建
          • 2、DiskLruCache 的添加
          • 3、DiskLruCache 的查找
          • 4、DiskLruCache 的移除
        • 二、部分原始碼決議
          • 1、快取日志 journal
          • 2、DiskLruCache 的 open()
          • 3、DiskLruCache 的 edit()
          • 4、DiskLruCache 的 get()
          • 5、DiskLruCache 的 remove()
          • 6、DiskLruCache 的 close()
          • 7、DiskLruCache 的 delete()
          • 8、DiskLruCache 的 size()
          • 9、DiskLruCache 的 flush()
        • 三、DiskLruCache 完整原始碼
        • 四、參考資料

DiskLruCache 用于實作存盤設備快取,即磁盤快取,它通過將快取物件寫入檔案系統從而實作快取的效果,DiskLruCache 得到了 Android 官方檔案的推薦,但它不屬于 Android SDK 的一部分,它的 原始碼及網址文末會貼出來,下面分別從 DiskLruCache 的創建、快取查找和快取添加這三個方面來介紹 DiskLruCache 的使用方式,

一、DiskLruCache 的使用

如前已知,DiskLruCache 不屬于 Android SDK 的一部分,且需要存盤權限

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

需要在 build.gradle 中配置

implementation 'com.jakewharton:disklrucache:2.0.2'

然后可以開始使用 DiskLruCache 了,

1、DiskLruCache 的創建

DiskLruCache 并不能通過構造方法來創建,它提供了 open() 方法用于創建自身

public static DiskLruCache open(File directory, int appVersion, invalueCount, long maxSize)

open() 方法有四個引數,第一個引數表示磁盤快取在檔案系統中的存盤路徑,快取路徑可以選擇 SD 卡上的快取目錄,具體是指 /sdcard/Android/data/<package_name>/cache 目錄,其中 <package_name> 表示當前應用的包名,當應用被卸載后,此目錄會一并被洗掉,當然也可以選擇 SD 卡上的其他指定目錄,還可以選擇 data 下的當前應用的目錄,具體可根據需要靈活設定,這里給出一個建議:如果應用卸載后就希望洗掉快取檔案,那么就選擇 SD 卡上的快取目錄,如果希望保留快取資料那就應該選擇 SD 卡上的其他特定目錄,
第二個引數表示應用的版本號,一般設為 1 即可,當版本號發生改變時 DiskLruCache 會清空之前所有的快取檔案,而這個特性在實際開發中作用并不大,很多情況下即使應用的版本號發生了改變快取檔案卻仍然是有效的,因此這個引數設為 1 比較好,
第三個引數表示同一個 key 可以對應多少個快取檔案,一般設為 1 即可,
第四個引數表示快取的總大小,比如 50MB,當快取大小超出這個設定值后,DiskLruCache 會清除一些快取從而保證總大小不大于這個設定值,
下面是一個典型的DiskLruCache的創建程序

private static final long DISK_CACHE_SIZE = 1024102450; //50MB
	File diskCacheDir = getDiskCacheDir(mContext, "bitmap");
    if (! diskCacheDir.exists()) {       	
    	diskCacheDir.mkdirs();
    }
    mDiskLruCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_CACHE_SIZE);

其中,getDiskCacheDir() 方法如下

public File getDiskCacheDir(Context context, String uniqueName) {
	String cachePath;
	if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
			|| !Environment.isExternalStorageRemovable()) {
		cachePath = context.getExternalCacheDir().getPath();
	} else {
		cachePath = context.getCacheDir().getPath();
	}
	return new File(cachePath + File.separator + uniqueName);
}

當 SD 卡存在或者 SD 卡不可被移除的時候,就呼叫getExternalCacheDir() 方法來獲取快取路徑,否則就呼叫getCacheDir() 方法來獲取快取路徑,前者獲取到的就是 /sdcard/Android/data/<application package>/cache 這個路徑,而后者獲取到的是 /data/data/<application package>/cache 這個路徑,

最后將獲取到的路徑和一個 uniqueName 進行拼接,作為最終的快取路徑回傳, uniqueName 是對不同型別的資料進行區分而設定的一個唯一值,比如 bitmap、file 等檔案夾,

2、DiskLruCache 的添加

DiskLruCache 的快取添加的操作是通過 Editor 完成的,Editor 表示一個快取物件的編輯物件,這里仍然以圖片快取舉例,首先需要獲取圖片url 所對應的 key,然后根據 key 就可以通過 edit() 來獲取 Editor 物件,如果這個快取正在被編輯,那么 edit() 會回傳 null,即 DiskLruCache 不允許同時編輯一個快取物件,之所以要把 url 轉換成 key,是因為圖片的 url 中很可能有特殊字符,這將影響 url 在 Android 中直接使用,一般采用 url 的 md5 值作為 key,如下所示

private String hashKeyFormUrl(String url) {
	String cacheKey;
    try {
         final MessageDigest mDigest = MessageDigest.getInstance("MD5");
         mDigest.update(url.getBytes());
         cacheKey = bytesToHexString(mDigest.digest());
    } catch (NoSuchAlgorithmException e) {
         cacheKey = String.valueOf(url.hashCode());
    }
      return cacheKey;
    }

private String bytesToHexString(byte[] bytes) {
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < bytes.length; i++) {
        String hex = Integer.toHexString(0xFF & bytes[i]);
        if (hex.length() == 1) {
            sb.append('0');
        }
         sb.append(hex);
    }
    return sb.toString();
}

將圖片的 url 轉成 key 后,就可以獲取 Editor 物件了,對于這個 key 來說,如果當前不存在其他 Editor 物件,那么 edit() 就會回傳一個新的 Editor 物件,通過它就可以得到一個檔案輸出流,需要注意的是,由于前面在 DiskLruCache 的 open 方法中設定了一個節點只能有一個資料,因此下面的 DISK_CACHE_INDEX 常量直接設為 0 即可,如下所示

String key = hashKeyFormUrl(url);
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if (editor ! = null) {
   OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
}

當從網路下載圖片時,圖片就可以通過檔案輸出流寫入到檔案系統上,這個程序的實作如下所示

public boolean downloadUrlToStream(String urlString, utputStream outputStream) {
	HttpURLConnection urlConnection = null;
    BufferedOutputStream out = null;
    BufferedInputStream in = null;
    try {
		final URL url = new URL(urlString);
		urlConnection = (HttpURLConnection)url.openConnection();
		in = new BufferedInputStream(urlConnection.getInputStream(), IO_BUFFER_SIZE);
        out = new BufferedOutputStream(outputStream, IO_BUFFER_SIZE);
		int b;
		while ((b = in.read()) ! = -1) {
			out.write(b);
		}
		return true;
	} catch (IOException e) {
		Log.e(TAG, "Download bitmap failed. " + e);
	} finally {
		if (urlConnection ! = null) {
		urlConnection.disconnect();
	}			
		MyUtils.close(out);
		MyUtils.close(in);
	}
    return false;
}

其中 MyUtils 原始碼在這里,之后,還必須通過 Editor 的 commit() 來提交寫入操作,真正地將圖片寫入檔案系統,如果圖片下載程序發生了例外,那么還可以通過 Editor 的 abort() 來回退整個操作,這個程序如下所示

OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
if (downloadUrlToStream(url, outputStream)) {
	editor.commit();
} else {
	editor.abort();
}
mDiskLruCache.flush();

經過上面的幾個步驟,圖片已經被正確地寫入到檔案系統了,接下來圖片獲取的操作就不需要請求網路了,

3、DiskLruCache 的查找

和快取的添加程序類似,快取查找程序也需要將 url 轉換為 key,然后通過 DiskLruCache 的 get() 方法得到一個 Snapshot 物件,接著再通過 Snapshot 物件即可得到快取的檔案輸入流,有了檔案輸入流,自然就可以得到 Bitmap 物件了,為了避免加載圖片程序中導致的 OOM 問題,一般不建議直接加載原始圖片,可以通過 BitmapFactory.Options 物件來加載一張縮放后的圖片,但是那種方法對 FileInputStream 的縮放存在問題,原因是 FileInputStream 是一種有序的檔案流,而兩次 decodeStream 呼叫影響了檔案流的位置屬性,導致了第二次 decodeStream 時得到的是 null,為了解決這個問題,可以通過檔案流來得到它所對應的檔案描述符,然后再通過 BitmapFactory.decodeFileDescriptor() 方法來加載一張縮放后的圖片,這個程序的實作如下所示

Bitmap bitmap = null;
String key = hashKeyFormUrl(url);
DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
if (snapShot ! = null) {
	FileInputStream fileInputStream = (FileInputStream)snapShot.getInputStream(DISK_CACHE_INDEX);
	FileDescriptor fileDescriptor = fileInputStream.getFD();
	bitmap = mImageResizer.decodeSampledBitmapFromFileDescriptor(fileDescriptor, reqWidth, reqHeight);
    if (bitmap ! = null) {
		addBitmapToMemoryCache(key, bitmap);
	}
}

每個物體都是檔案,你可以利用 fileInputStream 讀取出里面的內容,然后做其他操作,上面介紹了 DiskLruCache 的創建、添加和查找程序,除此之外,DiskLruCache 還提供了 remove() 、delete() 等方法用于磁盤快取的洗掉操作,

4、DiskLruCache 的移除

移除快取主要是借助 DiskLruCache 的 remove() 方法實作的,原始碼如下所示

    /**
     * Drops the entry for {@code key} if it exists and can be removed. Entries
     * actively being edited cannot be removed.
     *
     * @return true if an entry was removed.
     */
    public synchronized boolean remove(String key) throws IOException {
        checkNotClosed();
        validateKey(key);
        Entry entry = lruEntries.get(key);
        if (entry == null || entry.currentEditor != null) {
            return false;
        }
        for (int i = 0; i < valueCount; i++) {
            File file = entry.getCleanFile(i);
            if (!file.delete()) {
                throw new IOException("failed to delete " + file);
            }
            size -= entry.lengths[i];
            entry.lengths[i] = 0;
        }
        redundantOpCount++;
        journalWriter.append(REMOVE + ' ' + key + '\n');
        lruEntries.remove(key);
        if (journalRebuildRequired()) {
            executorService.submit(cleanupCallable);
        }
        return true;
    }

remove() 方法中要求傳入一個 key,然后會洗掉這個 key 對應的快取,
示例代碼如下

try {
	String imageUrl = "https://chittyo/img.jpghttps://img-blog.csdnimg.cn/2020110614184215.jpg alt="在這里插入圖片描述" />
可以看到,一個名稱很長的檔案和一個 journal 檔案,其中,名稱很長的檔案是被快取的檔案,它的名稱是 MD5 編碼之后的,我們來看看 journal 檔案中的內容是什么樣的吧,如下所示

libcore.io.DiskLruCache
1
1
1

DIRTY 27c7e00adbacc71dc793e5e7bf02f861
CLEAN 27c7e00adbacc71dc793e5e7bf02f861 1208
READ 27c7e00adbacc71dc793e5e7bf02f861
DIRTY b80f9eec4b616dc6682c7fa8bas2061f
CLEAN b80f9eec4b616dc6682c7fa8bas2061f 1208
READ b80f9eec4b616dc6682c7fa8bas2061f
DIRTY be3fgac81c12a08e89088555d85dfd2b
CLEAN be3fgac81c12a08e89088555d85dfd2b 99
READ be3fgac81c12a08e89088555d85dfd2b
DIRTY 536990f4dbddfghcfbb8f350a941wsxd
REMOVE 536990f4dbddfghcfbb8f350a941wsxd

來看一下原始碼注釋

    /*
     * This cache uses a journal file named "journal". A typical journal file
     * looks like this:
     *     libcore.io.DiskLruCache
     *     1
     *     100
     *     2
     *
     *     CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
     *     DIRTY 335c4c6028171cfddfbaae1a9c313c52
     *     CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
     *     REMOVE 335c4c6028171cfddfbaae1a9c313c52
     *     DIRTY 1ab96a171faeeee38496d8b330771a7a
     *     CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
     *     READ 335c4c6028171cfddfbaae1a9c313c52
     *     READ 3400330d1dfc7f3f7f4b8d4d803dfcf6
     *
     * The first five lines of the journal form its header. They are the
     * constant string "libcore.io.DiskLruCache", the disk cache's version,
     * the application's version, the value count, and a blank line.
     *
     * Each of the subsequent lines in the file is a record of the state of a
     * cache entry. Each line contains space-separated values: a state, a key,
     * and optional state-specific values.
     *   o DIRTY lines track that an entry is actively being created or updated.
     *     Every successful DIRTY action should be followed by a CLEAN or REMOVE
     *     action. DIRTY lines without a matching CLEAN or REMOVE indicate that
     *     temporary files may need to be deleted.
     *   o CLEAN lines track a cache entry that has been successfully published
     *     and may be read. A publish line is followed by the lengths of each of
     *     its values.
     *   o READ lines track accesses for LRU.
     *   o REMOVE lines track entries that have been deleted.
     *
     * The journal file is appended to as cache operations occur. The journal may
     * occasionally be compacted by dropping redundant lines. A temporary file named
     * "journal.tmp" will be used during compaction; that file should be deleted if
     * it exists when the cache is opened.
     */

來看一下前五行:

  1. libcore.io.DiskLruCache 是固定字串,表明使用的是 DiskLruCache 技術;
  2. DiskLruCache 的版本號,原始碼中為常量 1;
  3. APP 的版本號,即我們在 open() 方法里傳入的版本號;
  4. valueCount,這個值也是在 open() 方法中傳入的,指每個 key 對應幾個檔案,通常情況下都為 1;
  5. 空行

前五行是該檔案的檔案頭,DiskLruCache 初始化的時候,如果該檔案存在,就需要校驗該檔案頭,

接下來看下操作記錄:以 DIRTY 為前綴開始的行,后面是快取檔案的 key,DIRTY 英文是“臟的” 的意思,此處譯為臟資料,每當我們呼叫一次 DiskLruCache 的 edit() 方法時,都會向 journal 檔案中寫入一條 DIRTY 記錄,表示我們正準備寫入一條快取資料,但不知結果如何,然后呼叫 commit() 方法表示寫入快取成功,這時會向 journal 中寫入一條 CLEAN 記錄,意味著這條 “臟” 資料被 “洗干凈了” ,呼叫 abort() 方法表示寫入快取失敗,這時會向 journal 中寫入一條 REMOVE 記錄,也就是說,每一行 DIRTY 的 key,后面都應該有一行對應的 CLEAN 或者 REMOVE 的記錄,否則這條資料就是 “臟” 的,會被自動洗掉掉,

REMOVE 除了上述的情況,當你自己手動呼叫 remove(key) 方法的時候也會寫入一條 REMOVE 記錄,

如果你足夠細心的話應該還會注意到,第七行的那條記錄,除了 CLEAN 前綴和 key 之外,后面還有一個1208,這是什么意思呢?其實,DiskLruCache 會在每一行 CLEAN 記錄的最后加上該條快取資料的大小,以位元組為單位,1208 也就是我們快取檔案的位元組數了,原始碼中的 size() 方法可以獲取到當前快取路徑下所有快取資料的總位元組數,其實它的作業原理就是把 journal 檔案中所有 CLEAN 記錄的位元組數相加,回傳求出的總和,

前綴是 READ 的記錄,每當我們呼叫 get() 方法去讀取一條快取資料時,就會向 journal 檔案中寫入一條 READ 記錄,因此,圖片和資料量都非常大的 APP 的 journal 檔案中就可能會有大量的 READ 記錄,那如果我不停地頻繁操作的話,就會不斷地向 journal 檔案中寫入資料,那這樣 journal 檔案豈不是會越來越大?這倒不必擔心,DiskLruCache 中使用了一個 redundantOpCount 變數來記錄用戶操作的次數,每執行一次寫入、讀取或移除快取的操作,這個變數值都會加 1,當變數值達到 2000 的時候就會觸發重構 journal 的事件,這時會自動把 journal 中一些多余的、不必要的記錄全部清除掉,保證 journal 檔案的大小始終保持在一個合理的范圍內,

2、DiskLruCache 的 open()
  /**
   * Opens the cache in {@code directory}, creating a cache if none exists
   * there.
   *
   * @param directory a writable directory
   * @param valueCount the number of values per cache entry. Must be positive.
   * @param maxSize the maximum number of bytes this cache should use to store
   * @throws IOException if reading or writing the cache directory fails
   */
  public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
      throws IOException {
    if (maxSize <= 0) {
      throw new IllegalArgumentException("maxSize <= 0");
    }
    if (valueCount <= 0) {
      throw new IllegalArgumentException("valueCount <= 0");
    }

    // If a bkp file exists, use it instead.
    File backupFile = new File(directory, JOURNAL_FILE_BACKUP);
    if (backupFile.exists()) {
      File journalFile = new File(directory, JOURNAL_FILE);
      // If journal file also exists just delete backup file.
      if (journalFile.exists()) {
        backupFile.delete();
      } else {
        renameTo(backupFile, journalFile, false);
      }
    }

    // Prefer to pick up where we left off.
    DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
    if (cache.journalFile.exists()) {
      try {
        cache.readJournal();
        cache.processJournal();
        return cache;
      } catch (IOException journalIsCorrupt) {
        System.out
            .println("DiskLruCache "
                + directory
                + " is corrupt: "
                + journalIsCorrupt.getMessage()
                + ", removing");
        cache.delete();
      }
    }

    // Create a new empty cache.
    directory.mkdirs();
    cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
    cache.rebuildJournal();
    return cache;
  }

首先檢查 journal 的備份檔案是否存在( journal.bkp),如果備份存在,然后檢查 journal 檔案是否存在,如果 journal 檔案存在,備份檔案就可以洗掉了;如果 journal 檔案不存在,將備份檔案檔案重命名為 journal 檔案,

然后檢查 journal 檔案是否存在:

  • 如果不存在,創建 directory,重新構造 DiskLruCache,再呼叫 rebuildJournal() 建立 journal 檔案,
  /**
   * Creates a new journal that omits redundant information. This replaces the
   * current journal if it exists.
   */
  private synchronized void rebuildJournal() throws IOException {
    if (journalWriter != null) {
      journalWriter.close();
    }

    Writer writer = new BufferedWriter(
        new OutputStreamWriter(new FileOutputStream(journalFileTmp), Util.US_ASCII));
    try {
      writer.write(MAGIC);
      writer.write("\n");
      writer.write(VERSION_1);
      writer.write("\n");
      writer.write(Integer.toString(appVersion));
      writer.write("\n");
      writer.write(Integer.toString(valueCount));
      writer.write("\n");
      writer.write("\n");

      for (Entry entry : lruEntries.values()) {
        if (entry.currentEditor != null) {
          writer.write(DIRTY + ' ' + entry.key + '\n');
        } else {
          writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
        }
      }
    } finally {
      writer.close();
    }

    if (journalFile.exists()) {
      renameTo(journalFile, journalFileBackup, true);
    }
    renameTo(journalFileTmp, journalFile, false);
    journalFileBackup.delete();

    journalWriter = new BufferedWriter(
        new OutputStreamWriter(new FileOutputStream(journalFile, true), Util.US_ASCII));
  }

可以看到首先構建一個 journal.tmp 檔案,然后寫入檔案頭(5行),然后遍歷 lruEntries( LinkedHashMap<String, Entry> lruEntries =
new LinkedHashMap<String, Entry>(0, 0.75f, true); ),此時 lruEntries 里沒有任何資料,接下來將 tmp 檔案重命名為 journal 檔案,這樣一個 journal 檔案便生成了,

  • 如果存在,那么呼叫 readJournal()
private void readJournal() throws IOException {
    StrictLineReader reader = new StrictLineReader(new FileInputStream(journalFile), Util.US_ASCII);
    try {
      String magic = reader.readLine();
      String version = reader.readLine();
      String appVersionString = reader.readLine();
      String valueCountString = reader.readLine();
      String blank = reader.readLine();
      if (!MAGIC.equals(magic)
          || !VERSION_1.equals(version)
          || !Integer.toString(appVersion).equals(appVersionString)
          || !Integer.toString(valueCount).equals(valueCountString)
          || !"".equals(blank)) {
        throw new IOException("unexpected journal header: [" + magic + ", " + version + ", "
            + valueCountString + ", " + blank + "]");
      }

      int lineCount = 0;
      while (true) {
        try {
          readJournalLine(reader.readLine());
          lineCount++;
        } catch (EOFException endOfJournal) {
          break;
        }
      }
      redundantOpCount = lineCount - lruEntries.size();

      // If we ended on a truncated line, rebuild the journal before appending to it.
      if (reader.hasUnterminatedLine()) {
        rebuildJournal();
      } else {
        journalWriter = new BufferedWriter(new OutputStreamWriter(
            new FileOutputStream(journalFile, true), Util.US_ASCII));
      }
    } finally {
      Util.closeQuietly(reader);
    }
  }

首先校驗檔案頭,接下來呼叫 readJournalLine 按行讀取內容,

private void readJournalLine(String line) throws IOException {
    int firstSpace = line.indexOf(' ');
    if (firstSpace == -1) {
      throw new IOException("unexpected journal line: " + line);
    }

    int keyBegin = firstSpace + 1;
    int secondSpace = line.indexOf(' ', keyBegin);
    final String key;
    if (secondSpace == -1) {
      key = line.substring(keyBegin);
      if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) {
        lruEntries.remove(key);
        return;
      }
    } else {
      key = line.substring(keyBegin, secondSpace);
    }

    Entry entry = lruEntries.get(key);
    if (entry == null) {
      entry = new Entry(key);
      lruEntries.put(key, entry);
    }

    if (secondSpace != -1 && firstSpace == CLEAN.length() && line.startsWith(CLEAN)) {
      String[] parts = line.substring(secondSpace + 1).split(" ");
      entry.readable = true;
      entry.currentEditor = null;
      entry.setLengths(parts);
    } else if (secondSpace == -1 && firstSpace == DIRTY.length() && line.startsWith(DIRTY)) {
      entry.currentEditor = new Editor(entry);
    } else if (secondSpace == -1 && firstSpace == READ.length() && line.startsWith(READ)) {
      // This work was already done by calling lruEntries.get().
    } else {
      throw new IOException("unexpected journal line: " + line);
    }
  }

journal 日志每一行中的各個部分都是用 ' ' 空格來分割的,所以先用 空格來截取一下,拿到 key,然后判斷 firstSpace 是 REMOVE 就會呼叫 lruEntries.remove(key); ,若不是 REMOVE ,如果該 key 沒有加入到 lruEntries ,則創建并且加入,然后繼續判斷 firstSpace ,若是 CLEAN ,則初始化 entry ,設定 readable=true , currentEditor 為 null ,初始化長度等,若是 DIRTY ,則設定 currentEditor 物件,若是 READ,無操作,

一般正常操作下 DIRTY 不會單獨出現,會和 REMOVE or CLEAN 成對出現;經過上面這個流程,基本上加入到 lruEntries 里面的只有 CLEAN 且沒有被 REMOVE 的 key,

然后我們回到 readJournal 方法,在我們按行讀取的時候,會記錄一下 lineCount ,然后賦值給 redundantOpCount ,該變數記錄的是多余的記錄條數( redundantOpCount = lineCount - lruEntries.size(); 檔案的行數-真正可以的 key 的行數),最后,如果讀取程序中發現 journal 檔案有問題,則重建 journal 檔案,沒有問題的話,初始化下 journalWriter,關閉 reader,

我們再回到 open() 方法中,readJournal 完成了,會繼續呼叫 processJournal() 這個方法

  /**
   * Computes the initial size and collects garbage as a part of opening the
   * cache. Dirty entries are assumed to be inconsistent and will be deleted.
   */
  private void processJournal() throws IOException {
    deleteIfExists(journalFileTmp);
    for (Iterator<Entry> i = lruEntries.values().iterator(); i.hasNext(); ) {
      Entry entry = i.next();
      if (entry.currentEditor == null) {
        for (int t = 0; t < valueCount; t++) {
          size += entry.lengths[t];
        }
      } else {
        entry.currentEditor = null;
        for (int t = 0; t < valueCount; t++) {
          deleteIfExists(entry.getCleanFile(t));
          deleteIfExists(entry.getDirtyFile(t));
        }
        i.remove();
      }
    }
  }

計算快取初始大小,賦值給 size,并收集垃圾作為打開快取的一部分,對于所有非法 DIRTY 狀態(就是 DIRTY 單獨出現的)的 entry,如果存在 檔案則洗掉,并且從 lruEntries 中移除,此時,剩下的就只有 CLEAN 狀態的 key 記錄了,

到此 DiskLruCache 就初始化完畢了,為了方便記憶,我們來捋一下流程:

根據我們傳入的 directory,去找 journal 檔案,如果沒找到,則創建一個,只寫入檔案頭 (5行) ,如果找到,則遍歷該檔案,將里面所有的 CLEAN 記錄的 key,存到 lruEntries 中,
經過 open 以后,journal 檔案肯定存在了,lruEntries 里面肯定有值了,size 為當前所有物體占據的容量,

3、DiskLruCache 的 edit()
  /**
   * Returns an editor for the entry named {@code key}, or null if another
   * edit is in progress.
   */
  public Editor edit(String key) throws IOException {
    return edit(key, ANY_SEQUENCE_NUMBER);
  }

  private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
    checkNotClosed();
    validateKey(key);
    Entry entry = lruEntries.get(key);
    if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null
        || entry.sequenceNumber != expectedSequenceNumber)) {
      return null; // Snapshot is stale.
    }
    if (entry == null) {
      entry = new Entry(key);
      lruEntries.put(key, entry);
    } else if (entry.currentEditor != null) {
      return null; // Another edit is in progress.
    }

    Editor editor = new Editor(entry);
    entry.currentEditor = editor;

    // Flush the journal before creating files to prevent file leaks.
    journalWriter.write(DIRTY + ' ' + key + '\n');
    journalWriter.flush();
    return editor;
  }
  
 private void validateKey(String key) {
    Matcher matcher = LEGAL_KEY_PATTERN.matcher(key);
    if (!matcher.matches()) {
      throw new IllegalArgumentException("keys must match regex "
              + STRING_KEY_PATTERN + ": \"" + key + "\"");
    }
 }

private void checkNotClosed() {
    if (journalWriter == null) {
      throw new IllegalStateException("cache is closed");
    }
  }

首先驗證 key,必須是由字母、數字、下劃線、橫線(-) 組成,且長度在 1-120 之間,然后通過 key 獲取物體 Entry ,如果 Entry 不存在,則創建一個 Entry 加入到 lruEntries 中;如果存在且不是正在編輯的物體,則直接使用,然后為 entry.currentEditor 進行賦值為 new Editor(entry); ,最后在 journal 檔案中寫入一條 DIRTY 記錄,代表這個檔案正在被操作,拿到 editor 物件以后,就是去呼叫 newOutputStream 去獲得一個檔案輸入流了,

    /**
     * Returns a new unbuffered output stream to write the value at
     * {@code index}. If the underlying output stream encounters errors
     * when writing to the filesystem, this edit will be aborted when
     * {@link #commit} is called. The returned output stream does not throw
     * IOExceptions.
     */
    public OutputStream newOutputStream(int index) throws IOException {
      if (index < 0 || index >= valueCount) {
        throw new IllegalArgumentException("Expected index " + index + " to "
                + "be greater than 0 and less than the maximum value count "
                + "of " + valueCount);
      }
      synchronized (DiskLruCache.this) {
        if (entry.currentEditor != this) {
          throw new IllegalStateException();
        }
        if (!entry.readable) {
          written[index] = true;
        }
        File dirtyFile = entry.getDirtyFile(index);
        FileOutputStream outputStream;
        try {
          outputStream = new FileOutputStream(dirtyFile);
        } catch (FileNotFoundException e) {
          // Attempt to recreate the cache directory.
          directory.mkdirs();
          try {
            outputStream = new FileOutputStream(dirtyFile);
          } catch (FileNotFoundException e2) {
            // We are unable to recover. Silently eat the writes.
            return NULL_OUTPUT_STREAM;
          }
        }
        return new FaultHidingOutputStream(outputStream);
      }
    }

首先校驗 index 是否在 (0, valueCount] 范圍內,一般我們使用都是一個 key 對應一個檔案,所以傳入的基本都是 0,接下來就是通過 entry.getDirtyFile(index); 拿到一個 dirtyFile 物件,其實就是個中轉檔案,檔案格式為 key.index.tmp ,將這個檔案的 FileOutputStream 通過 FaultHidingOutputStream 封裝下傳給我們,

最后,我們通過 outputStream 寫入資料以后,需要呼叫 commit 方法

    /**
     * Commits this edit so it is visible to readers.  This releases the
     * edit lock so another edit may be started on the same key.
     */
    public void commit() throws IOException {
      if (hasErrors) {
        completeEdit(this, false);
        remove(entry.key); // The previous entry is stale.
      } else {
        completeEdit(this, true);
      }
      committed = true;
    }

首先通過 hasErrors 判斷,是否有錯誤發生,如果有就呼叫 completeEdit(this, false); 和 remove(entry.key); ;如果沒有就呼叫 completeEdit(this, true); ,

那么這里這個 hasErrors 哪來的呢?還記得上面 newOutputStream 的時候,回傳了一個 outputStream,這個 outputStream 是 FileOutputStream ,但是經過了 FaultHidingOutputStream 的封裝,這個類實際上就是重寫了 FilterOutputStream 的 write 相關方法,將所有的 IOException 給屏蔽了,如果發生 IOException 就將 hasErrors 賦值為 true,這樣的設計的好處是不直接將 OutputStream 回傳給用戶,如果出錯可以檢測到,不需要用戶手動去呼叫一些操作,
下面看看 completeEdit 方法

private synchronized void completeEdit(Editor editor, boolean success) throws IOException {
    Entry entry = editor.entry;
    if (entry.currentEditor != editor) {
      throw new IllegalStateException();
    }

    // If this edit is creating the entry for the first time, every index must have a value.
    if (success && !entry.readable) {
      for (int i = 0; i < valueCount; i++) {
        if (!editor.written[i]) {
          editor.abort();
          throw new IllegalStateException("Newly created entry didn't create value for index " + i);
        }
        if (!entry.getDirtyFile(i).exists()) {
          editor.abort();
          return;
        }
      }
    }

    for (int i = 0; i < valueCount; i++) {
      File dirty = entry.getDirtyFile(i);
      if (success) {
        if (dirty.exists()) {
          File clean = entry.getCleanFile(i);
          dirty.renameTo(clean);
          long oldLength = entry.lengths[i];
          long newLength = clean.length();
          entry.lengths[i] = newLength;
          size = size - oldLength + newLength;
        }
      } else {
        deleteIfExists(dirty);
      }
    }

    redundantOpCount++;
    entry.currentEditor = null;
    if (entry.readable | success) {
      entry.readable = true;
      journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
      if (success) {
        entry.sequenceNumber = nextSequenceNumber++;
      }
    } else {
      lruEntries.remove(entry.key);
      journalWriter.write(REMOVE + ' ' + entry.key + '\n');
    }
    journalWriter.flush();

    if (size > maxSize || journalRebuildRequired()) {
      executorService.submit(cleanupCallable);
    }
  }

首先判斷 if (success && !entry.readable) 是否成功,且是第一次寫入(如果以前這個記錄有值,則 readable = true ),內部的判斷,我們都不會走,因為 written[i] 在 newOutputStream 的時候被寫入 true 了,而且正常情況下,getDirtyFile 是存在的,

然后如果成功,將 dirtyFile 進行重命名為 cleanFile,檔案名為:key.index,然后重繪 size 的長度,如果失敗,則洗掉 dirtyFile .

然后,如果成功或者 readable 為 true ,將 readable 設定為 true ,寫入一條 CLEAN 記錄,如果第一次提交且失敗,那么就會從 lruEntries.remove(key) ,寫入一條 REMOVE 記錄,

寫入快取,要判斷是否超過了最大 size ,或者需要重建 journal 檔案

  /**
   * We only rebuild the journal when it will halve the size of the journal
   * and eliminate at least 2000 ops.
   */
  private boolean journalRebuildRequired() {
    final int redundantOpCompactThreshold = 2000;
    return redundantOpCount >= redundantOpCompactThreshold //
        && redundantOpCount >= lruEntries.size();
  }

如果 redundantOpCount 達到 2000,且超過了 lruEntries.size() 就會重建,這里就可以看到 redundantOpCount 的作用了,防止 journal 檔案過大,到此我們的存入快取就分析完成了,

總結:首先呼叫 editor,拿到指定的 dirtyFile 的 OutputStream ,然后開始寫操作,寫完后,記得呼叫 commit .
commit 中會檢測你是否發生 IOException ,若無,則將 dirtyFile -> cleanFile ,將 readable = true ,寫入 CLEAN 記錄,如若發生錯誤,則洗掉 dirtyFile ,從 lruEntries 中移除,然后寫入一條 REMOVE 記錄,

4、DiskLruCache 的 get()
  /**
   * Returns a snapshot of the entry named {@code key}, or null if it doesn't
   * exist is not currently readable. If a value is returned, it is moved to
   * the head of the LRU queue.
   */
  public synchronized Snapshot get(String key) throws IOException {
    checkNotClosed();
    validateKey(key);
    Entry entry = lruEntries.get(key);
    if (entry == null) {
      return null;
    }

    if (!entry.readable) {
      return null;
    }

    // Open all streams eagerly to guarantee that we see a single published
    // snapshot. If we opened streams lazily then the streams could come
    // from different edits.
    InputStream[] ins = new InputStream[valueCount];
    try {
      for (int i = 0; i < valueCount; i++) {
        ins[i] = new FileInputStream(entry.getCleanFile(i));
      }
    } catch (FileNotFoundException e) {
      // A file must have been deleted manually!
      for (int i = 0; i < valueCount; i++) {
        if (ins[i] != null) {
          Util.closeQuietly(ins[i]);
        } else {
          break;
        }
      }
      return null;
    }

    redundantOpCount++;
    journalWriter.append(READ + ' ' + key + '\n');
    if (journalRebuildRequired()) {
      executorService.submit(cleanupCallable);
    }

    return new Snapshot(key, entry.sequenceNumber, ins, entry.lengths);
  }

如果根據 key 取到的 Entry 為 null ,或者 readable = false ,則回傳 null,否則將 cleanFile 的 FileInputStream 進行封裝回傳 Snapshot,且寫入一條 READ 陳述句,
然后 getInputStream 就是回傳該 FileInputStream 了,

5、DiskLruCache 的 remove()
  /**
   * Drops the entry for {@code key} if it exists and can be removed. Entries
   * actively being edited cannot be removed.
   *
   * @return true if an entry was removed.
   */
  public synchronized boolean remove(String key) throws IOException {
    checkNotClosed();
    validateKey(key);
    Entry entry = lruEntries.get(key);
    if (entry == null || entry.currentEditor != null) {
      return false;
    }

    for (int i = 0; i < valueCount; i++) {
      File file = entry.getCleanFile(i);
      if (file.exists() && !file.delete()) {
        throw new IOException("failed to delete " + file);
      }
      size -= entry.lengths[i];
      entry.lengths[i] = 0;
    }

    redundantOpCount++;
    journalWriter.append(REMOVE + ' ' + key + '\n');
    lruEntries.remove(key);

    if (journalRebuildRequired()) {
      executorService.submit(cleanupCallable);
    }

    return true;
  }

如果物體存在且不是正在被編輯,就可以直接進行洗掉,然后寫入一條 REMOVE 記錄,

remove() 與 open() 對應,在使用完成 cache 后可以手動關閉,

6、DiskLruCache 的 close()
	/**
     * Closes this cache. Stored values will remain on the filesystem.
     */
    public synchronized void close() throws IOException {
        if (journalWriter == null) {
            return; // already closed
        }
        for (Entry entry : new ArrayList<Entry>(lruEntries.values())) {
            if (entry.currentEditor != null) {
                entry.currentEditor.abort();
            }
        }
        trimToSize();
        journalWriter.close();
        journalWriter = null;
    }

這個方法用于將 DiskLruCache 關閉掉,關閉前,會判斷所有正在編輯的物體,呼叫 abort() 方法,最后關閉 journalWriter ,close() 是和 open() 方法對應的一個方法,關閉之后就不能再呼叫 DiskLruCache 中任何操作快取資料的方法,通常只應該在 Activity 的 onDestroy() 方法中去呼叫 close() 方法

其中 abort() 方法是存盤失敗時的邏輯,中止此編輯,這將釋放編輯鎖,以便其他操作可以獲取編輯鎖,操作同一個 key,

    /**
     * Aborts this edit. This releases the edit lock so another edit may be
     * started on the same key.
     */
    public void abort() throws IOException {
      completeEdit(this, false);
    }
7、DiskLruCache 的 delete()
	/**
     * Closes the cache and deletes all of its stored values. This will delete
     * all files in the cache directory including files that weren't created by the cache.
     */
    public void delete() throws IOException {
        close();
        IoUtils.deleteContents(directory);
    }

這個方法用于將所有的快取資料全部洗掉,比如 APP 中手動清理快取功能,只需要呼叫一下 DiskLruCache 的 delete() 方法就可以實作了,

8、DiskLruCache 的 size()
 	/**
     * Returns the number of bytes currently being used to store the values in
     * this cache. This may be greater than the max size if a background
     * deletion is pending.
     */
    public synchronized long size() {
        return size;
    }

這個方法會回傳當前快取路徑下所有快取資料的總位元組數,以 byte 為單位,如果應用程式中需要在界面上顯示當前快取資料的總大小,就可以通過呼叫這個方法計算出來,如果后臺洗掉掛起,則此值可能大于最大大小,

9、DiskLruCache 的 flush()
	/**
     * Force buffered operations to the filesystem.
     */
    public synchronized void flush() throws IOException {
        checkNotClosed();
        trimToSize();
        journalWriter.flush();
    }

這個方法用于將記憶體中的操作記錄同步到日志檔案(也就是 journal 檔案)當中,這個方法非常重要,因為 DiskLruCache 能夠正常作業是依賴于 journal 檔案中的內容,其實并不是每次寫入快取都要呼叫一次 flush() 方法,頻繁地呼叫并不會帶來任何好處,只會額外增加同步 journal 檔案的時間,比較標準的做法是在 Activity 的 onPause() 方法中呼叫一次 flush() 方法就可以了,

至此,我們的原始碼分析就結束了,可以看到 DiskLruCache ,利用一個 journal 檔案,保證了 cache 物體的可用性(只有 CLEAN 的可用),且獲取檔案的長度的時候可以通過在該檔案的記錄中讀取,利用 FaultHidingOutputStream 對 FileOutPutStream 在寫入檔案程序中是否發生錯誤進行捕獲,而不是讓用戶手動去呼叫出錯后的處理方法,其內部的很多細節都很值得我們推敲和學習,不過也可以看到,存取的操作不是特別的方便易用,需要我們自己去操作檔案流,

三、DiskLruCache 完整原始碼

DiskLruCache 的完整原始碼
Github - JakeWharton/DiskLruCache
本站免費下載鏈接

四、參考資料

任玉剛的《Android 開發藝術探索》一書,
郭霖 CSDN
鴻洋 CSDN

轉載請註明出處,本文鏈接:https://www.uj5u.com/shujuku/213647.html

標籤:其他

上一篇:IDEA 常用的一些 (就幾個) 快捷鍵

下一篇:Android WebView

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • GPU虛擬機創建時間深度優化

    **?桔妹導讀:**GPU虛擬機實體創建速度慢是公有云面臨的普遍問題,由于通常情況下創建虛擬機屬于低頻操作而未引起業界的重視,實際生產中還是存在對GPU實體創建時間有苛刻要求的業務場景。本文將介紹滴滴云在解決該問題時的思路、方法、并展示最終的優化成果。 從公有云服務商那里購買過虛擬主機的資深用戶,一 ......

    uj5u.com 2020-09-10 06:09:13 more
  • 可編程網卡芯片在滴滴云網路的應用實踐

    **?桔妹導讀:**隨著云規模不斷擴大以及業務層面對延遲、帶寬的要求越來越高,采用DPDK 加速網路報文處理的方式在橫向縱向擴展都出現了局限性。可編程芯片成為業界熱點。本文主要講述了可編程網卡芯片在滴滴云網路中的應用實踐,遇到的問題、帶來的收益以及開源社區貢獻。 #1. 資料中心面臨的問題 隨著滴滴 ......

    uj5u.com 2020-09-10 06:10:21 more
  • 滴滴資料通道服務演進之路

    **?桔妹導讀:**滴滴資料通道引擎承載著全公司的資料同步,為下游實時和離線場景提供了必不可少的源資料。隨著任務量的不斷增加,資料通道的整體架構也隨之發生改變。本文介紹了滴滴資料通道的發展歷程,遇到的問題以及今后的規劃。 #1. 背景 資料,對于任何一家互聯網公司來說都是非常重要的資產,公司的大資料 ......

    uj5u.com 2020-09-10 06:11:05 more
  • 滴滴AI Labs斬獲國際機器翻譯大賽中譯英方向世界第三

    **桔妹導讀:**深耕人工智能領域,致力于探索AI讓出行更美好的滴滴AI Labs再次斬獲國際大獎,這次獲獎的專案是什么呢?一起來看看詳細報道吧! 近日,由國際計算語言學協會ACL(The Association for Computational Linguistics)舉辦的世界最具影響力的機器 ......

    uj5u.com 2020-09-10 06:11:29 more
  • MPP (Massively Parallel Processing)大規模并行處理

    1、什么是mpp? MPP (Massively Parallel Processing),即大規模并行處理,在資料庫非共享集群中,每個節點都有獨立的磁盤存盤系統和記憶體系統,業務資料根據資料庫模型和應用特點劃分到各個節點上,每臺資料節點通過專用網路或者商業通用網路互相連接,彼此協同計算,作為整體提供 ......

    uj5u.com 2020-09-10 06:11:41 more
  • 滴滴資料倉庫指標體系建設實踐

    **桔妹導讀:**指標體系是什么?如何使用OSM模型和AARRR模型搭建指標體系?如何統一流程、規范化、工具化管理指標體系?本文會對建設的方法論結合滴滴資料指標體系建設實踐進行解答分析。 #1. 什么是指標體系 ##1.1 指標體系定義 指標體系是將零散單點的具有相互聯系的指標,系統化的組織起來,通 ......

    uj5u.com 2020-09-10 06:12:52 more
  • 單表千萬行資料庫 LIKE 搜索優化手記

    我們經常在資料庫中使用 LIKE 運算子來完成對資料的模糊搜索,LIKE 運算子用于在 WHERE 子句中搜索列中的指定模式。 如果需要查找客戶表中所有姓氏是“張”的資料,可以使用下面的 SQL 陳述句: SELECT * FROM Customer WHERE Name LIKE '張%' 如果需要 ......

    uj5u.com 2020-09-10 06:13:25 more
  • 滴滴Ceph分布式存盤系統優化之鎖優化

    **桔妹導讀:**Ceph是國際知名的開源分布式存盤系統,在工業界和學術界都有著重要的影響。Ceph的架構和演算法設計發表在國際系統領域頂級會議OSDI、SOSP、SC等上。Ceph社區得到Red Hat、SUSE、Intel等大公司的大力支持。Ceph是國際云計算領域應用最廣泛的開源分布式存盤系統, ......

    uj5u.com 2020-09-10 06:14:51 more
  • es~通過ElasticsearchTemplate進行聚合~嵌套聚合

    之前寫過《es~通過ElasticsearchTemplate進行聚合操作》的文章,這一次主要寫一個嵌套的聚合,例如先對sex集合,再對desc聚合,最后再對age求和,共三層嵌套。 Aggregations的部分特性類似于SQL語言中的group by,avg,sum等函式,Aggregation ......

    uj5u.com 2020-09-10 06:14:59 more
  • 爬蟲日志監控 -- Elastc Stack(ELK)部署

    傻瓜式部署,只需替換IP與用戶 導讀: 現ELK四大組件分別為:Elasticsearch(核心)、logstash(處理)、filebeat(采集)、kibana(可視化) 下載均在https://www.elastic.co/cn/downloads/下tar包,各組件版本最好一致,配合fdm會 ......

    uj5u.com 2020-09-10 06:15:05 more
最新发布
  • day02-2-商鋪查詢快取

    功能02-商鋪查詢快取 3.商鋪詳情快取查詢 3.1什么是快取? 快取就是資料交換的緩沖區(稱作Cache),是存盤資料的臨時地方,一般讀寫性能較高。 快取的作用: 降低后端負載 提高讀寫效率,降低回應時間 快取的成本: 資料一致性成本 代碼維護成本 運維成本 3.2需求說明 如下,當我們點擊商店詳 ......

    uj5u.com 2023-04-20 08:33:24 more
  • MySQL中binlog備份腳本分享

    關于MySQL的二進制日志(binlog),我們都知道二進制日志(binlog)非常重要,尤其當你需要point to point災難恢復的時侯,所以我們要對其進行備份。關于二進制日志(binlog)的備份,可以基于flush logs方式先切換binlog,然后拷貝&壓縮到到遠程服務器或本地服務器 ......

    uj5u.com 2023-04-20 08:28:06 more
  • day02-短信登錄

    功能實作02 2.功能01-短信登錄 2.1基于Session實作登錄 2.1.1思路分析 2.1.2代碼實作 2.1.2.1發送短信驗證碼 發送短信驗證碼: 發送驗證碼的介面為:http://127.0.0.1:8080/api/user/code?phone=xxxxx<手機號> 請求方式:PO ......

    uj5u.com 2023-04-20 08:27:27 more
  • 快取與資料庫雙寫一致性幾種策略分析

    本文將對幾種快取與資料庫保證資料一致性的使用方式進行分析。為保證高并發性能,以下分析場景不考慮執行的原子性及加鎖等強一致性要求的場景,僅追求最終一致性。 ......

    uj5u.com 2023-04-20 08:26:48 more
  • sql陳述句優化

    問題查找及措施 問題查找 需要找到具體的代碼,對其進行一對一優化,而非一直把關注點放在服務器和sql平臺 降低簡化每個事務中處理的問題,盡量不要讓一個事務拖太長的時間 例如檔案上傳時,應將檔案上傳這一步放在事務外面 微軟建議 4.啟動sql定時執行計劃 怎么啟動sqlserver代理服務-百度經驗 ......

    uj5u.com 2023-04-20 08:26:35 more
  • 云時代,MySQL到ClickHouse資料同步產品對比推薦

    ClickHouse 在執行分析查詢時的速度優勢很好的彌補了MySQL的不足,但是對于很多開發者和DBA來說,如何將MySQL穩定、高效、簡單的同步到 ClickHouse 卻很困難。本文對比了 NineData、MaterializeMySQL(ClickHouse自帶)、Bifrost 三款產品... ......

    uj5u.com 2023-04-20 08:26:29 more
  • sql陳述句優化

    問題查找及措施 問題查找 需要找到具體的代碼,對其進行一對一優化,而非一直把關注點放在服務器和sql平臺 降低簡化每個事務中處理的問題,盡量不要讓一個事務拖太長的時間 例如檔案上傳時,應將檔案上傳這一步放在事務外面 微軟建議 4.啟動sql定時執行計劃 怎么啟動sqlserver代理服務-百度經驗 ......

    uj5u.com 2023-04-20 08:25:13 more
  • Redis 報”OutOfDirectMemoryError“(堆外記憶體溢位)

    Redis 報錯“OutOfDirectMemoryError(堆外記憶體溢位) ”問題如下: 一、報錯資訊: 使用 Redis 的業務介面 ,產生 OutOfDirectMemoryError(堆外記憶體溢位),如圖: 格式化后的報錯資訊: { "timestamp": "2023-04-17 22: ......

    uj5u.com 2023-04-20 08:24:54 more
  • day02-2-商鋪查詢快取

    功能02-商鋪查詢快取 3.商鋪詳情快取查詢 3.1什么是快取? 快取就是資料交換的緩沖區(稱作Cache),是存盤資料的臨時地方,一般讀寫性能較高。 快取的作用: 降低后端負載 提高讀寫效率,降低回應時間 快取的成本: 資料一致性成本 代碼維護成本 運維成本 3.2需求說明 如下,當我們點擊商店詳 ......

    uj5u.com 2023-04-20 08:24:03 more
  • day02-短信登錄

    功能實作02 2.功能01-短信登錄 2.1基于Session實作登錄 2.1.1思路分析 2.1.2代碼實作 2.1.2.1發送短信驗證碼 發送短信驗證碼: 發送驗證碼的介面為:http://127.0.0.1:8080/api/user/code?phone=xxxxx<手機號> 請求方式:PO ......

    uj5u.com 2023-04-20 08:23:11 more