點擊關注公眾號,實用技術文章及時了解
Redis就不多做介紹了,直接進入正題,通過本篇將學習到(代碼地址:https://gitee.com/chaitou/leilema.git):
初學者往往認為Redis就是緩存,這其實是個誤區,僅僅拿Redis當緩存好比拿瑞士軍刀開瓶蓋,但是Redis能做的遠不止如此,以下列舉幾種Redis的常見應用
Redis只有一個實例,沒有任何高可用分布式可言,只適合於初學者學習時使用,生產環境是絕對不允許這種情況出現的。一旦這個Redis實例崩潰了,小則緩存失效,全部數據查詢走數據庫,數據庫訪問需求暴增。大則影響分布式鎖的等功能造成業務異常
高可用Sentinel
如上圖,Sentinel模式也稱之為哨兵模式,該模式下擁有多個節點,當其中的master節點出現故障時,其他節點會自動頂替master節點,繼續提供服務,實現高可用。由於篇幅有限,這裡做個簡單的原理介紹:
首先可以看到圖上只有一個master節點(主節點),多個slave節點(從節點)。slave從節點根據一定的機制去複製主節點的數據,起到備份作用,也就是備胎,隨時等待上位的那種。(當然,這裡還有一個功能,可以根據系統情況做讀寫分離,只在master寫,只在slave讀)
每個Sentinel每隔一段時間就會向所有的Redis節點發送心跳檢測,來監控Redis節點是否正常。如果Sentinel1發現其中一個Redis1節點死掉了,為了公平起見,那麼他就會表態:「Redis1節點死掉了,誰贊成誰反對?」。
此時的所有Sentinel都會表態,當大多數Sentinel覺得這個redis節點死掉時,那就說明他死掉了。如果這個節點是master節點,那麼Sentinel就會挑選一個新的slave節點作為master節點,同時告訴所有slave節點要求成為該新master的slave節點。如果死掉的是slave節點,那就只需要通知以下slave節點死掉了,畢竟他不是master

而對於客戶端來說,也就是我們的Java程序來說,我們不再直連Redis節點了,我們需要連接的是Sentinel節點,讓Sentinel節點告訴我們真實的Redis節點信息。當然了,這些工作Jedis或者其他客戶端都幫我們做好了,只需要做個配置就行
高可用集群Cluster
Sentinel模式做到了高可用,但是實質還是只有一個master在提供服務(讀寫分離的情況本質也是master在提供服務),當master節點所在的機器內存不足以支撐系統的數據時,就需要考慮集群了。
如上圖所示,Cluster集群有多個Redis節點,每個節點負責一部分槽。也就是說Redis總共擁有16384個哈西槽,我們指定節點各自負責的槽。假設有3個節點,那麼1節點可以負責1-5461,2節點負責5462-10922,3節點負責10923-16384。當我們要存儲一個key時,key通過一致性hash算法尋找應該落到的槽,然後找到其對應Redis節點進行存儲。這樣就實現了Redis集群。
當然,考慮到穩定性,我們一般會給沒每個節點設置slave從節點,確保該集群的高可用。因此Cluster經常聽到的三主三從指的就是3個master集群,同時擁有3個slave從節點。
對比單機版就不對比了,沒什麼意義。關鍵是Cluster集群與Sentinel的對比
Cluster集群可擴展性強,當一台機器不夠用時,加機器重新分配槽就可以解決性能瓶頸。同時Cluster也是高可用的,一旦出現某個節點宕機,從節點會自動替補上去。同時當數據量大時,Cluster每個節點只負責一小部分槽,在確保命中率的情況下,性能更好
說了這麼多是不是意味着Sentinel對比起Cluster就一無是處了呢?當然不是,Cluster雖然好,但是幾乎只要涉及多key操作的命令,Cluster都是不支持的。比如mget、mset、pipeline等。原因也很好理解,mget key1 key2 key3 ...,這上面的key都分布在不同的cluster節點上,一條命令怎麼可能解決這個問題呢?我們能做的只有將所有key取出來,再進行分類,然後去不同的Redis實例上取(當然還有可能取錯實例),其他的命令讀者自行分析
因此,其實Cluster並非想象中的那麼好,架構師還是得根據系統情況進行分析。雖然大部分情況下我們都會選擇Cluster集群,但是當系統緩存的數據量小,但是頻繁需要使用sort、mget這類多key指令時,則Sentinel會更合適。還是那句話,沒有最完美的架構,只有最適合的架構。
springboot集成RedisTemplate說了這麼多,正餐終於來了,本篇我們還是主要以講解Redis Cluster為主,在集成之前,我們得先理清楚幾個概念
之前我們提到過Springboot使用了約定大於配置的思想,這使得我們集成Redis Cluster的RedisTemplate變得容易許多。只要我們按Springboot的約定來,就可以省去很多Bean的配置。簡化歸簡化,原理我們還是要懂的,如果我們使用Spring集成,我們需要配置以下幾個Bean
spring-boot-starter-data-redis引入相關依賴,如果是老版本的Springboot,引入的則是spring-boot-starter-redis。同時由於默認引入的是Lettuce,而本文使用的是Jedis,因此我們需要排除Lettuce的依賴,引入Jedis依賴
<properties><jedis-version>3.1.0</jedis-version></properties><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId><exclusions><exclusion><groupId>io.lettuce</groupId><artifactId>lettuce-core</artifactId></exclusion></exclusions></dependency><dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId><version>${jedis-version}</version></dependency>配置RedisTemplate既然使用了Springboot,約定大於配置。如果我們遵循了這一法則,JedisPoolConfig、RedisClusterConfiguration、JedisConnectionFactory這3個Bean是可以不需要手動配置的,而Springboot會幫我們做好,我們只需要專注於配置RedisTemplate就行
yml配置:
spring:cache:redis:time-to-live:10000redis:timeout:5000database:0cluster:nodes:148.70.139.121:7000,148.70.139.121:7001,148.70.139.121:7002,148.70.139.121:7003,148.70.139.121:7004,148.70.139.121:7005max-redirects:3jedis:pool:max-active:8max-wait:-1max-idle:8min-idle:0RedisTemplate Bean:這裡需要注意一下序列化的操作
packagecom.bugpool.leilema.freamwork.configuration;importcom.fasterxml.jackson.annotation.JsonAutoDetect;importcom.fasterxml.jackson.annotation.PropertyAccessor;importcom.fasterxml.jackson.databind.ObjectMapper;importorg.springframework.cache.annotation.EnableCaching;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importorg.springframework.data.redis.connection.RedisConnectionFactory;importorg.springframework.data.redis.core.RedisTemplate;importorg.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;importorg.springframework.data.redis.serializer.StringRedisSerializer;@Configuration@EnableCachingpublicclassRedisConfiguration{@BeanpublicRedisTemplate<String,Object>redisTemplate(RedisConnectionFactoryfactory){RedisTemplate<String,Object>template=newRedisTemplate<>();template.setConnectionFactory(factory);//使用Jackson2JsonRedisSerialize替換默認的jdkSerializeable序列化Jackson2JsonRedisSerializerjackson2JsonRedisSerializer=newJackson2JsonRedisSerializer(Object.class);ObjectMapperom=newObjectMapper();om.setVisibility(PropertyAccessor.ALL,JsonAutoDetect.Visibility.ANY);om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);jackson2JsonRedisSerializer.setObjectMapper(om);StringRedisSerializerstringRedisSerializer=newStringRedisSerializer();//key採用String的序列化方式template.setKeySerializer(stringRedisSerializer);//hash的key也採用String的序列化方式template.setHashKeySerializer(stringRedisSerializer);//value序列化方式採用jacksontemplate.setValueSerializer(jackson2JsonRedisSerializer);//hash的value序列化方式採用jacksontemplate.setHashValueSerializer(jackson2JsonRedisSerializer);template.afterPropertiesSet();returntemplate;}}由於原生的RedisTemplate也不是非常好用,一般我們會再自己封裝一層。有些人習慣把這一層稱之為RedisDao,當然也有人習慣把他當RedisUtils工具類來使用,這裡筆者並不糾結那種方式跟好,筆者就將他作為Service,需要是注入使用就好
RedisService:
packagecom.bugpool.leilema.freamwork.utils;importlombok.extern.slf4j.Slf4j;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.data.redis.core.RedisTemplate;importorg.springframework.stereotype.Component;importorg.springframework.util.CollectionUtils;importjava.util.List;importjava.util.Map;importjava.util.Set;importjava.util.concurrent.TimeUnit;@Component@Slf4jpublicclassRedisService{@AutowiredprivateRedisTemplate<String,Object>redisTemplate;/***指定緩存失效時間**@paramkey鍵*@paramtime時間(秒)*@return*/publicbooleanexpire(Stringkey,longtime){try{if(time>0){redisTemplate.expire(key,time,TimeUnit.SECONDS);}returntrue;}catch(Exceptione){log.error("exceptionwhenexpirekey{}.",key,e);returnfalse;}}/***根據key獲取過期時間**@paramkey鍵不能為null*@return時間(秒)返回0代表為永久有效*/publiclonggetExpire(Stringkey){returnredisTemplate.getExpire(key,TimeUnit.SECONDS);}/***判斷key是否存在**@paramkey鍵*@returntrue存在false不存在*/publicbooleanhasKey(Stringkey){try{returnredisTemplate.hasKey(key);}catch(Exceptione){log.error("exceptionwhencheckkey{}.",key,e);returnfalse;}}/***刪除緩存**@paramkey可以傳一個值或多個*/@SuppressWarnings("unchecked")publicvoiddel(String...key){if(key!=null&&key.length>0){if(key.length==1){redisTemplate.delete(key[0]);}else{redisTemplate.delete(CollectionUtils.arrayToList(key));}}}/***普通緩存獲取**@paramkey鍵*@return值*/publicObjectget(Stringkey){returnkey==null?null:redisTemplate.opsForValue().get(key);}/***普通緩存放入**@paramkey鍵*@paramvalue值*@returntrue成功false失敗*/publicbooleanset(Stringkey,Objectvalue){try{redisTemplate.opsForValue().set(key,value);returntrue;}catch(Exceptione){log.error("exceptionwhensetkey{}.",key,e);returnfalse;}}/***普通緩存放入並設置時間**@paramkey鍵*@paramvalue值*@paramtime時間(秒)time要大於0如果time小於等於0將設置無限期*@returntrue成功false失敗*/publicbooleanset(Stringkey,Objectvalue,longtime){try{if(time>0){redisTemplate.opsForValue().set(key,value,time,TimeUnit.SECONDS);}else{set(key,value);}returntrue;}catch(Exceptione){log.error("exceptionwhensetkey{}.",key,e);returnfalse;}}/***遞增**@paramkey鍵*@paramdelta要增加幾(大於0)*@return*/publiclongincr(Stringkey,longdelta){if(delta<=0){thrownewRuntimeException("遞增因子必須大於0");}returnredisTemplate.opsForValue().increment(key,delta);}/***遞減**@paramkey鍵*@paramdelta要減少幾(小於0)*@return*/publiclongdecr(Stringkey,longdelta){if(delta<=0){thrownewRuntimeException("遞減因子必須大於0");}returnredisTemplate.opsForValue().increment(key,-delta);}/***HashGet**@paramkey鍵不能為null*@paramitem項不能為null*@return值*/publicObjecthget(Stringkey,Stringitem){returnredisTemplate.opsForHash().get(key,item);}/***獲取hashKey對應的所有鍵值**@paramkey鍵*@return對應的多個鍵值*/publicMap<Object,Object>hmget(Stringkey){returnredisTemplate.opsForHash().entries(key);}/***HashSet**@paramkey鍵*@parammap對應多個鍵值*@returntrue成功false失敗*/publicbooleanhmset(Stringkey,Map<String,Object>map){try{redisTemplate.opsForHash().putAll(key,map);returntrue;}catch(Exceptione){log.error("exceptionwhenhashsetkey{}.",key,e);returnfalse;}}/***HashSet並設置時間**@paramkey鍵*@parammap對應多個鍵值*@paramtime時間(秒)*@returntrue成功false失敗*/publicbooleanhmset(Stringkey,Map<String,Object>map,longtime){try{redisTemplate.opsForHash().putAll(key,map);if(time>0){expire(key,time);}returntrue;}catch(Exceptione){log.error("exceptionwhenhashsetkey{}.",key,e);returnfalse;}}/***向一張hash表中放入數據,如果不存在將創建**@paramkey鍵*@paramitem項*@paramvalue值*@returntrue成功false失敗*/publicbooleanhset(Stringkey,Stringitem,Objectvalue){try{redisTemplate.opsForHash().put(key,item,value);returntrue;}catch(Exceptione){log.error("exceptionwhenhashsetkey{},item{}",key,item,e);returnfalse;}}/***向一張hash表中放入數據,如果不存在將創建**@paramkey鍵*@paramitem項*@paramvalue值*@paramtime時間(秒)注意:如果已存在的hash表有時間,這裡將會替換原有的時間*@returntrue成功false失敗*/publicbooleanhset(Stringkey,Stringitem,Objectvalue,longtime){try{redisTemplate.opsForHash().put(key,item,value);if(time>0){expire(key,time);}returntrue;}catch(Exceptione){log.error("exceptionwhenhashsetkey{},item{}",key,item,e);returnfalse;}}/***刪除hash表中的值**@paramkey鍵不能為null*@paramitem項可以使多個不能為null*/publicvoidhdel(Stringkey,Object...item){redisTemplate.opsForHash().delete(key,item);}/***判斷hash表中是否有該項的值**@paramkey鍵不能為null*@paramitem項不能為null*@returntrue存在false不存在*/publicbooleanhHasKey(Stringkey,Stringitem){returnredisTemplate.opsForHash().hasKey(key,item);}/***hash遞增如果不存在,就會創建一個並把新增後的值返回**@paramkey鍵*@paramitem項*@paramby要增加幾(大於0)*@return*/publicdoublehincr(Stringkey,Stringitem,doubleby){returnredisTemplate.opsForHash().increment(key,item,by);}/***hash遞減**@paramkey鍵*@paramitem項*@paramby要減少記(小於0)*@return*/publicdoublehdecr(Stringkey,Stringitem,doubleby){returnredisTemplate.opsForHash().increment(key,item,-by);}/***根據key獲取Set中的所有值**@paramkey鍵*@return*/publicSet<Object>sGet(Stringkey){try{returnredisTemplate.opsForSet().members(key);}catch(Exceptione){returnnull;}}/***根據value從一個set中查詢,是否存在**@paramkey鍵*@paramvalue值*@returntrue存在false不存在*/publicbooleansHasKey(Stringkey,Objectvalue){try{returnredisTemplate.opsForSet().isMember(key,value);}catch(Exceptione){returnfalse;}}/***將數據放入set緩存**@paramkey鍵*@paramvalues值可以是多個*@return成功個數*/publiclongsSet(Stringkey,Object...values){try{returnredisTemplate.opsForSet().add(key,values);}catch(Exceptione){return0;}}/***將set數據放入緩存**@paramkey鍵*@paramtime時間(秒)*@paramvalues值可以是多個*@return成功個數*/publiclongsSetAndTime(Stringkey,longtime,Object...values){try{Longcount=redisTemplate.opsForSet().add(key,values);if(time>0)expire(key,time);returncount;}catch(Exceptione){return0;}}/***獲取set緩存的長度**@paramkey鍵*@return*/publiclongsGetSetSize(Stringkey){try{returnredisTemplate.opsForSet().size(key);}catch(Exceptione){return0;}}/***移除值為value的**@paramkey鍵*@paramvalues值可以是多個*@return移除的個數*/publiclongsetRemove(Stringkey,Object...values){try{Longcount=redisTemplate.opsForSet().remove(key,values);returncount;}catch(Exceptione){return0;}}/***獲取list緩存的內容**@paramkey鍵*@paramstart開始*@paramend結束0到-1代表所有值*@return*/publicList<Object>lGet(Stringkey,longstart,longend){try{returnredisTemplate.opsForList().range(key,start,end);}catch(Exceptione){returnnull;}}/***獲取list緩存的長度**@paramkey鍵*@return*/publiclonglGetListSize(Stringkey){try{returnredisTemplate.opsForList().size(key);}catch(Exceptione){return0;}}/***通過索引獲取list中的值**@paramkey鍵*@paramindex 索引 index>=0時,0表頭,1 第二個元素,依次類推;index<0時,-1,表尾,-2倒數第二個元素,依次類推*@return*/publicObjectlGetIndex(Stringkey,longindex){try{returnredisTemplate.opsForList().index(key,index);}catch(Exceptione){returnnull;}}/***將list放入緩存**@paramkey鍵*@paramvalue值*@return*/publicbooleanlSet(Stringkey,Objectvalue){try{redisTemplate.opsForList().rightPush(key,value);returntrue;}catch(Exceptione){returnfalse;}}/***將list放入緩存**@paramkey鍵*@paramvalue值*@paramtime時間(秒)*@return*/publicbooleanlSet(Stringkey,Objectvalue,longtime){try{redisTemplate.opsForList().rightPush(key,value);if(time>0)expire(key,time);returntrue;}catch(Exceptione){returnfalse;}}/***將list放入緩存**@paramkey鍵*@paramvalue值*@return*/publicbooleanlSet(Stringkey,List<Object>value){try{redisTemplate.opsForList().rightPushAll(key,value);returntrue;}catch(Exceptione){returnfalse;}}/***將list放入緩存**@paramkey鍵*@paramvalue值*@paramtime時間(秒)*@return*/publicbooleanlSet(Stringkey,List<Object>value,longtime){try{redisTemplate.opsForList().rightPushAll(key,value);if(time>0)expire(key,time);returntrue;}catch(Exceptione){returnfalse;}}/***根據索引修改list中的某條數據**@paramkey鍵*@paramindex索引*@paramvalue值*@return*/publicbooleanlUpdateIndex(Stringkey,longindex,Objectvalue){try{redisTemplate.opsForList().set(key,index,value);returntrue;}catch(Exceptione){e.printStackTrace();returnfalse;}}/***移除N個值為value**@paramkey鍵*@paramcount移除多少個*@paramvalue值*@return移除的個數*/publiclonglRemove(Stringkey,longcount,Objectvalue){try{Longremove=redisTemplate.opsForList().remove(key,count,value);returnremove;}catch(Exceptione){return0;}}}使用當我們需要使用到Redis時,使用@Autowired注入。一篇是不可能講完所有Redis的操作的,因此舉個例子,大家自己摸索。
packagecom.bugpool.leilema.freamwork.utils;importcom.bugpool.leilema.product.entity.ProductInfo;importorg.junit.Assert;importorg.junit.jupiter.api.Test;importorg.junit.runner.RunWith;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.boot.test.context.SpringBootTest;importorg.springframework.test.context.junit4.SpringRunner;importjava.math.BigDecimal;importstaticorg.junit.jupiter.api.Assertions.*;@RunWith(SpringRunner.class)@SpringBootTestclassRedisServiceTest{@AutowiredRedisServiceredisService;@Testvoidget(){ProductInfoproductInfo=newProductInfo();productInfo.setProductName("推拿").setProductId(1).setProductPrice(newBigDecimal(100));redisService.set("testRedisGet",productInfo,100);ProductInfoproductInfo1=(ProductInfo)redisService.get("testRedisGet");Assert.assertTrue(productInfo1.getProductName().equals(productInfo.getProductName()));}}集成Spring Cache如果你只是想要使用Redis作為緩存,而在每個方法中都使用redisService.set("testRedisGet", productInfo, 100);去設置緩存,侵入性還是很高的。因此Spring Cache通過註解的方式,方便緩存的使用。Spring Cache的配置我們上方已經配置過了,這裡拿出來再講一遍
配置yml: 以下配置指定了Spring Cache使用Redis做緩存,並且緩存失效時間是10s(該有效時間只針對使用@Cacheable這些註解,不影響我們RedisService的使用)
spring:cache:redis:time-to-live:10000RedisConfiguration: 我們已經在配置RedisTemplate時加上了@EnableCaching的註解,該註解通知Spring Ioc開啟Spring Cache,實質是一個後置處理器,它檢查每個Spring bean是否在公共方法上有@Cacheable子類的注釋。
如果找到這樣的注釋,則自動創建代理通過攔截方法調用處理緩存。在Jdk動態代理中我曾寫過一個例子,大致原理可以參考
@EnableCachingpublicclassRedisConfiguration{}使用@Override@Cacheable(value="redis",key="#root.targetClass+'::'+#root.methodName+'::'+#productName")publicListgetByLikeName(StringproductName){returnproductInfoMapper.getByLikeName(productName);}註解包括@Cacheable、@CacheEvict、@CachePut
@Cacheable: 每次執行方法前,會根據key查找redis是否存在緩存,如果存在則直接返回緩存結果。如果不存在,則執行方法,方法結束後,將結果放入緩存中。一般用在select查詢類的方法上
@CachePut: 執行方法前,不管緩存是否存在,都執行方法,並且把結果放入緩存中。一般用在Update方法上
@CacheEvict: 清除緩存,一般放在delete方法上
SpringEl表達式: key = "#root.targetClass + '::' + #root.methodName + '::' + #productName"這句話使用的就是SpringEL表達式,一般我們設置Key都是需要加上類名做前綴,防止與其他類的緩存混淆
最後要強調的是,Spring Cache的使用在大部分的場景下,提升都非常有限,想要用好Redis,還是認真分析業務場景,手動使用RedisTemplate進行優化吧!
推薦
Java面試題寶典
技術內卷群,一起來學習!!
PS:因為公眾號平台更改了推送規則,如果不想錯過內容,記得讀完點一下「在看」,加個「星標」,這樣每次新文章推送才會第一時間出現在你的訂閱列表里。點「在看」支持我們吧!