關於億級流量網站架構一書緩存機制的探討
阿新 • • 發佈:2017-09-16
obj dpa array ride 定義 from 有客 build 遠程
在京東的億級流量網站架構一書,175頁介紹緩存有這樣一段話
僅就這段代碼來看,在高並發情況下,實際上並不能阻止大量線程調用loadSync函數
當然這個書裏的代碼是作者的簡寫,這裏探討只是針對書中這段代碼,實際生成代碼應該有考慮這個問題,另外loadSync函數的邏輯看不到,也可能有考慮到到這個問題。
這中情況應該使用雙鎖,另外firstCreateNewEntry也應該是定義為volatile類型。還有如果是分布式緩存,針對遠程客戶端的回源請求應該要設置一個時限,比如30秒內只受理一個回源請求。
下面給出本人項目中使用的緩存加載機制。本人項目根據機構,應用,數據庫類型三個字段進行了分庫。因此緩存最粗粒度也是這個級別的。
高並發治理關鍵點:
- 初始獲取鎖對象時使用雙鎖機制。
- 使用獲取到的鎖對象同步代碼。
- 記錄上次重刷時間的 lastReloadDate來自ConcurrentHashMap對象,對象在jvm的堆中,所以無需volatile類型就能保證線程間的可見性。
- 最終加載數據時沒有使用雙鎖,因為本項目是使用分布式緩存,都是由遠程客戶端發起的回源請求,雙鎖只能保證本地緩存高並發時刻多線程不會同時進入,而不能防止遠程回源。因為遠程調用時陸陸續續到達的,這裏假設30秒緩存能加載完成,一旦加載完成就不會有客戶端要求回源。該機制保障了30秒內的回源請求只會觸發一次緩存加載。(客戶端發現無緩存,發起回源請求,由於網絡延遲,請求還未到達緩存加載服務器,這時即使緩存已經加載完成了,如果不防範,這些在路上的回源請求也會被受理)
/** * 作者: 林森 * 日期: 2017年1月5日 * CopyRight @lins */ package com.yunkang.ykcachemanage.provider.service; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.ConcurrentHashMap;import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.ColumnMapRowMapper; import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.TransactionCallback; import org.springframework.transaction.support.TransactionTemplate; import com.alibaba.dubbo.config.annotation.Service; import com.yunkang.ykcachemanage.keys.CacheKeys.cache_keys; import com.yunkang.ykcachemanage.keys.CacheKeys; import com.yunkang.ykcachemanage.keys.DictCacheLoader; import com.yunkang.yktechcom.cache.DictCacheHelper; import com.yunkang.yktechcom.jdbc.JDBCRouteHelper; /** * * 項目名稱:ykcachemanage-provider * * 類描述: * * 從數據庫獲取數據並緩存到redis中 為防止多個線程並發重刷redis可能導致無限循環加載問題,該服務建議只開一一個,同時刷新方法提供同步保護 * 另外限制同一個字典十秒內只能刷新一次 * 該服務一般用於重置或初始化redis,初始化後redis與mysql的同步由業務系統的在維護字典時,手工調用緩存更新api保證. * * * 創建人:林森 * * 創建時間:2017年1月5日 修改人: 修改時間: 修改備註: * * @version * */ @Service public class DictCacheLoaderProvider implements DictCacheLoader { @Autowired JDBCRouteHelper jdbcRouteHelper; @Autowired DictCacheHelper dictCacheHelper; //針對不同的緩存集合,使用不同的鎖 Map<String, Object> mapLock = new ConcurrentHashMap<>(); //記錄上次重刷緩存的時間,用於防止惡意重刷。 static Map<String, Date> lastReload = new ConcurrentHashMap<>(); @Override public void reloadCache(String idFieldName, cache_keys setKey, Map<String, Object> splitFields, Map<String, Object> filter, Map<String, String> dbParam) { String[] splitFieldValue = splitFields.values().toArray(new String[0]); String lockKey = CacheKeys.getPrefix(splitFieldValue) + setKey; if (mapLock.get(lockKey) == null) { synchronized (this) { if (mapLock.get(lockKey) == null) { mapLock.put(lockKey, new Object()); } } } Object lock = mapLock.get(lockKey); synchronized (lock) { Date lastReloadDate = lastReload.get(lockKey); if (lastReload.get(lockKey) == null || (new Date().getTime() - lastReloadDate.getTime()) > 30 * 1000) {// 30秒內不重刷 dictCacheHelper.removeAll(CacheKeys.getPrefix(splitFieldValue) + setKey); this.reloadCacheFromDB(idFieldName, setKey, splitFields, filter,dbParam); lastReload.put(lockKey, new Date()); } else { // ===rejectreload redis from db " + setKey + Thread.currentThread()); } } } static String sqlGetDoctor = "select * from doctors where status=‘1‘"; static String sqlDefault = "select * from %s where 1=1 "; private void reloadCacheFromDB(String idFieldName, cache_keys setKey, Map<String, Object> splitFields, Map<String, Object> filter, Map<String, String> dbParam) { switch (setKey) { case KHLIS_DOCTORS: doLoadFromDB(setKey, idFieldName, splitFields, sqlGetDoctor, filter, dbParam); break; default: doLoadFromDB(setKey, idFieldName, splitFields, getSqlFromSetKey(setKey, sqlDefault), filter, dbParam); break; } } // 使用setkey推測數據表的名字. private String getSqlFromSetKey(cache_keys setKey, String sql) { String tableName = setKey.toString().substring(setKey.toString().indexOf(‘_‘) + 1).toLowerCase(); return String.format(sql, tableName); } /** * @param setKey 用於確定要加載的字典表 * @param idFieldName 字典表的主鍵名稱,多個冒號隔開 * @param splitFields 字典表切割字段,將字典表切割為多個緩存. * @param filter 只有滿足條件的才會加載到緩存 * @param dbParam 對應數據路由表的用於分庫的三個字段 org_id,app_id,dbs_type * 從數據庫讀取字典表的數據後更新緩存 */ private void doLoadFromDB(cache_keys setKey, String idFieldName, Map<String, Object> splitFields, String sql, Map<String, Object> filter, Map<String, String> dbParam) { String orgId = dbParam.get("org_id"); String appId = dbParam.get("app_id"); String dbsType = dbParam.get("dbs_type"); if (orgId == null || appId == null|| dbsType == null) throw new RuntimeException("org_id,app_id,dbs_type必須有,否則無法加載緩存"); String[] splitFieldValues = splitFields.values().toArray(new String[0]); List<Map<String, Object>> po = new ArrayList<>(); StringBuilder sb = new StringBuilder(); sb.append(sql); for (Entry<String, Object> item : splitFields.entrySet()) { sb.append(String.format(" and %s=:%s ", item.getKey(), item.getKey())); } Map<String, Object> paramMap = new HashMap<String, Object>(); if (filter != null) { for (Entry<String, Object> item : filter.entrySet()) { sb.append(String.format(" and %s=:%s ", item.getKey(), item.getKey())); } paramMap.putAll(filter); } paramMap.putAll(splitFields); MapSqlParameterSource paramSource = new MapSqlParameterSource(paramMap); NamedParameterJdbcTemplate jdbc = jdbcRouteHelper.getJDBCTemplate(orgId, appId,dbsType); TransactionTemplate trans = jdbcRouteHelper.getTransactionTemplate(orgId, appId,dbsType); po = (List<Map<String, Object>>) trans .execute(new TransactionCallback<List<Map<String, Object>>>() { public List<Map<String, Object>> doInTransaction(TransactionStatus status) { return jdbc.query(sb.toString(), paramSource, new ColumnMapRowMapper()); } }); Map<String, Object> mapValues = new HashMap<>(); for (Map<String, Object> item : po) { if (idFieldName.contains(":")) {// 多字段主鍵 String[] ids = idFieldName.split(":"); String keys = ""; for (String _id : ids) { keys += item.get(_id).toString() + ":"; } mapValues.put(keys, item); } else { mapValues.put(item.get(idFieldName).toString(), item); } } dictCacheHelper.setAll(CacheKeys.getPrefix(splitFieldValues) + setKey, mapValues); } }
關於億級流量網站架構一書緩存機制的探討