如何使用Guava Cache做緩存
1. 概述
1.1 適用場景
Cache在ConcurrentHashMap的基礎(chǔ)上提供了自動加載數(shù)據(jù)、清除數(shù)據(jù)、get-if-absend-compute的功能,適用場景:
- 愿意花一些內(nèi)存來提高訪問速度
- 緩存的數(shù)據(jù)查詢或計(jì)算代碼高昂,但是需要查詢不止一次
- 緩存的數(shù)據(jù)在內(nèi)存中放得下,否則應(yīng)該考慮Redis、Memcached
1.2 Hello world
LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.removalListener(MY_LISTENER)
.build (
new CacheLoader<Key, Graph>() {
@Override
public Graph load(Key key) throws AnyException {
return createExpensiveGraph(key);
}
}
);2. 數(shù)據(jù)加載使用
2.1 CacheLoader.load(K key)
LoadingCache是包含了數(shù)據(jù)加載方式的Cache,加載方式由CacheLoader指定,CacheLoader可以簡單到只實(shí)現(xiàn)一個(gè)V load(K key)方法,如:
CacheLoader<Key,Graph> cacheLoader = new CacheLoader<Key,Graph> {
public Grapch load(Key key) throws AnyException {
return createExpensiveGraph(key);
}
}LoadingCache和Cache都是通過CacheBuilder創(chuàng)建,唯一的區(qū)別是LoadingCache需要要提供CacheLoader實(shí)例。
LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder().maximumSize(1000).build(cacheLoader); graphs.get(key);
LoadingCache經(jīng)典的使用方式是通過get(K)獲取數(shù)據(jù),有緩存則直接返回,否則調(diào)用CacheLoader.load(K)計(jì)算并寫入緩存。
CacheLoader可以拋出異常,檢查型異常會被封裝為ExecutionException,RuntimeException會被封裝為UncheckedExecutionException。
如果不想在客戶端代碼里處理異常,可以使用LoadingCache.getUnchecked(K)方法,該方法只會拋出UncheckedExecutionException,它是一個(gè)RuntimeException。
2.2 CacheLoader.loadAll(keys) 批量加載
在客戶端調(diào)用LoadingCache.getAll的時(shí)候,會優(yōu)先嘗試CacheLoader.loadAll(Iterable<? extends K> keys)方法,這個(gè)方法默認(rèn)實(shí)現(xiàn)是拋出UnsupportedLoadingOperationException,LocalCache默認(rèn)優(yōu)先嘗試調(diào)用ClassLoader.loadAll,如果異常則挨個(gè)Key調(diào)用CacheLoader.load(K)并組成Map<Key,Value>返回。
LoadingCache<String, Integer> cache = CacheBuilder.newBuilder().maximumSize(100).build(new CacheLoader<String, Integer>() {
@Override
public Integer load(String s) throws Exception {
System.out.println("going to load from data, key:" + s);
return s.matches("\\d+") ? Integer.parseInt(s) : -1;
}
@Override
public Map<String, Integer> loadAll(Iterable<? extends String> keys) throws Exception {
System.out.println("going to loadAll from data, keys:" + keys);
Map<String, Integer> result = new LinkedHashMap<>();
for (String s : keys) {
result.put(s, s.matches("\\d+") ? Integer.parseInt(s) : -1);
}
result.put("99", 99);
result.put("WhatIsTheFuck", 100);
return result;
}
});
System.out.println(cache.get("10"));
List<String> ls = Lists.newArrayList("1", "2", "a");
System.out.println(cache.getAll(ls));
System.out.println(cache.get("WhatIsTheFuck"));getAll調(diào)用CacheLoader.loadAll,該方法返回一個(gè)Map,可以包含非指定Key數(shù)據(jù),整個(gè)Map會被緩存,但getAll只返回指定的Key的數(shù)據(jù)。
2.3 Callable.call
所有Guava Cache的實(shí)現(xiàn)類都支持get(K, Callable<V>)方法, 返回K對應(yīng)的緩存,或者使用Callable<V>計(jì)算新值并存入緩存,實(shí)現(xiàn)get-if-absent-compute。
相同的Key如果有多個(gè)調(diào)用同時(shí)進(jìn)入,Guava保證只有一個(gè)線程在加載,且其他線程會阻塞等待加載結(jié)果。
Guava Cache內(nèi)部使用了類型ConcurrentHashMap的概念,為了將鎖分片,減少race-condition發(fā)生的范圍。
Cache<String, Integer> cache = CacheBuilder.newBuilder().maximumSize(10).build();
final String key = "2";
Integer value = cache.get(key, new Callable<Integer>() {
public Integer call() throws Exception {
System.out.println("Callable.call running, key:" + key);
return key.matches("\\d+") ? Integer.parseInt(key) : -1;
}
});
System.out.println(value);
System.out.println(value);2.4 手工寫入
我們可以通過cache.put(key,value)直接寫入緩存,寫入會覆蓋之前的值。 也可以通過cache.asMap()視圖來操作數(shù)據(jù)。 cache.asMap()并不會促發(fā)緩存的自動加載,應(yīng)該盡可能使用cache.put和cache.get。
Cache<String,Integer> cache = CacheBuilder.newBuilder().maximumSize(3).build();
cache.put("1",1);
cache.put("2",2);
cache.put("3",3);
cache.put("4",4);
System.out.println(cache.asMap().get("1")); // 因?yàn)樽疃嗑彺?個(gè),get("1")數(shù)據(jù)被清除,返回null
System.out.println(cache.asMap().get("2"));3. 緩存清除
現(xiàn)實(shí)實(shí)際我們總是不可能有足夠的內(nèi)存來緩存所有數(shù)據(jù)的,你總是需要關(guān)注緩存的清除策略。
3.1 基于maximumSize的清除
用于控制緩存的大小,通過CacheBuilder.maximumSize(long),當(dāng)緩存的數(shù)據(jù)項(xiàng)解決maximum的數(shù)量時(shí),采用類似LRU的算法過期歷史數(shù)據(jù)。
Cache<String, Integer> cache = CacheBuilder.newBuilder().maximumSize(3).build();
cache.put("1", 1);
cache.put("2", 2);
cache.put("3", 3);
cache.put("4", 4);
System.out.println(cache.asMap().get("1")); // 因?yàn)樽疃嗑彺?個(gè),get("1")數(shù)據(jù)被清除,返回null
System.out.println(cache.asMap().get("2"));3.2 基于maximumWeight的清除
和maximun類似,只是統(tǒng)計(jì)的weight而不是緩存的記錄數(shù)。
LoadingCache<String, Integer> cache = CacheBuilder.newBuilder().maximumWeight(10).weigher(new Weigher<String, Integer>() {
public int weigh(String s, Integer integer) {
return integer;
}
}).build(new CacheLoader<String, Integer>() {
@Override
public Integer load(String s) throws Exception {
System.out.println("loading from CacheLoader, key:" + s);
return Integer.parseInt(s);
}
});3.3 基于時(shí)間的清除
數(shù)據(jù)寫入指定時(shí)間后過期(expireAfterWrite),也可以指定數(shù)據(jù)一段時(shí)間沒有訪問后清除(expireAfterAccess)。
final long start = System.nanoTime();
LoadingCache<String, Integer> cache = CacheBuilder.newBuilder().expireAfterWrite(1, TimeUnit.HOURS).build(new CacheLoader<String, Integer>() {
public Integer load(String s) throws Exception {
System.out.println("loading data from CacheLoader, key:" + s);
return Integer.parseInt(s);
}
});測試基于時(shí)間的清除,緩存一個(gè)小時(shí),然后我們真的等一個(gè)小時(shí)后來驗(yàn)證是不現(xiàn)實(shí)的,Guava提供了Ticker類用于提供模擬時(shí)鐘,返回的是時(shí)間納秒數(shù)。
下面這個(gè)實(shí)例通過自定義Ticker,讓1s變成10分鐘(*600),緩存一個(gè)小時(shí)的數(shù)據(jù),實(shí)際過6s后數(shù)據(jù)就會過期。
final long start = System.nanoTime();
LoadingCache<String, Integer> cache = CacheBuilder.newBuilder().expireAfterWrite(1, TimeUnit.HOURS).ticker(new Ticker() {
public long read() {
long current = System.nanoTime();
long diff = current - start;
System.out.println("diff:" + (diff / 1000 / 1000 / 1000));
long time = start + (diff * 600);
return time;
}
}).build(new CacheLoader<String, Integer>() {
@Override
public Integer load(String s) throws Exception {
System.out.println("loading data from CacheLoader, key:" + s);
return Integer.parseInt(s);
}
});3.4 使用WeakReferenct、SoftReference保存Key和Value
Guava允許設(shè)置弱引用(weak reference)和軟銀用(soft reference)來引用實(shí)際的Key、Value數(shù)據(jù)。
通過CacheBuilder.weakKeys、CacheBuilder.weakValues、CacheBuilder.softValues來運(yùn)行JVM的垃圾回收,同時(shí)帶來的問題是Cache的Key只用==來比較而不是equals,要想從Cache里取回之前的緩存,必須保存Key的Reference對象。
3.5 顯示的移除緩存
刪除單個(gè)Key、批量刪除Key、清空緩存
Cache.invalidate(key) Cache.invalidateAll(keys) Cache.invalidateAll()
3.6 緩存清除監(jiān)聽
不是太實(shí)用,并不是Key一過期就會觸發(fā)RemovalListener回調(diào),你需要再次寫入數(shù)據(jù)的時(shí)候才會觸發(fā)同一個(gè)Segment的過期,Cache.get官網(wǎng)文檔說特定條件下也會觸發(fā)清空過期數(shù)據(jù)。
Cache<String, Integer> cache = CacheBuilder.newBuilder().maximumSize(3).expireAfterWrite(10, TimeUnit.SECONDS)
.removalListener(new RemovalListener<String, Integer>() {
public void onRemoval(RemovalNotification<String, Integer> r) {
System.out.println("Key:" + r.getKey());
System.out.println("Value:" + r.getValue());
System.out.println("Cause:" + r.getCause());
}
}).build();4. 緩存的清除時(shí)機(jī)
Cache不會自動的清除緩存,不會在數(shù)據(jù)過期后立即就清除,只有發(fā)生寫入動作(如Cache.put)才會觸發(fā)清除動作(包括LoadingCache.get新加載數(shù)據(jù)也會清除當(dāng)前Segement過期數(shù)據(jù))。
這樣做的目的好處是不用額外維護(hù)一個(gè)線程做緩存管理動作,如果想要定期清除,開發(fā)者可以自行創(chuàng)建一個(gè)線程,定期調(diào)用Cache.cleanUp()方法。
4.1 通過refresh優(yōu)化讀取性能
LoadingCache.refresh(K)和清除緩存(eviction)不同,refresh會導(dǎo)致Cache重新加載Key對應(yīng)的值,加載期間,老的值依然可用; 而清除(eviction)之后,其他現(xiàn)在再來取值會阻塞直至新數(shù)據(jù)加載完成。
CacheLoader.reload(K,V)方法是專門處理refresh提供的方法,refresh調(diào)用后實(shí)際會調(diào)用CacheLoader.reload(K,V)方法,這個(gè)方法的第2個(gè)入?yún)?shí)際是當(dāng)前K的歷史值。
通過CacheBuilder.refreshAfterWrite(long,TimeUnit)設(shè)定,Key在寫入Cache指定時(shí)間區(qū)間后,自動刷新Key的值,而此時(shí)歷史數(shù)據(jù)仍然對外提供服務(wù)。
CacheBuilder.refreshAfterWrite(long,TimeUnit)只會在下次查詢的時(shí)候生效,你可以同時(shí)指定refreshAfterWrite和expireAfterWrite,這樣在指定的時(shí)間段過了之后,如果數(shù)據(jù)還沒有被查詢,數(shù)據(jù)會把清除。
final ScheduledExecutorService es = Executors.newScheduledThreadPool(5);
LoadingCache<String, Integer> cache = CacheBuilder.newBuilder().maximumSize(3).refreshAfterWrite(3, TimeUnit.SECONDS).build(new CacheLoader<String, Integer>() {
@Override
public Integer load(String s) throws Exception {
System.out.println("loading from load...s:" + s);
return Integer.parseInt(s);
}
@Override
public ListenableFuture<Integer> reload(final String key, final Integer oldValue) throws Exception {
if (oldValue > 5) { // 立即返回舊值
System.out.println("loading from reload immediate...key:" + key);
return Futures.immediateFuture(oldValue);
} else {
ListenableFutureTask<Integer> fi = ListenableFutureTask.create(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
System.out.println("loading from reload...key:" + key);
return oldValue;
}
});
es.execute(fi);
return fi;
}
}
});5. 緩存性能指標(biāo)
通過調(diào)用CacheBuilder.recordStats()可以打開統(tǒng)計(jì)功能,打開功能后可以通過Cache.stats()返回統(tǒng)計(jì)信息
LoadingCache<String, Integer> cache = CacheBuilder.newBuilder().maximumSize(3).recordStats().build(new CacheLoader<String, Integer>() {
public Integer load(String s) throws Exception {
return Integer.parseInt(s);
}
});
CacheStats stats = cache.stats();
System.out.println(stats.hitRate()); // 緩存命中率
System.out.println(stats.averageLoadPenalty()); // 平均數(shù)加載時(shí)間,單位納秒
System.out.println(stats.evictionCount()); // 緩存過期數(shù)據(jù)數(shù)6. 原理、長處和限制
LocalLoadingCache通過公式Math.min(concurrencyLevel, maxWeight / 20)計(jì)算Segment數(shù)量,數(shù)據(jù)根據(jù)key的Hash值被分散到不同的Segment中。
默認(rèn)的concurrencyLevel是4,相當(dāng)于默認(rèn)情況下Segment數(shù)量最大就是4。
LocalLoadingCache指定Capacity,默認(rèn)是16,Capacity會轉(zhuǎn)換為大于指定Capacity的最小的2冪次方。
SegmentCapacity等于Capacity/SegmentCount, 轉(zhuǎn)換為大于SegmentCapacity的最小的2冪次方。
SegmentCapacity的值指定了Segment下AtomicReferenceArray的長度,AtomicReferenceArray每一個(gè)下標(biāo)對應(yīng)一個(gè)鏈表。
SegmentCount和SegmentCapacity決定了緩存數(shù)據(jù)被切分的份數(shù),相當(dāng)于決定了查找效率。
Segment內(nèi)部還維護(hù)著writeQueue、accessQueue、recencyQueue每一次讀寫操作都會更新對應(yīng)隊(duì)列,后續(xù)expireAfterWrite、expireAfterAccess只需要順著隊(duì)列找即可,因?yàn)殛?duì)列的順序就是操作的順序, writeQueue、accessQueue是特制的隊(duì)列,只用簡單的鏈表實(shí)現(xiàn),從鏈表移除插入都很高效。
Segement還維護(hù)了keyReferenceQueue、valueReferenceQueue,他們是Java里的ReferenceQueue,當(dāng)采用WeakReference、SoftReference做為Key/Value存儲時(shí),自動加入到keyReferenceQueue和valueReferenceQueue中,Guava處理并刪除對應(yīng)的緩存。

