基於redis的三級分佈式緩存實現實例 1. 分佈式系統中的領域信息模型與緩存機制 2 spring提供的緩存機制 3 分佈式緩存實現實例 4 小結

1. 分佈式系統中的領域信息模型與緩存機制

  在領域驅動設計的方法中,確定領域的信息模型(知識模型)是系統設計的重要工作。在我們識別了領域模型中的實體,值對象和聚合對象之後,需要在面向對象的系統中將其實例化。
  在分佈式系統中,不同的服務(子系統)可能關注於不同的實體對象,但也有一些公共的實體對象可能會被多個服務所關注。在分佈式系統中,一些實體對象會被某些服務頻繁使用,因而緩存機制在系統實現中必不可少。
  在實際應用中,我們通常使用三級緩存機制,即內存(一級)、redis(二級)、數據庫(三級)三級數據緩存機制。在讀取數據時依次從內存、redis、數據庫中讀取數據。在寫入數據時依次從數據庫、redis、內存中寫入。
  根據spring的設計慣例,我們常常將一個實體對象的訪問類用一個DAO對象來表示,在DAO對象中實現分佈式的三級緩存機制。則使用DAO對象對應用系統屏蔽了緩存的實現細節。應用系統則只關注如何使用DAO與領域實體進行交互。
  分佈式系統中常常使用一個共享的緩存中間件實現各服務(子系統)之間的緩存的交互,這個緩存中間件我們常常使用redis集羣。對於分佈式系統而言,在三級緩存體系中,各服務(子系統)擁有自己的內存緩存(一級)和獨立的數據庫(三級),共享一個二級緩存redis集羣。
  在這樣一個分佈式的緩存體系中,緩存數據的同步是必須要考慮的。考慮這樣一個場景:A服務修改了某個實體對象,同時更新到二級緩存,B服務再次使用該實體對象時,應該使用更新後的實體對象。如果使用redis集羣來實現二級緩存,應該使用訂閱發佈機制來實現各服務(子系統)之間的緩存同步。
  同時,在一級緩存中,緩存的過期策略通常也需要設置。設置的目的也是爲了確保數據不同步引發的錯誤是可自愈的,參數常用的是寫過期時間和讀過期時間兩類。
  整個分佈式應用系統中的三級緩存架構如圖1所示。


2 spring提供的緩存機制

  spring框架中提供對緩存的支持,支持如圖2所示的緩存類型。



  spring框架中對緩存的支持主要有兩個接口的實現來支持,一個爲CacheManager接口(org.springframework.cache.CacheManager),一個爲Cache接口(org.springframework.cache.Cache)。
  spring框架中對緩存的應用主要使用:@Cacheable @CachePut @CacheEvict @Caching等註解實現。
  相關的資料可從網上獲取,在此不再贅訴。

3 分佈式緩存實現實例

  在我們的實際應用項目中,我們設計了一種分佈式的三級緩存機制,這裏使用了redis作爲二級緩存,caffeine作爲一級緩存,數據庫作爲三級存儲介質。

3.1 實現類的層次

  在實際應用中,針對redis的各數據類型設計了對不同數據類型的抽象操作子類,對於領域內各實體對象的操作DAO類則繼承自這些redis的抽象操作子類。其類層次結構圖如圖3所示:


3.2 一級緩存的實現與策略配置

  在實現中,我們使用caffeine作爲一級緩存,實現了自定義的CacheManager,可實現動態與指定名稱cache的生成,並可針對指定名字的cache配置不同的緩存策略。其部分實現代碼如下:

/**
     * 自定義cacheManager,實現動態生成的cache使用缺省的配置
     * 
     * @author zhang.kai
     *
     */
    public class VlineCacheManager extends SimpleCacheManager {
        @Override
        protected Cache getMissingCache(String cacheName) {
            return createCaffeineCache(cacheName, cacheProperties.getDefaultspec());
        }
    }

    /**
     * CacheManager對象注入
     * 
     * @return
     */
    @Bean
    public CacheManager getCacheManager() {
        if ((null == cacheProperties) || (null == cacheProperties.getDefaultspec())
                || (null == cacheProperties.getCachespecs())) {
            log.error("cacheProperties is invalid:{}", cacheProperties);
            return null;
        }
        VlineCacheManager cm = new VlineCacheManager();
        List<Cache> caches = new ArrayList<>();
        cacheProperties.getCachespecs().keySet().forEach(cacheName -> {
            String cacheSpec = cacheProperties.getCachespecs().get(cacheName);
            if (StringUtils.isEmpty(cacheName)) {
                log.error("XXX no cacheSpec for cacheName{}", cacheName);
                return;
            }
            CaffeineCache cache = createCaffeineCache(cacheName, cacheSpec);
            caches.add(cache);
        });
        if (caches.size() > 0) {
            cm.setCaches(caches);
        } else {
            log.error("XXX no cache inited!");
            return null;
        }

        // 設置redis的發佈ID
        redisPublisherId = getRedisPublishId();
        log.info("%%%%%get redisPublisherId:{}",redisPublisherId);
        return cm;
    }

  對應的配置文件中,對cache的讀寫策略配置如下:

vline:
  cache:
    defaultspec: initialCapacity=50,maximumSize=500,expireAfterWrite=5s,expireAfterAccess=500s
    cachespecs:
      CONFIG_INFO: initialCapacity=50,maximumSize=500,expireAfterWrite=5s,expireAfterAccess=500s
      USER_LOGIN_DEVICE_INFO: initialCapacity=60,maximumSize=500,expireAfterWrite=5s,expireAfterAccess=7s

