文章目錄
- 10. redis配置mysql實戰優化[重要]
- 11. redis之緩存擊穿、緩存穿透、緩存雪崩
- 12. redis實現分布式session
10. redis配置mysql實戰優化[重要]
// 最初實現@Override@Transactionalpublic Product createProduct(Product product) {productRepo.saveAndFlush(product);jedis.set(SystemConstants.REDIS_KEY_PREFIX + product.getProductId(), gson.toJson(product))return product;}@Override@Transactionalpublic Product updateProduct(Product product) {productRepo.saveAndFlush(product);jedis.set(SystemConstants.REDIS_KEY_PREFIX + product.getProductId(), gson.toJson(product))return product;}@Overridepublic Product getProduct(Long productId) {// 1. 先查redisString productRedis = jedis.get(SystemConstants.REDIS_KEY_PREFIX + productId);if (!StringUtil.isBlank(productRedis)) {return gson.fromJson(productRedis, Product.class);}// 2. redis沒有,再查mysql數據庫Product productMysql = productRepo.findByProductId(productId);if (productMysql != null) {// 3. 數據庫有,則更新redis數據jedis.set(SystemConstants.REDIS_KEY_PREFIX + productMysql.getProductId(), gson.toJson(productMysql));}// 4. 返回mysql數據庫數據return productMysql;}
小公司并發量不大的情況下,問題不是很大,但是大公司高并發量,會出現大量問題,列舉如下:
存在的問題:1. 緩存容量小問題:幾百G的海量數據不可能一直都放到redis緩存中,大大降低redis(<10G)作為內存數據庫的效率解決方案:設置固定過期時間,比如說一天,雖然一開始redis數據量很大,但是一天之后,會有大量數據失效,達到冷熱數據的分離。
jedis.set(SystemConstants.REDIS_KEY_PREFIX + product.getProductId(), gson.toJson(product));
jedis.expire(SystemConstants.REDIS_KEY_PREFIX + product.getProductId(), SystemConstants.REDIS_KEY_EXPIRED_TIME);2. 緩存擊穿問題:雖然設置了過期時間,仍然會出現緩存擊穿問題, 即單個熱點key失效的瞬間,持續的大并發請求就會擊破緩存,直接請求到數據庫,好像蠻力擊穿一樣(緩存無數據/數據庫有數據)解決方案:設置隨機過期時間
jedis.expire(SystemConstants.REDIS_KEY_PREFIX + productId, genRandomExpiredTime(5));
public Integer genRandomExpiredTime(Integer random) {return SystemConstants.REDIS_KEY_EXPIRED_TIME + new Random().nextInt(random) * 60 * 60;
}3. 緩存穿透問題:用戶訪問的數據既不在緩存當中,也不在數據庫中,按道理說數據庫都沒有這個數據,就不能一直來查數據庫了,防止黑客惡意攻擊。解決方案一:緩存空值(null)或默認值 + 過期時間在數據庫查詢不存在時,將其緩存為空值(null)或默認值,緩存失效時間一般設置為5分鐘之內,當數據庫被寫入或更新該key的新數據時,緩存必須同時被刷新,避免數據不一致。@Overridepublic Product getProduct(Long productId) {String redisId = SystemConstants.REDIS_KEY_PREFIX + productId;// 1. 先查redisString productRedis = jedis.get(redisId);if (!StringUtil.isBlank(productRedis)) {// 判斷緩存是否是默認值,避免緩存穿透if (productRedis.equals(SystemConstants.REDIS_DEFAULT_CACHE)) {jedis.expire(redisId, genRandomExpiredTime(3));return null;}jedis.expire(redisId, genRandomExpiredTime(5));return gson.fromJson(productRedis, Product.class);}// 2. redis沒有,再查mysql數據庫Product productMysql = productRepo.findByProductId(productId);if (productMysql != null) {// 3. 數據庫有,則更新redis數據jedis.set(redisId, gson.toJson(productMysql));jedis.expire(redisId, genRandomExpiredTime(5));} else {// 緩存空或默認值 + 過期時間,避免緩存穿透jedis.set(redisId, SystemConstants.REDIS_DEFAULT_CACHE);jedis.expire(redisId, genRandomExpiredTime(3));}return productMysql;}4. 突發性熱點緩存重建導致數據庫系統壓力倍增:也就是說某一數據本來是冷數據,存儲在數據庫中,突然出現大量訪問,redis還沒緩存該數據,因此需要大量查詢數據庫并重建緩存,也就是以下代碼重復執行,要是只執行一次就好了。if (!StringUtil.isBlank(productRedis)) {// 3. 數據庫有,則更新redis數據jedis.set(redisId, gson.toJson(productMysql));jedis.expire(redisId, genRandomExpiredTime(5));}解決方案一:DCL雙端檢鎖機制
但仍然存在以下問題,一方面synchronized鎖住的是單個JVM,若是該web項目集群部署,則在每個JVM都需要鎖一次,另一方面,假如productId=101是熱點數據會被鎖住,但是其他數據productId=202也需要排隊等待,效率降低。解決方案二:分布式鎖setnx
但仍然存在redis緩存和mysql數據庫數據不一致問題解決方案三:鎖優化-讀寫鎖5. 緩存雪崩:在使用緩存時,通常會對緩存設置過期時間,一方面目的是保持緩存與數據庫數據的一致性,另一方面是減少冷緩存占用過多的內存空間。但當緩存中大量熱點緩存在某一個時刻同時實效,請求全部轉發到數據庫,從而導致數據庫壓力驟增,造成系統崩潰等情況,這就是緩存雪崩。解決方案:1. key均勻失效: 將key的過期時間后面加上一個隨機數(比如隨機1-5分鐘),讓key均勻的失效。2. 雙key策略: 主key設置過期時間,備key不設置過期時間,當主key失效時,直接返回備key值。3. 構建緩存高可用集群
// 解決方案一:DCL雙端檢鎖機制
@Overridepublic Product getProduct(Long productId) {String redisId = SystemConstants.REDIS_KEY_PREFIX + productId;// 1. 先查redisString productRedis = jedis.get(redisId);if (!StringUtil.isBlank(productRedis)) {// 判斷緩存默認值,避免緩存穿透if (productRedis.equals(SystemConstants.REDIS_DEFAULT_CACHE)) {jedis.expire(redisId, genRandomExpiredTime(3));return null;}jedis.expire(redisId, genRandomExpiredTime(5));return gson.fromJson(productRedis, Product.class);}Product productMysql = null;synchronized (this) {// 2. DCL再查redis,因為只要有一次查詢數據庫操作,redis就已經有緩存數據了productRedis = jedis.get(redisId);if (!StringUtil.isBlank(productRedis)) {if (productRedis.equals(SystemConstants.REDIS_DEFAULT_CACHE)) {jedis.expire(redisId, genRandomExpiredTime(3));return null;}jedis.expire(redisId, genRandomExpiredTime(5));return gson.fromJson(productRedis, Product.class);}// 3. redis還是沒有,再查mysql數據庫productMysql = productRepo.findByProductId(productId);if (productMysql != null) {// 4. 數據庫有,則更新redis數據【可能出現突發性熱點緩存重建導致數據庫系統壓力倍增】jedis.set(redisId, gson.toJson(productMysql));jedis.expire(redisId, genRandomExpiredTime(5));} else {// 緩存空或默認值 + 過期時間,避免緩存穿透jedis.set(redisId, SystemConstants.REDIS_DEFAULT_CACHE);jedis.expire(redisId, genRandomExpiredTime(3));}}return productMysql;}
// 解決方案二:分布式鎖setnx<dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.20.0</version></dependency>@Configurationpublic class RedissonConfig {@Beanpublic Redisson redisson() {Config config = new Config();config.useSingleServer().setAddress("redis://localhost:6379").setDatabase(0);return (Redisson) Redisson.create(config);}}// 集群部署:分布式鎖public Product getProduct2(Long productId) {String redisId = SystemConstants.REDIS_KEY_PREFIX + productId;// 1. 先查redis緩存Product product = getProductFromRedis(redisId);if (product != null) {return product;}// 分布式鎖RLock確保鎖住特定的productId,不影響其他productId,解決所有問題RLock lock = redisson.getLock(SystemConstants.LOCK_HOT_CACHE_PREFIX + productId);lock.lock(); // 等價于setnx(SystemConstants.LOCK_HOT_CACHE_PREFIX + productId, value)// 2. DCL再查redis,因為只要有一次查詢數據庫操作,redis就已經有緩存數據了Product productMysql = null;try {product = getProductFromRedis(redisId);if (product != null) {return product;}// 3. redis還是沒有,最后查mysql數據庫productMysql = getProductFromMysql(productId);} finally {lock.unlock();}return productMysql;}private Product getProductFromRedis(String redisId) {Product product = null;String productRedis = jedis.get(redisId);if (!StringUtil.isBlank(productRedis)) {if (productRedis.equals(SystemConstants.REDIS_DEFAULT_CACHE)) {// 緩存中存在,卻是緩存默認值,也就是數據庫沒有數據,設置過期時間,避免緩存穿透jedis.expire(redisId, genRandomExpiredTime(3));return new Product(); // 特殊情況}// 緩存中存在,也是正常值jedis.expire(redisId, genRandomExpiredTime(5));product = gson.fromJson(productRedis, Product.class);}return product;}private Product getProductFromMysql(Long productId) {String redisId = SystemConstants.REDIS_KEY_PREFIX + productId;Product productMysql = productRepo.findByProductId(productId);if (productMysql != null) {// 數據庫有,則同步更新redis緩存數據【但是可能出現突發性熱點緩存重建導致數據庫系統壓力倍增,也就是這段代碼大量執行】jedis.set(redisId, gson.toJson(productMysql));jedis.expire(redisId, genRandomExpiredTime(5));} else {// 數據庫沒有,則設置默認值緩存 + 過期時間,避免緩存穿透jedis.set(redisId, SystemConstants.REDIS_DEFAULT_CACHE);jedis.expire(redisId, genRandomExpiredTime(3));}return productMysql;}
// 解決方案三:鎖優化-讀寫鎖
public Product getProductByReadWriteLock(Long productId) {String redisId = SystemConstants.REDIS_KEY_PREFIX + productId;// 1. 先查redis緩存Product product = getProductFromRedis(redisId);if (product != null) {return product;}// 加寫鎖ReadWriteLock readWriteLock = redisson.getReadWriteLock(SystemConstants.LOCK_HOT_UPDATE_PREFIX + productId);Lock writeLock = readWriteLock.writeLock();writeLock.lock();// 2. DCL再查redis,因為只要有一次查詢數據庫操作,redis就已經有緩存數據了Product productMysql;try {product = getProductFromRedis(redisId);if (product != null) {return product;}// 3. 加讀鎖 讀數據庫ReadWriteLock readWriteLock2 = redisson.getReadWriteLock(SystemConstants.LOCK_HOT_UPDATE_PREFIX + productId);Lock readLock = readWriteLock2.readLock();readLock.lock();productMysql = getProductFromMysql(productId);readLock.unlock();} finally {writeLock.unlock();}return productMysql;}
11. redis之緩存擊穿、緩存穿透、緩存雪崩
- 緩存擊穿-緩存無數據/數據庫有數據
單個熱點key失效的瞬間,持續的大并發請求就會擊破緩存,直接請求到數據庫,好像蠻力擊穿一樣。這種情況就是緩存擊穿(Cache Breakdown)。
1. 使用互斥鎖(Mutex Key)只讓一個線程構建緩存,其他線程等待構建緩存執行完畢,重新從緩存中獲取數據。
2. 熱點數據設置隨機過期時間,后臺異步更新緩存,適用于不嚴格要求緩存一致性的場景。
-
緩存穿透-緩存無數據/數據庫無數據
緩存穿透(cache penetration)是用戶訪問的數據既不在緩存當中,也不在數據庫中。但出于容錯的考慮,如果從數據庫查詢不到數據,則無法寫入緩存。這就導致每次請求都會到底層數據庫進行查詢,緩存也失去了意義。當高并發或有人利用不存在的Key頻繁攻擊時,數據庫的壓力驟增,甚至崩潰,這就是緩存穿透問題。
解決方案:
方案一:緩存空值(null)或默認值 + 過期時間在數據庫查詢不存在時,將其緩存為空值(null)或默認值,緩存失效時間一般設置為5分鐘之內,當數據庫被寫入或更新該key的新數據時,緩存必須同時被刷新,避免數據不一致。方案二:業務邏輯前置校驗在業務請求的入口處進行數據合法性校驗,檢查請求參數是否合理、是否包含非法值、是否惡意請求等,提前有效阻斷非法請求。比如,根據年齡查詢時,請求的年齡為-10歲,這顯然是不合法的請求參數,直接在參數校驗時進行判斷返回。方案三:使用布隆過濾器請求白名單寫入數據時,使用布隆過濾器進行標記(相當于設置白名單),業務請求發現緩存中無對應數據時,可先通過查詢布隆過濾器判斷數據是否在白名單內,如果不在白名單內,則直接返回空或失敗。方案四:用戶黑名單限制當發生異常情況時,實時監控訪問的對象和數據,分析用戶行為,針對故意請求、爬蟲或攻擊者,進行特定用戶的限制;
-
緩存雪崩-緩存無數據/數據庫有數據
在使用緩存時,通常會對緩存設置過期時間,一方面目的是保持緩存與數據庫數據的一致性,另一方面是減少冷緩存占用過多的內存空間。但當緩存中大量熱點緩存在某一個時刻同時實效,請求全部轉發到數據庫,從而導致數據庫壓力驟增,造成系統崩潰等情況,這就是緩存雪崩(Cache Avalanche)。
解決方案:
1. key均勻失效: 將key的過期時間后面加上一個隨機數(比如隨機1-5分鐘),讓key均勻的失效。
2. 雙key策略: 主key設置過期時間,備key不設置過期時間,當主key失效時,直接返回備key值。
3. 構建緩存高可用集群(針對緩存服務故障情況)
12. redis實現分布式session
基于redis的分布式session實現,依賴于前臺請求中攜帶的cookie和后臺生成的token。大致原理可以分為以下步驟:1,前端請求目標方法,攔截器判斷請求頭中是否攜帶cookie。
2,如果請求頭中攜帶cookie,則取出cookie并查詢redis中該cookie是否過期。如果沒有過期,則放行讓該請求去請求目標方法;如果已經過期,重新登陸
3,如果請求頭中,沒有攜帶cookie,則跳轉到登錄方法(同時攜帶當前請求的鏈接作為登錄后的回調地址)
4,進行登錄,登錄完畢生成指定的token存入redis中,生成cookie設置到response中。
5,登錄成功之后前端通過回調繼續請求目標方法。