SpringBoot中熱點(diǎn)KEY緩存優(yōu)化的2種主流策略
所謂熱點(diǎn)KEY,是指在緩存或數(shù)據(jù)庫(kù)中被頻繁訪問(wèn)的少量鍵值,這些鍵往往承載了系統(tǒng)中大部分的訪問(wèn)流量。
根據(jù)二八原則,通常20%的數(shù)據(jù)承擔(dān)了80%的訪問(wèn)量,甚至在某些極端情況下,單個(gè)KEY可能會(huì)吸引系統(tǒng)超過(guò)50%的流量。
當(dāng)這些熱點(diǎn)KEY沒(méi)有得到合理處理時(shí),可能導(dǎo)致:
- 緩存節(jié)點(diǎn)CPU使用率飆升
- 網(wǎng)絡(luò)帶寬爭(zhēng)用
- 緩存服務(wù)響應(yīng)延遲增加
- 緩存穿透導(dǎo)致數(shù)據(jù)庫(kù)壓力驟增
- 在極端情況下,甚至引發(fā)系統(tǒng)雪崩
本文將深入探討SpringBoot中三種主流的熱點(diǎn)KEY緩存優(yōu)化策略,提升系統(tǒng)在面對(duì)熱點(diǎn)KEY時(shí)的性能表現(xiàn)。
1. 分級(jí)緩存策略
1.1 原理解析
分級(jí)緩存策略采用多層次的緩存架構(gòu),通常包括本地緩存(L1)和分布式緩存(L2)。當(dāng)訪問(wèn)熱點(diǎn)KEY時(shí),系統(tǒng)首先查詢本地內(nèi)存緩存,避免網(wǎng)絡(luò)開(kāi)銷(xiāo);僅當(dāng)本地緩存未命中時(shí),才請(qǐng)求分布式緩存。
開(kāi)源實(shí)現(xiàn)有JetCache、J2Cache
這種策略能有效降低熱點(diǎn)KEY對(duì)分布式緩存的訪問(wèn)壓力,同時(shí)大幅提升熱點(diǎn)數(shù)據(jù)的訪問(wèn)速度。
分級(jí)緩存的核心工作流程:
- 請(qǐng)求首先訪問(wèn)本地緩存(如Caffeine)
- 本地緩存命中直接返回?cái)?shù)據(jù)(納秒級(jí))
- 本地緩存未命中,請(qǐng)求分布式緩存(如Redis)
- 分布式緩存命中,返回?cái)?shù)據(jù)并回填本地緩存
- 分布式緩存未命中,查詢數(shù)據(jù)源并同時(shí)更新本地和分布式緩存
1.2 實(shí)現(xiàn)方式
步驟1:添加相關(guān)依賴
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency> <dependency> <groupId>com.github.ben-manes.caffeine</groupId> <artifactId>caffeine</artifactId> </dependency>
步驟2:配置分級(jí)緩存管理器
@Configuration @EnableCaching public class LayeredCacheConfig { @Bean public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) { LayeredCacheManager cacheManager = new LayeredCacheManager( createLocalCacheManager(), createRedisCacheManager(redisConnectionFactory) ); return cacheManager; } private CacheManager createLocalCacheManager() { CaffeineCacheManager caffeineCacheManager = new CaffeineCacheManager(); // 本地緩存配置 - 為熱點(diǎn)KEY特別優(yōu)化 caffeineCacheManager.setCaffeine(Caffeine.newBuilder() .initialCapacity(100) // 初始大小 .maximumSize(1000) // 最大緩存對(duì)象數(shù) .expireAfterWrite(1, TimeUnit.MINUTES) // 寫(xiě)入后1分鐘過(guò)期 .recordStats()); // 開(kāi)啟統(tǒng)計(jì) return caffeineCacheManager; } private CacheManager createRedisCacheManager(RedisConnectionFactory redisConnectionFactory) { RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofMinutes(10)) // Redis緩存10分鐘過(guò)期 .serializeKeysWith(RedisSerializationContext.SerializationPair .fromSerializer(new StringRedisSerializer())) .serializeValuesWith(RedisSerializationContext.SerializationPair .fromSerializer(new GenericJackson2JsonRedisSerializer())); return RedisCacheManager.builder(redisConnectionFactory) .cacheDefaults(config) .build(); } }
步驟3:實(shí)現(xiàn)自定義分級(jí)緩存管理器
public class LayeredCacheManager implements CacheManager { private final CacheManager localCacheManager; // 本地緩存(L1) private final CacheManager remoteCacheManager; // 分布式緩存(L2) private final Map<String, Cache> cacheMap = new ConcurrentHashMap<>(); public LayeredCacheManager(CacheManager localCacheManager, CacheManager remoteCacheManager) { this.localCacheManager = localCacheManager; this.remoteCacheManager = remoteCacheManager; } @Override public Cache getCache(String name) { return cacheMap.computeIfAbsent(name, this::createLayeredCache); } @Override public Collection<String> getCacheNames() { Set<String> names = new LinkedHashSet<>(); names.addAll(localCacheManager.getCacheNames()); names.addAll(remoteCacheManager.getCacheNames()); return names; } private Cache createLayeredCache(String name) { Cache localCache = localCacheManager.getCache(name); Cache remoteCache = remoteCacheManager.getCache(name); return new LayeredCache(name, localCache, remoteCache); } // 分級(jí)緩存實(shí)現(xiàn) static class LayeredCache implements Cache { private final String name; private final Cache localCache; private final Cache remoteCache; public LayeredCache(String name, Cache localCache, Cache remoteCache) { this.name = name; this.localCache = localCache; this.remoteCache = remoteCache; } @Override public String getName() { return name; } @Override public Object getNativeCache() { return this; } @Override public ValueWrapper get(Object key) { // 先查本地緩存 ValueWrapper localValue = localCache.get(key); if (localValue != null) { return localValue; } // 本地未命中,查遠(yuǎn)程緩存 ValueWrapper remoteValue = remoteCache.get(key); if (remoteValue != null) { // 回填本地緩存 localCache.put(key, remoteValue.get()); return remoteValue; } return null; } @Override public <T> T get(Object key, Class<T> type) { // 先查本地緩存 T localValue = localCache.get(key, type); if (localValue != null) { return localValue; } // 本地未命中,查遠(yuǎn)程緩存 T remoteValue = remoteCache.get(key, type); if (remoteValue != null) { // 回填本地緩存 localCache.put(key, remoteValue); return remoteValue; } return null; } @Override public <T> T get(Object key, Callable<T> valueLoader) { // 先查本地緩存 ValueWrapper localValue = localCache.get(key); if (localValue != null) { return (T) localValue.get(); } // 本地未命中,查遠(yuǎn)程緩存 ValueWrapper remoteValue = remoteCache.get(key); if (remoteValue != null) { // 回填本地緩存 T value = (T) remoteValue.get(); localCache.put(key, value); return value; } // 遠(yuǎn)程也未命中,調(diào)用值加載器 try { T value = valueLoader.call(); if (value != null) { // 同時(shí)更新本地和遠(yuǎn)程緩存 put(key, value); } return value; } catch (Exception e) { throw new ValueRetrievalException(key, valueLoader, e); } } @Override public void put(Object key, Object value) { localCache.put(key, value); remoteCache.put(key, value); } @Override public void evict(Object key) { localCache.evict(key); remoteCache.evict(key); } @Override public void clear() { localCache.clear(); remoteCache.clear(); } } }
步驟4:在服務(wù)中使用分級(jí)緩存
@Service public class ProductService { private final ProductRepository productRepository; public ProductService(ProductRepository productRepository) { this.productRepository = productRepository; } // 使用自定義緩存處理熱點(diǎn)商品數(shù)據(jù) @Cacheable(value = "products", key = "#id", cacheManager = "cacheManager") public Product getProductById(Long id) { // 模擬數(shù)據(jù)庫(kù)訪問(wèn)延遲 try { Thread.sleep(200); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return productRepository.findById(id) .orElseThrow(() -> new ProductNotFoundException("Product not found: " + id)); } // 處理熱門(mén)商品列表 @Cacheable(value = "hotProducts", key = "'top' + #limit", cacheManager = "cacheManager") public List<Product> getHotProducts(int limit) { // 復(fù)雜查詢獲取熱門(mén)商品 return productRepository.findTopSellingProducts(limit); } // 更新商品信息 - 同時(shí)更新緩存 @CachePut(value = "products", key = "#product.id", cacheManager = "cacheManager") public Product updateProduct(Product product) { return productRepository.save(product); } // 刪除商品 - 同時(shí)刪除緩存 @CacheEvict(value = "products", key = "#id", cacheManager = "cacheManager") public void deleteProduct(Long id) { productRepository.deleteById(id); } }
1.3 優(yōu)缺點(diǎn)分析
優(yōu)點(diǎn)
- 顯著降低熱點(diǎn)KEY的訪問(wèn)延遲,本地緩存訪問(wèn)速度可達(dá)納秒級(jí)
- 大幅減輕分布式緩存的負(fù)載壓力,提高系統(tǒng)整體吞吐量
- 減少網(wǎng)絡(luò)IO開(kāi)銷(xiāo),節(jié)約帶寬資源
- 即使分布式緩存短暫不可用,本地緩存仍可提供服務(wù),增強(qiáng)系統(tǒng)彈性
缺點(diǎn)
- 增加了系統(tǒng)復(fù)雜度,需管理兩層緩存
- 存在數(shù)據(jù)一致性挑戰(zhàn),不同節(jié)點(diǎn)的本地緩存可能不同步
- 本地緩存占用應(yīng)用服務(wù)器內(nèi)存資源
- 適合讀多寫(xiě)少的場(chǎng)景,寫(xiě)入頻繁場(chǎng)景效果有限
適用場(chǎng)景
- 高頻訪問(wèn)且相對(duì)穩(wěn)定的熱點(diǎn)數(shù)據(jù)(如商品詳情、用戶配置)
- 讀多寫(xiě)少的業(yè)務(wù)場(chǎng)景
- 對(duì)訪問(wèn)延遲敏感的關(guān)鍵業(yè)務(wù)
- 分布式緩存面臨高負(fù)載的系統(tǒng)
2. 緩存分片策略
2.1 原理解析
緩存分片策略針對(duì)單個(gè)熱點(diǎn)KEY可能導(dǎo)致的單點(diǎn)壓力問(wèn)題,通過(guò)將一個(gè)熱點(diǎn)KEY拆分為多個(gè)物理子KEY,將訪問(wèn)負(fù)載均勻分散到多個(gè)緩存節(jié)點(diǎn)或?qū)嵗稀_@種策略在不改變業(yè)務(wù)邏輯的前提下,有效提升了系統(tǒng)處理熱點(diǎn)KEY的能力。
其核心原理是:
- 將一個(gè)邏輯上的熱點(diǎn)KEY映射為多個(gè)物理子KEY
- 訪問(wèn)時(shí),隨機(jī)或按某種規(guī)則選擇一個(gè)子KEY進(jìn)行操作
- 寫(xiě)入時(shí),同步更新所有子KEY,保證數(shù)據(jù)一致性
- 通過(guò)分散訪問(wèn)壓力,避免單個(gè)緩存節(jié)點(diǎn)的性能瓶頸
2.2 實(shí)現(xiàn)方式
步驟1:創(chuàng)建緩存分片管理器
@Component public class ShardedCacheManager { private final RedisTemplate<String, Object> redisTemplate; private final Random random = new Random(); // 熱點(diǎn)KEY分片數(shù)量 private static final int DEFAULT_SHARDS = 10; // 分片KEY的有效期略有差異,避免同時(shí)過(guò)期 private static final int BASE_TTL_MINUTES = 30; private static final int TTL_VARIATION_MINUTES = 10; public ShardedCacheManager(RedisTemplate<String, Object> redisTemplate) { this.redisTemplate = redisTemplate; } /** * 獲取分片緩存的值 */ public <T> T getValue(String key, Class<T> type) { // 隨機(jī)選擇一個(gè)分片 String shardKey = generateShardKey(key, random.nextInt(DEFAULT_SHARDS)); return (T) redisTemplate.opsForValue().get(shardKey); } /** * 設(shè)置分片緩存的值 */ public void setValue(String key, Object value) { // 寫(xiě)入所有分片 for (int i = 0; i < DEFAULT_SHARDS; i++) { String shardKey = generateShardKey(key, i); // 計(jì)算略有差異的TTL,避免同時(shí)過(guò)期 int ttlMinutes = BASE_TTL_MINUTES + random.nextInt(TTL_VARIATION_MINUTES); redisTemplate.opsForValue().set( shardKey, value, ttlMinutes, TimeUnit.MINUTES ); } } /** * 刪除分片緩存 */ public void deleteValue(String key) { // 刪除所有分片 List<String> keys = new ArrayList<>(DEFAULT_SHARDS); for (int i = 0; i < DEFAULT_SHARDS; i++) { keys.add(generateShardKey(key, i)); } redisTemplate.delete(keys); } /** * 生成分片KEY */ private String generateShardKey(String key, int shardIndex) { return String.format("%s:%d", key, shardIndex); } }
步驟2:創(chuàng)建熱點(diǎn)KEY識(shí)別和處理組件
@Component public class HotKeyDetector { private final RedisTemplate<String, Object> redisTemplate; private final ShardedCacheManager shardedCacheManager; // 熱點(diǎn)KEY計(jì)數(shù)器的Hash名稱 private static final String HOT_KEY_COUNTER = "hotkey:counter"; // 熱點(diǎn)判定閾值 - 每分鐘訪問(wèn)次數(shù) private static final int HOT_KEY_THRESHOLD = 1000; // 熱點(diǎn)KEY記錄 private final Set<String> detectedHotKeys = ConcurrentHashMap.newKeySet(); public HotKeyDetector(RedisTemplate<String, Object> redisTemplate, ShardedCacheManager shardedCacheManager) { this.redisTemplate = redisTemplate; this.shardedCacheManager = shardedCacheManager; // 啟動(dòng)定時(shí)任務(wù),定期識(shí)別熱點(diǎn)KEY scheduleHotKeyDetection(); } /** * 記錄KEY的訪問(wèn)次數(shù) */ public void recordKeyAccess(String key) { redisTemplate.opsForHash().increment(HOT_KEY_COUNTER, key, 1); } /** * 檢查KEY是否是熱點(diǎn)KEY */ public boolean isHotKey(String key) { return detectedHotKeys.contains(key); } /** * 使用合適的緩存策略獲取值 */ public <T> T getValue(String key, Class<T> type, Supplier<T> dataLoader) { if (isHotKey(key)) { // 使用分片策略處理熱點(diǎn)KEY T value = shardedCacheManager.getValue(key, type); if (value != null) { return value; } // 分片中沒(méi)有找到,從數(shù)據(jù)源加載并更新分片 value = dataLoader.get(); if (value != null) { shardedCacheManager.setValue(key, value); } return value; } else { // 對(duì)于非熱點(diǎn)KEY,使用常規(guī)方式處理 T value = (T) redisTemplate.opsForValue().get(key); if (value != null) { return value; } // 緩存未命中,記錄訪問(wèn)并從數(shù)據(jù)源加載 recordKeyAccess(key); value = dataLoader.get(); if (value != null) { redisTemplate.opsForValue().set(key, value, 30, TimeUnit.MINUTES); } return value; } } /** * 定期識(shí)別熱點(diǎn)KEY的任務(wù) */ private void scheduleHotKeyDetection() { ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); executor.scheduleAtFixedRate(() -> { try { // 獲取所有KEY的訪問(wèn)計(jì)數(shù) Map<Object, Object> counts = redisTemplate.opsForHash().entries(HOT_KEY_COUNTER); // 清空之前識(shí)別的熱點(diǎn)KEY Set<String> newHotKeys = new HashSet<>(); // 識(shí)別新的熱點(diǎn)KEY for (Map.Entry<Object, Object> entry : counts.entrySet()) { String key = (String) entry.getKey(); int count = ((Number) entry.getValue()).intValue(); if (count > HOT_KEY_THRESHOLD) { newHotKeys.add(key); // 對(duì)新發(fā)現(xiàn)的熱點(diǎn)KEY,預(yù)熱分片緩存 if (!detectedHotKeys.contains(key)) { preloadHotKeyToShards(key); } } } // 更新熱點(diǎn)KEY集合 detectedHotKeys.clear(); detectedHotKeys.addAll(newHotKeys); // 清除計(jì)數(shù)器,開(kāi)始新一輪計(jì)數(shù) redisTemplate.delete(HOT_KEY_COUNTER); } catch (Exception e) { // 異常處理 e.printStackTrace(); } }, 1, 1, TimeUnit.MINUTES); } /** * 預(yù)熱熱點(diǎn)KEY到分片緩存 */ private void preloadHotKeyToShards(String key) { // 獲取原始緩存中的值 Object value = redisTemplate.opsForValue().get(key); if (value != null) { // 將值復(fù)制到所有分片 shardedCacheManager.setValue(key, value); } } }
步驟3:在服務(wù)中集成熱點(diǎn)KEY處理
@Service public class EnhancedProductService { private final ProductRepository productRepository; private final HotKeyDetector hotKeyDetector; public EnhancedProductService(ProductRepository productRepository, HotKeyDetector hotKeyDetector) { this.productRepository = productRepository; this.hotKeyDetector = hotKeyDetector; } /** * 獲取商品信息,自動(dòng)處理熱點(diǎn)KEY */ public Product getProductById(Long id) { String cacheKey = "product:" + id; return hotKeyDetector.getValue(cacheKey, Product.class, () -> { // 從數(shù)據(jù)庫(kù)加載產(chǎn)品信息 return productRepository.findById(id) .orElseThrow(() -> new ProductNotFoundException("Product not found: " + id)); }); } /** * 獲取熱門(mén)商品列表,自動(dòng)處理熱點(diǎn)KEY */ public List<Product> getHotProducts(int limit) { String cacheKey = "products:hot:" + limit; return hotKeyDetector.getValue(cacheKey, List.class, () -> { // 從數(shù)據(jù)庫(kù)加載熱門(mén)商品 return productRepository.findTopSellingProducts(limit); }); } /** * 更新商品信息,同時(shí)處理緩存 */ public Product updateProduct(Product product) { Product savedProduct = productRepository.save(product); // 清除所有相關(guān)緩存 String cacheKey = "product:" + product.getId(); if (hotKeyDetector.isHotKey(cacheKey)) { // 如果是熱點(diǎn)KEY,清除分片緩存 hotKeyDetector.getShardedCacheManager().deleteValue(cacheKey); } else { // 常規(guī)緩存清除 redisTemplate.delete(cacheKey); } return savedProduct; } }
2.3 優(yōu)缺點(diǎn)分析
優(yōu)點(diǎn)
- 有效分散單個(gè)熱點(diǎn)KEY的訪問(wèn)壓力
- 不依賴于特定的緩存架構(gòu),可適用于多種緩存系統(tǒng)
- 對(duì)客戶端透明,無(wú)需修改調(diào)用方代碼
- 可動(dòng)態(tài)識(shí)別和調(diào)整熱點(diǎn)KEY的處理策略
- 通過(guò)錯(cuò)峰過(guò)期時(shí)間,避免緩存雪崩問(wèn)題
缺點(diǎn)
- 增加寫(xiě)入開(kāi)銷(xiāo),需同步更新多個(gè)緩存分片
- 實(shí)現(xiàn)復(fù)雜度較高,需維護(hù)熱點(diǎn)KEY檢測(cè)和分片邏輯
- 額外的內(nèi)存占用(一個(gè)值存儲(chǔ)多份)
- 可能引入短暫的數(shù)據(jù)不一致窗口
適用場(chǎng)景
- 特定KEY訪問(wèn)頻率遠(yuǎn)高于其他KEY的場(chǎng)景
- 讀多寫(xiě)少的數(shù)據(jù)(商品詳情、活動(dòng)信息等)
- 大型促銷(xiāo)活動(dòng)、爆款商品等可預(yù)見(jiàn)的流量突增場(chǎng)景
- Redis集群面臨單個(gè)KEY訪問(wèn)熱點(diǎn)問(wèn)題的系統(tǒng)
兩種策略對(duì)比
特性 | 分級(jí)緩存策略 | 緩存分片策略 |
---|---|---|
主要解決問(wèn)題 | 熱點(diǎn)KEY訪問(wèn)延遲 | 熱點(diǎn)KEY單點(diǎn)壓力 |
實(shí)現(xiàn)復(fù)雜度 | 中等 | 高 |
額外存儲(chǔ)開(kāi)銷(xiāo) | 中等 | 高 |
寫(xiě)入性能影響 | 中等 | 大 |
一致性保障 | 最終一致 | 最終一致 |
對(duì)原有代碼改動(dòng) | 中等 | 大 |
適用熱點(diǎn)類(lèi)型 | 通用熱點(diǎn) | 超級(jí)熱點(diǎn) |
總結(jié)
在實(shí)際應(yīng)用中,我們可以根據(jù)業(yè)務(wù)特點(diǎn)和系統(tǒng)架構(gòu)選擇合適的策略,甚至將多種策略組合使用,構(gòu)建更加健壯的緩存體系。
無(wú)論選擇哪種策略,都應(yīng)當(dāng)結(jié)合監(jiān)控、預(yù)熱、降級(jí)等最佳實(shí)踐,才能真正發(fā)揮緩存的價(jià)值,保障系統(tǒng)在面對(duì)熱點(diǎn)KEY時(shí)的性能和穩(wěn)定性。
最后,緩存優(yōu)化是一個(gè)持續(xù)改進(jìn)的過(guò)程,隨著業(yè)務(wù)發(fā)展和流量變化,需要不斷調(diào)整和優(yōu)化緩存策略,才能確保系統(tǒng)始終保持高性能和高可用性。
以上就是SpringBoot中熱點(diǎn)KEY緩存優(yōu)化的2種主流策略的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot熱點(diǎn)KEY緩存優(yōu)化的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
SpringBoot的ConfigurationProperties或Value注解無(wú)效問(wèn)題及解決
在SpringBoot項(xiàng)目開(kāi)發(fā)中,全局靜態(tài)配置類(lèi)讀取application.yml或application.properties文件時(shí),可能會(huì)遇到配置值始終為null的問(wèn)題,這通常是因?yàn)樵趧?chuàng)建靜態(tài)屬性后,IDE自動(dòng)生成的Get/Set方法包含了static關(guān)鍵字2024-11-11Java并發(fā)編程之ReentrantLock實(shí)現(xiàn)原理及源碼剖析
ReentrantLock 是常用的鎖,相對(duì)于Synchronized ,lock鎖更人性化,閱讀性更強(qiáng),文中將會(huì)詳細(xì)的說(shuō)明,請(qǐng)君往下閱讀2021-09-09Java中遍歷ConcurrentHashMap的四種方式詳解
這篇文章主要介紹了Java中遍歷ConcurrentHashMap的四種方式詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-10-10java商城項(xiàng)目實(shí)戰(zhàn)之購(gòu)物車(chē)功能實(shí)現(xiàn)
這篇文章主要為大家詳細(xì)介紹了java商城項(xiàng)目實(shí)戰(zhàn)之購(gòu)物車(chē)功能實(shí)現(xiàn),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-04-04java在linux系統(tǒng)下開(kāi)機(jī)啟動(dòng)無(wú)法使用sudo命令的原因及解決辦法
每次開(kāi)機(jī)自動(dòng)啟動(dòng)的java進(jìn)程,頁(yè)面上的關(guān)機(jī)按鈕都無(wú)法實(shí)現(xiàn)關(guān)機(jī)功能,但是此時(shí)如果以chb賬號(hào)通過(guò)ssh登錄該服務(wù)器,手動(dòng)殺掉tomcat進(jìn)程,然后再重新啟動(dòng)tomcat,頁(yè)面上的關(guān)機(jī)按鈕就有效了2013-08-08Springmvc如何返回xml及json格式數(shù)據(jù)
這篇文章主要介紹了Springmvc如何返回xml及json格式數(shù)據(jù),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-09-09Java多線程中的Exchanger應(yīng)用簡(jiǎn)析
這篇文章主要介紹了Java多線程中的Exchanger應(yīng)用簡(jiǎn)析,Exchanger提供了一個(gè)同步點(diǎn)exchange方法,兩個(gè)線程調(diào)用exchange方法時(shí),無(wú)論調(diào)用時(shí)間先后,兩個(gè)線程會(huì)互相等到線程到達(dá)exchange方法調(diào)用點(diǎn),此時(shí)兩個(gè)線程可以交換數(shù)據(jù),將本線程產(chǎn)出數(shù)據(jù)傳遞給對(duì)方,需要的朋友可以參考下2023-12-12Flutter實(shí)現(xiàn)容器組件、圖片組件 的代碼
容器組件(Container)可以理解為在Android中的RelativeLayout或LinearLayout等,在其中你可以放置你想布局的元素控件,從而形成最終你想要的頁(yè)面布局。這篇文章主要介紹了Flutter實(shí)現(xiàn)容器組件、圖片組件 的代碼,需要的朋友可以參考下2019-07-07JAVASE系統(tǒng)實(shí)現(xiàn)抽卡功能
這篇文章主要為大家詳細(xì)介紹了JAVASE系統(tǒng)實(shí)現(xiàn)抽卡功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-11-11