3.3 緩存讀取方式

  對於三級緩存依次從內存,redis,數據庫中獲取,其代碼片段如下所示:

/**
     * 從內存,redis,DB中依次獲取實體
     * 
     * @param key
     * @return value
     */
    public T get(String key) {
        T value = null;

        // 1.如果在內存緩存中,則獲取內存緩存
        if (enableMemoryCached) {
            value = getFromCacheManager(key);
            if (null != value) {
                log.debug("***get from memory***{}->{}", getRedisKey(key), value);
                return value;
            }
        }
        // 2.從redis中獲取,對於鍵不存在的hash,redis返回empty map
        value = getFromRedis(key);
        if (null != value) {
            // 從redis中獲取並回寫到內存
            log.debug("***get from redis***{}->{}", getRedisKey(key), value);
            putToCacheManager(key, value);
        } else {
            if (enableDbLoad) {
                // 3.從數據庫中獲取並回寫到redis和內存,可能存在競爭,重試一定次數
                RedisLock redisLock = new RedisLock(srt, getRedisKey(key) + "_sync_lock");
                int retryCount = 0;
                do {
                    try {
                        // 如果獲取到分佈式鎖,則從數據庫中獲取,取完後返回
                        if (redisLock.lock()) {
                            value = loadValueFromDb(key);
                            log.debug("***get from db***{}->{}", getRedisKey(key), value);
                            if (null != value) {
                                set(key, value); // 回寫入redis與內存
                            }
                            threadAwait.signalAll(getRedisKey(key)); // 喚醒所有等待該key的線程
                            return value;
                        }
                        // 如果未獲取到分佈式鎖,說明別的進程正在進行查詢數據庫,等待一段時間後查詢redis
                        threadAwait.await(getRedisKey(key), CacheConsts.LOAD_FROM_DB_WAIT_TIME);
                        value = getFromRedis(key);
                        if (null != value) {
                            if (enableMemoryCached) {
                                putToCacheManager(key, value);
                            }
                            return value;
                        }
                    } catch (Exception e) {
                        log.error("get" + getRedisKey(key) + "fail!", e);
                    } finally {
                        redisLock.unlock();
                    }
                    retryCount++;
                } while (retryCount < this.retryTimes);
            }
        }
        return value;
    }  

  這裏考慮當多個進程(線程)讀取同一key時,使用分佈式鎖來實現。使得只有第一個獲取鎖的進程(線程),會去三級存儲數據庫中獲取並存入redis中。其它讀取該key的進程(線程)將在redis中獲取。這裏代碼中使用的分佈式鎖和線程喚醒對象參考了來自https://github.com/wyh-spring-ecosystem-student/spring-boot-student處的代碼。

3.4 緩存的失效與同步

  在一級緩存caffeine中,我們設置了緩存的失效策略。同時在分佈式系統中,不同的進程可能都會在一級緩存中緩存相同的key,因此在對應的redis key發生變化時(修改、刪除),應該通知倒各進程,這個通過redis的訂閱發佈機制實現。
  一個進程可能是某個key發生改變的發起者,則它應該在訂閱到該key發生變化時不做任何處理,這裏我們通過在發佈信息中增加一個發佈者的唯一標識來實現,發佈者的唯一標識在public CacheManager getCacheManager()函數中實現。在redis中發佈的信息形式爲:{redisPublishId}${keyPrefix}:{key}。
  每個進程的監聽處理函數是這樣的:

@Override
    public void onMessage(Message message, byte[] pattern) {
        if (null != message.getBody()) {
            String key = new String(message.getBody(), Charset.forName("UTF8"));
            log.debug("+++reveive msg:{} which topic is:{}", key,
                    new String(message.getChannel(), Charset.forName("UTF8")));
            String publisherId = findRedisPublisherId(key);
            // 如果是本進程發佈的,不處理
            if (StringUtils.isEmpty(publisherId) || publisherId.equals(CacheConfiguration.redisPublisherId)) {
                return;
            }
            // 不是本進程發佈的,在一級緩存中失效.發佈的key形式爲:{redisPublishId}${keyPrefix}:{key}
            String redisKey = key.substring(key.indexOf(CacheConsts.REDIS_PUBLISHER_ID_SEPARATE) + 1);
            String keyPrefix = findKeyprefixFromRedisKey(redisKey);
            Cache cache = cm.getCache(keyPrefix);
            if (null != cache) {
                // 從一級緩存中刪除
                log.debug("---evict key:{} from cache:{}", redisKey, cache.getName());
                cache.evict(redisKey);
            }
        } else {
            log.error("RedisMsgListener onMessage null!");
        }
    }

4 小結

  本文所描述的示例代碼可從:https://github.com/solarkai/distributedcache
處獲取。
  對於三級緩存方式,網上有很多不同的實現方式。在我們的實現中,針對redis的不同數據類型衍生出不同的針對領域實體的DAO對象,這些對象屏蔽了緩存使用的細節;同時,在我們的實現中,實現了動態生成與指定名稱的緩存生成方式,可針對不同的緩存配置不同的策略;針對一級緩存的同步,我們使用redis的訂閱發佈機制實現,並在發佈內容中增加發布者的標識,避免重複的同步操作。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章