7. 測試代碼
package com.hujiang.track.pageview;
import com.google.common.base.Ticker;
import com.google.common.cache.*;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListenableFutureTask;
import org.junit.Test;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.*;
public class TestCache {
@Test
public void testCache() throws ExecutionException {
LoadingCache<String, Integer> cache = CacheBuilder.newBuilder().maximumSize(100).build(new CacheLoader<String, Integer>() {
@Override
public Integer load(String s) throws Exception {
System.out.println("going to load from data, key:" + s);
return s.matches("\\d+") ? Integer.parseInt(s) : -1;
}
@Override
public Map<String, Integer> loadAll(Iterable<? extends String> keys) throws Exception {
System.out.println("going to loadAll from data, keys:" + keys);
Map<String, Integer> result = new LinkedHashMap<>();
for (String s : keys) {
result.put(s, s.matches("\\d+") ? Integer.parseInt(s) : -1);
}
result.put("99", 99);
result.put("WhatIsTheFuck", 100);
return result;
}
});
System.out.println(cache.get("10"));
System.out.println(cache.get("20"));
System.out.println(cache.get("a0"));
List<String> ls = Lists.newArrayList("1", "2", "a");
System.out.println(cache.getAll(ls));
System.out.println(cache.get("WhatIsTheFuck"));
}
@Test
public void testCallable() throws ExecutionException {
Cache<String, Integer> cache = CacheBuilder.newBuilder().maximumSize(10).build();
final String key = "2";
Integer value = cache.get(key, new Callable<Integer>() {
public Integer call() throws Exception {
System.out.println("Callable.call running, key:" + key);
return key.matches("\\d+") ? Integer.parseInt(key) : -1;
}
});
System.out.println(value);
System.out.println(value);
}
@Test
public void testPut() {
Cache<String, Integer> cache = CacheBuilder.newBuilder().maximumSize(3).build();
cache.put("1", 1);
cache.put("2", 2);
cache.put("3", 3);
cache.put("4", 4);
System.out.println(cache.asMap().get("1")); // 因?yàn)樽疃嗑彺?個(gè),get("1")數(shù)據(jù)被清除,返回null
System.out.println(cache.asMap().get("2"));
}
@Test
public void testWeight() throws ExecutionException {
LoadingCache<String, Integer> cache = CacheBuilder.newBuilder().maximumWeight(10).weigher(new Weigher<String, Integer>() {
public int weigh(String s, Integer integer) {
return integer;
}
}).build(new CacheLoader<String, Integer>() {
@Override
public Integer load(String s) throws Exception {
System.out.println("loading from CacheLoader, key:" + s);
return Integer.parseInt(s);
}
});
cache.get("1");
cache.get("3");
cache.get("5");
cache.get("1");
cache.get("7");
cache.get("1");
cache.get("3");
}
@Test
public void testTimeEviction() throws InterruptedException, ExecutionException {
System.out.println("nano:" + System.nanoTime());
System.out.println("ms :" + System.currentTimeMillis());
final long start = System.nanoTime();
LoadingCache<String, Integer> cache = CacheBuilder.newBuilder().expireAfterWrite(1, TimeUnit.HOURS).ticker(new Ticker() {
public long read() {
long current = System.nanoTime();
long diff = current - start;
System.out.println("diff:" + (diff / 1000 / 1000 / 1000));
long time = start + (diff * 600);
return time;
}
}).build(new CacheLoader<String, Integer>() {
@Override
public Integer load(String s) throws Exception {
System.out.println("loading data from CacheLoader, key:" + s);
return Integer.parseInt(s);
}
});
System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
System.out.println(cache.get("1"));
TimeUnit.SECONDS.sleep(1);
System.out.println("time:" + (new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())) + "-------" + cache.get("1"));
TimeUnit.SECONDS.sleep(1);
System.out.println("time:" + (new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())) + "-------" + cache.get("1"));
TimeUnit.SECONDS.sleep(1);
System.out.println("time:" + (new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())) + "-------" + cache.get("1"));
TimeUnit.SECONDS.sleep(1);
System.out.println("time:" + (new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())) + "-------" + cache.get("1"));
TimeUnit.SECONDS.sleep(1);
System.out.println("time:" + (new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())) + "-------" + cache.get("1"));
TimeUnit.SECONDS.sleep(1);
System.out.println("time:" + (new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())) + "-------" + cache.get("1"));
TimeUnit.SECONDS.sleep(1);
System.out.println("time:" + (new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())) + "-------" + cache.get("1"));
}
@Test
public void testWeakKeys() {
CacheBuilder.newBuilder().weakKeys().weakValues().build();
}
@Test
public void testRemovalListener() throws InterruptedException {
Cache<String, Integer> cache = CacheBuilder.newBuilder().maximumSize(3).expireAfterWrite(10, TimeUnit.SECONDS).removalListener(new RemovalListener<String, Integer>() {
public void onRemoval(RemovalNotification<String, Integer> r) {
System.out.println("Key:" + r.getKey());
System.out.println("Value:" + r.getValue());
System.out.println("Cause:" + r.getCause());
}
}).build();
cache.put("1", 1);
cache.put("2", 2);
cache.put("3", 3);
cache.put("4", 4);
TimeUnit.SECONDS.sleep(11);
System.out.println("get-from-cache-2:" + cache.getIfPresent("2"));
cache.put("2", 3);
TimeUnit.SECONDS.sleep(11);
}
@Test
public void testEvict() throws ExecutionException {
LoadingCache<String, Integer> cache = CacheBuilder.newBuilder().maximumSize(2).removalListener(new RemovalListener<String, Integer>() {
public void onRemoval(RemovalNotification<String, Integer> r) {
System.out.println("Key:" + r.getKey() + ", Value:" + r.getValue() + ", Cause:" + r.getCause());
}
}).recordStats().build(new CacheLoader<String, Integer>() {
@Override
public Integer load(String s) throws Exception {
System.out.println("CacheLoader.load key:" + s);
return Integer.parseInt(s);
}
});
System.out.println(cache.get("2"));
System.out.println(cache.get("5"));
System.out.println(cache.get("6"));
System.out.println(cache.get("1"));
}
@Test
public void testStatistics() {
LoadingCache<String, Integer> cache = CacheBuilder.newBuilder().maximumSize(3).recordStats().build(new CacheLoader<String, Integer>() {
public Integer load(String s) throws Exception {
return Integer.parseInt(s);
}
});
CacheStats stats = cache.stats();
System.out.println(stats.hitRate()); // 緩存命中率
System.out.println(stats.averageLoadPenalty()); // 平均數(shù)加載時(shí)間,單位納秒
System.out.println(stats.evictionCount()); // 緩存過期數(shù)據(jù)數(shù)
}
@Test
public void testRefresh() throws ExecutionException, InterruptedException {
final ScheduledExecutorService es = Executors.newScheduledThreadPool(5);
LoadingCache<String, Integer> cache = CacheBuilder.newBuilder().maximumSize(3).refreshAfterWrite(3, TimeUnit.SECONDS).build(new CacheLoader<String, Integer>() {
@Override
public Integer load(String s) throws Exception {
System.out.println("loading from load...s:" + s);
return Integer.parseInt(s);
}
@Override
public ListenableFuture<Integer> reload(final String key, final Integer oldValue) throws Exception {
if (oldValue > 5) { // 立即返回舊值
System.out.println("loading from reload immediate...key:" + key);
return Futures.immediateFuture(oldValue);
} else {
ListenableFutureTask<Integer> fi = ListenableFutureTask.create(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
System.out.println("loading from reload...key:" + key);
return oldValue;
}
});
es.execute(fi);
return fi;
}
}
});
cache.get("5");
cache.get("6");
TimeUnit.SECONDS.sleep(4);
cache.get("5");
cache.get("6");
}
}到此這篇關(guān)于使用Guava Cache做緩存的文章就介紹到這了,更多相關(guān)Guava Cache緩存內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot2底層注解@Configuration配置類詳解
這篇文章主要為大家介紹了SpringBoot2底層注解@Configuration配置類詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-05-05
Java?數(shù)據(jù)結(jié)構(gòu)與算法系列精講之隊(duì)列
這篇文章主要介紹了Java隊(duì)列數(shù)據(jù)結(jié)構(gòu)的實(shí)現(xiàn),隊(duì)列是一種特殊的線性表,只允許在表的隊(duì)頭進(jìn)行刪除操作,在表的后端進(jìn)行插入操作,隊(duì)列是一個(gè)有序表先進(jìn)先出,想了解更多相關(guān)資料的小伙伴可以參考下面文章的詳細(xì)內(nèi)容2022-02-02
Java如何獲取當(dāng)前進(jìn)程ID以及所有Java進(jìn)程的進(jìn)程ID
本篇文章主要介紹了Java如何獲取當(dāng)前進(jìn)程ID以及所有Java進(jìn)程的進(jìn)程ID,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-06-06
SpringBoot集成Redis向量數(shù)據(jù)庫實(shí)現(xiàn)相似性搜索功能
Redis?是一個(gè)開源(BSD?許可)的內(nèi)存數(shù)據(jù)結(jié)構(gòu)存儲,用作數(shù)據(jù)庫、緩存、消息代理和流式處理引擎,向量檢索的核心原理是通過將文本或數(shù)據(jù)表示為高維向量,并在查詢時(shí)根據(jù)向量的相似度進(jìn)行搜索,本文給大家介紹了SpringBoot集成Redis向量數(shù)據(jù)庫實(shí)現(xiàn)相似性搜索功能2024-09-09

