如何假裝成一個服務端開發(一)html
如何假裝成一個服務端開發(二)java
如何假裝成一個服務端開發(三) c++
如何假裝成一個服務端開發(五)redis
如何假裝成一個服務端開發(六)spring
若是你想在網上再找一個這麼詳細的入門 Spirng Boot + redis的項目,那你可得費點力氣了……由於我就嘗試過……數據庫
咱們知道數據庫鏈接和調用是耗時的(包括鏈接,查詢等操做)。並且在高併發的狀況下會出現明顯的瓶頸。因此如何減小數據庫訪問就逐漸成爲互聯網系統加速的重要優化點。爲此NoSQL誕生了,其中使用普遍的就是Redis和MongoDB。這裏先介紹一下Redis。centos
Redis 是一種運行在內存的數據庫,不少時候咱們都會把從數據庫查詢出來的數據放入Redis,當用戶再次查詢相同數據的時候,優先使用Redis中存在的數據,由於是存放在內存中,因此速度很快。另外Redis還能夠將數據持久化到磁盤中,不少網站甚至放棄了後臺數據庫,徹底使用Redis來進行數據存儲。緩存
筆者爲了學(fan)習(qiang),特意買了一個廉價的VPS,這裏正好利用起來,在服務器上安裝了mariadb 和 redis。這裏不詳細介紹安裝流程,你們能夠本機安裝,網上資料不少。
PS 筆者使用的是centos7 對於安全的限制很嚴,安裝完成mariadb和redis以後,若是須要遠程訪問,須要開啓防火牆端口。
PS2 這兩個東西須要遠程訪問都須要作一些設置,好比redis須要去掉bind 127.0.0.1的配置等。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <exclusions> <!--不依賴Redis的異步客戶端lettuce--> <exclusion> <groupId>io.lettuce</groupId> <artifactId>lettuce-core</artifactId> </exclusion> </exclusions> </dependency> <!--引入Redis的客戶端驅動jedis--> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> </dependency>
引入上面的依賴,對於redis的依賴咱們選擇了jedis而spring默認使用的是lettuce。至於jedis就相似於jdbc這種,至關於鏈接redis的驅動。redis相似一個數據庫,c++鏈接它須要本身的封裝,java固然也要本身的封裝,這就是jedis了。
查看網上的各類講解和例子,通常都是使用jedis,因此固然跟隨大衆科技了。
老夫寫代碼就是一把梭!開個玩笑……先來一個列子,固然你大概會 「???」
首先在application.properties中添加配置項
#配置鏈接池屬性 spring.redis.jedis.pool.min-idle=5 spring.redis.jedis.pool.max-active=10 spring.redis.jedis.pool.max-idle=10 spring.redis.jedis.pool.max-wait=2000 #配置Redis服務器屬性 spring.redis.port=6379 spring.redis.host=xxx.xxx.xxx.xxx #spring.redis.password=123456 #Redis鏈接超時時間,單位毫秒 spring.redis.timeout=1000
而後修改XXApplication
@SpringBootApplication public class RedisApplication { @Autowired private RedisTemplate redisTemplate = null; @PostConstruct public void init(){ initRedisTemplate(); } private void initRedisTemplate(){ RedisSerializer redisSerializer = redisTemplate.getStringSerializer(); redisTemplate.setKeySerializer(redisSerializer); redisTemplate.setHashKeySerializer(redisSerializer); } .... }
最後咱們須要一個controller來測試
@Controller @RequestMapping("/redis") public class RedisController { @Autowired private RedisTemplate redisTemplate; @Autowired private StringRedisTemplate stringRedisTemplate; @RequestMapping("/test") @ResponseBody public String testStringAndHash() { redisTemplate.opsForValue().set("username","yxwang"); return "OK"; }
訪問http://localhost:8080/redis/test 頁面輸出ok
而後經過redis看看有沒有存入,因爲我是遠程登陸 redis-cli -h xxx.xx.xx.xx -p 6379
而後 get username 發現有輸出,這就表示已經存進去了。
來看下咱們的auto-config(spring-boot-autoconfigure就是spring幫咱們作自動配置的核心包)幫咱們作了什麼。在application.properties中spring.redis相關的配置項目會被讀取到 RedisProperties 這個類中。
而咱們的配置類 JedisConnectionConfiguration又會讀取類 RedisProperties 中的內容。經過IoC向外暴露了這個麼一個bean
@Bean @ConditionalOnMissingBean(RedisConnectionFactory.class) public JedisConnectionFactory redisConnectionFactory() throws UnknownHostException { return createJedisConnectionFactory(); }
而JedisConnectionFactory這個類繼承與RedisConnectionFactory,經過它,能夠生成一個RedisConnection的接口對象,這個對象就是對Redis底層接口的封裝。
在RedisAutoConfiguration中提供了兩個bean
public class RedisAutoConfiguration { @Bean @ConditionalOnMissingBean(name = "redisTemplate") public RedisTemplate<Object, Object> redisTemplate( RedisConnectionFactory redisConnectionFactory) throws UnknownHostException { RedisTemplate<Object, Object> template = new RedisTemplate<>(); //注入了 RedisConnectionFactory 這個Factory主要用於生成RedisConnection,用於和Redis創建鏈接 template.setConnectionFactory(redisConnectionFactory); return template; } @Bean @ConditionalOnMissingBean public StringRedisTemplate stringRedisTemplate( RedisConnectionFactory redisConnectionFactory) throws UnknownHostException { StringRedisTemplate template = new StringRedisTemplate(); template.setConnectionFactory(redisConnectionFactory); return template; } }
這兩個bean就是咱們用於最終操做Redis的類,它首先從RedisConnectionFactory中獲取Redis鏈接,而後執行Redis操做,最終還會關閉Redis鏈接。
因此咱們解決了application.properties的做用流程,也知道了 redisTemplate stringRedisTemplate的注入流程。
PS 當不適用spring boot時,咱們也徹底能夠拷貝上面的代碼,手動生成RedisConnectionFactory,而後再手動生成redisTemplate
咱們發現,當輸出get username獲取redis中存儲的值時,返回的是 "\xac\xed\x00\x05t\x00\x06yxwang" 這麼一串東西。這是怎麼回事呢?首先須要清楚的是,Redis 是一種基於字符串存儲的 NoSQL,而 Java 是基於對象的語言,對象是沒法存儲到 Redis 中的,不過 Java 提供了序列化機制,只要類實現了 java.io.Serializable 接口,就表明類的對象可以進行序列化,經過將類對象進行序列化就可以獲得二進制字符串,這樣 Redis 就能夠將這些類對象以字符串進行存儲。
Spring 提供了序列化器的機制,而且實現了幾個序列化器
而上面這個奇怪的字符串就是由於String對象經過了JdkSerializationRedisSerializer序列化以後存入的。可是咱們的key "username" 爲何沒有變得奇怪呢?由於咱們在XXApplication主動設置了序列化的接口 StringRedisSerializer。
RedisTemplate能夠設置如下序列化器
屬 性 | 描 述 | 備 注 |
---|---|---|
defaultSerializer | 默認序列化器 | 若是沒有設置,則使用 JdkSerializationRedisSerializer |
keySerializer | Redis 鍵序列化器 | 若是沒有設置,則使用默認序列化器 |
valueSerializer | Redis 值序列化器 | 若是沒有設置,則使用默認序列化器 |
hashKeySerializer | Redis 散列結構 field 序列化器 | 若是沒有設置,則使用默認序列化器 |
hashValueSerializer | Redis 散列結構 value 序列化器 | 若是沒有設置,則使用默認序列化器 |
stringSerializer | 字符串序列化器 | RedisTemplate 自動賦值爲 StringRedisSerializer 對象 |
那麼對於上面例子,最後咱們還須要聊一下的就是@Controller中是如何將數據存入redis中的了。
咱們經過redisTemplate進行操做(也能夠經過stringRedisTemplate,區別就是stringRedisTemplate 至關於redisTemplate<String,String>),首先redisTemplate獲取redis鏈接,而後進行操做,而後關閉鏈接(上面有提到)。
redis 可以支持7種類型的數據結構,這7種類型是字符串、散列、列表(鏈表)、集合、有序集合、基數和地理位置。爲此 Spring 針對每一種數據結構的操做都提供了對應的操做接口.
PS 最新版本還有一種和分佈式相關的 ClusterOperations 這裏咱們暫且不表。若有須要能夠看這裏
操 做 接 口 | 功 能 | 備 注 | 獲取接口方法 | 連續操做接口 | 獲取連續操做接口 |
---|---|---|---|---|---|
GeoOperations | 地理位置操做接口 | 使用很少,本書再也不介紹 | redisTemplate.opsForGeo(); | BoundGeoOperations | redisTemplate.boundGeoOps("geo"); |
HashOperations | 散列操做接口 | redisTemplate.opsForHash(); | BoundHashOperations | redisTemplate.boundHashOps("hash"); | |
HyperLogLogOperations | 基數操做接口 | 使用很少,本書再也不介紹 | redisTemplate.opsForHyperLogLog(); | ||
ListOperations | 列表(鏈表)操做接口 | redisTemplate.opsForList(); | BoundListOperations | redisTemplate.boundListOps("list"); | |
SetOperations | 集合操做接口 | redisTemplate.opsForSet(); | BoundSetOperations | redisTemplate.boundSetOps("set"); | |
ValueOperations | 字符串操做接口 | redisTemplate.opsForValue(); | BoundValueOperations | redisTemplate.boundValueOps("string"); | |
ZSetOperations | 有序集合操做接口 | redisTemplate.opsForZSet(); | BoundZSetOperations | redisTemplate.boundZSetOps("zset"); |
這裏有必要介紹下所謂的連續操做。redis中可與存放多個Hash(list set等都同樣),好比咱們存在一個hash,名字叫作 "hash1",那麼咱們會像這樣添加數據 stringRedisTemplate.opsForHash().put("hash1", "field3", "value3"); 因而hash1這個hash表中,存在一個key是field3 ,value是 value3的鍵值對。
若是我須要繼續添加那麼仍是須要 stringRedisTemplate.opsForHash().put("hash1", "xxxx", "xxx"); 因此經過stringRedisTemplate.opsForHash() 返回的HashOperations並不會和hash1綁定,咱們能夠用它操做全部的hash表。
能夠經過stringRedisTemplate.boundHashOps("hash1"); 返回一個 BoundHashOperations ,它自動和hash1綁定,能夠直接操做hashOps.delete("field1", "field2");
這裏各類數據類型就再也不介紹了,基本上須要使用的時候學習下就行。不過有一點ZSet,可能在java中沒有對應的數據結構,它是用來作有權重的列表的,好比用來作排行榜。
這裏帖一段測試代碼,用到的時候能夠當作參考學習。
@RequestMapping("/zset") @ResponseBody public Map<String, Object> testZset() { Set<TypedTuple<String>> typedTupleSet = new HashSet<>(); for (int i=1; i<=9; i++) { // 分數 double score = i*0.1; // 建立一個TypedTuple對象,存入值和分數 TypedTuple<String> typedTuple = new DefaultTypedTuple<String>("value" + i, score); typedTupleSet.add(typedTuple); } // 往有序集合插入元素 stringRedisTemplate.opsForZSet().add("zset1", typedTupleSet); // 綁定zset1有序集合操做 BoundZSetOperations<String, String> zsetOps = stringRedisTemplate.boundZSetOps("zset1"); // 增長一個元素 zsetOps.add("value10", 0.26); Set<String> setRange = zsetOps.range(1, 6); // 按分數排序獲取有序集合 Set<String> setScore = zsetOps.rangeByScore(0.2, 0.6); // 定義值範圍 Range range = new Range(); range.gt("value3");// 大於value3 // range.gte("value3");// 大於等於value3 // range.lt("value8");// 小於value8 range.lte("value8");// 小於等於value8 // 按值排序,請注意這個排序是按字符串排序 Set<String> setLex = zsetOps.rangeByLex(range); // 刪除元素 zsetOps.remove("value9", "value2"); // 求分數 Double score = zsetOps.score("value8"); // 在下標區間下,按分數排序,同時返回value和score Set<TypedTuple<String>> rangeSet = zsetOps.rangeWithScores(1, 6); // 在分數區間下,按分數排序,同時返回value和score Set<TypedTuple<String>> scoreSet = zsetOps.rangeByScoreWithScores(1, 6); // 按從大到小排序 Set<String> reverseSet = zsetOps.reverseRange(2, 8); Map<String, Object> map = new HashMap<String, Object>(); map.put("success", true); return map; }
和sql同樣,每次咱們調用一個操做就會創建一條連接,好比
redisTemplate.opsForValue().set("key1", "value1"); redisTemplate.opsForHash().put("hash", "field", "hvalue");
上面代碼進行了兩次操做,這個時候回創建兩條和redis的連接,這樣是比較浪費資源的,爲此redis推出了兩個接口。它們的做用是讓 RedisTemplate 進行回調,經過它們能夠在同一條鏈接下執行多個 Redis 命令。其中 SessionCallback 提供了良好的封裝,對於開發者比較友好,所以在實際的開發中應該優先選擇使用它;相對而言,RedisCallback 接口比較底層,須要處理的內容也比較多,可讀性較差,因此在非必要的時候儘可能不選擇使用它。
// 須要處理底層的轉換規則,若是不考慮改寫底層,儘可能不使用它 public void useRedisCallback(RedisTemplate redisTemplate) { redisTemplate.execute(new RedisCallback() { @Override public Object doInRedis(RedisConnection rc) throws DataAccessException { rc.set("key1".getBytes(), "value1".getBytes()); rc.hSet("hash".getBytes(), "field".getBytes(), "hvalue".getBytes()); return null; } }); } // 高級接口,比較友好,通常狀況下,優先使用它 public void useSessionCallback(RedisTemplate redisTemplate) { redisTemplate.execute(new SessionCallback() { @Override public Object execute(RedisOperations ro) throws DataAccessException { ro.opsForValue().set("key1", "value1"); ro.opsForHash().put("hash", "field", "hvalue"); return null; } }); }
Redis中的事務有是哪一個關聯命令,watch 用於監聽Redis中的幾個鍵,而後經過multi表示開啓事務(注意是開啓,事務尚未被執行),而後經過exe命令執行事務。可是在事務執行以前會檢查被watch監聽的鍵是否發生變化,若是發生了變化,那麼就不會執行事務。當事務執行時原子性的不會被其餘客戶端打斷。
另外,通常若是須要執行事務,都會有多個語句,因此絕大多數狀況會和SessionCallback一塊兒使用。
@RequestMapping("/test/translation") @ResponseBody public String testTranslation() { redisTemplate.execute(new SessionCallback() { @Override public Object execute(RedisOperations operations) throws DataAccessException { // 設置要監控key1 key2 operations.watch(Arrays.asList("key1","key2")); operations.multi(); operations.opsForValue().set("key2", "value2"); operations.opsForValue().set("key1", "value1"); return operations.exec(); } }); return "OK"; }
上面代碼有一個地方須要特別注意,咱們看下execute方法源碼
public <T> T execute(SessionCallback<T> session) { Assert.isTrue(this.initialized, "template not initialized; call afterPropertiesSet() before using it"); Assert.notNull(session, "Callback object must not be null"); RedisConnectionFactory factory = this.getRequiredConnectionFactory(); RedisConnectionUtils.bindConnection(factory, this.enableTransactionSupport); Object var3; try { var3 = session.execute(this); } finally { RedisConnectionUtils.unbindConnection(factory); } return var3; }
注意到沒,execute的返回值,就是SessionCallback的返回值,並且……是同步的。因此redisTamplate.execute是同步執行。
不管是使用事務,仍是使用SessionCallback,redis仍是將命令一條一條送到服務端進行處理,這是相對比較慢的。咱們能夠將全部的命令進行打包,這樣就只會傳輸一次。
@RequestMapping("/test/pipeline") @ResponseBody public String testPipeline(){ redisTemplate.executePipelined(new SessionCallback<Object>() { @Override public Object execute(RedisOperations redisOperations) throws DataAccessException { for (int i=1; i<=1000; i++) { redisOperations.opsForValue().set("pipeline_" + i, "value_" + i); } return null; } }); return "OK"; }
我的以爲這個Redis最遊手好閒的功能,由於無論在使用上仍是過程當中這都和數據沒有太大的直接聯繫。(頗有多是我沒有深刻學習原理)
首先是 Redis 提供一個渠道,讓消息可以發送到這個渠道上,而多個系統能夠監聽這個渠道,如短信、微信和郵件系統均可以監聽這個渠道,當一條消息發送到渠道,渠道就會通知它的監聽者,這樣短信、微信和郵件系統就可以獲得這個渠道給它們的消息了,這些監聽者會根據本身的須要去處理這個消息
大概是就是這麼張圖
首先須要定義一個監聽器,這很簡單
@Component public class RedisMessageListener implements MessageListener { @Override public void onMessage(Message message, byte[] pattern) { // 消息體 String body = new String(message.getBody()); // 渠道名稱 String topic = new String(pattern); System.out.println(body); System.out.println(topic); } }
而後就是經過redis註冊監聽。
而後再XXXApplication中添加以下代碼
@Bean public ThreadPoolTaskScheduler initTaskScheduler() { if (taskScheduler != null) { return taskScheduler; } taskScheduler = new ThreadPoolTaskScheduler(); taskScheduler.setPoolSize(20); return taskScheduler; } /** * 定義Redis的監聽容器 * @return 監聽容器 */ @Bean public RedisMessageListenerContainer initRedisContainer() { RedisMessageListenerContainer container = new RedisMessageListenerContainer(); // Redis鏈接工廠 container.setConnectionFactory(connectionFactory); // 設置運行任務池 container.setTaskExecutor(initTaskScheduler()); // 定義監聽渠道,名稱爲topic1 Topic topic = new ChannelTopic("topic1"); // 使用監聽器監聽Redis的消息 container.addMessageListener(redisMessageListener, topic); return container; }
PS: 一些自動注入的東西這裏沒列出來
這裏我有個疑惑,須要提供一個返回RedisMessageListenerContainer的Bean,若是直接運行initRedisContainer的代碼,沒有提供bean,那麼註冊不會生效。
也就是Redis內部是經過依賴注入獲取RedisMessageListenerContainer對象,而後將其註冊到某個地方的。
最後咱們能夠經過命令行運行 publish topic1 msg 往 topic1通道發送msg消息。
也能夠經過代碼發送
redisTemplate.convertAndSend(channel, message);
爲了加強 Redis 的計算能力,Redis 在2.6版本後提供了 Lua 腳本的支持,並且執行 Lua 腳本在 Redis 中還具有原子性,因此在須要保證數據一致性的高併發環境中,咱們也可使用 Redis 的 Lua 語言來保證數據的一致性,且 Lua 腳本具有更增強大的運算功能,在高併發須要保證數據一致性時,Lua 腳本方案比使用 Redis 自身提供的事務要更好一些。
在 Redis 中有兩種運行 Lua 的方法,一種是直接發送 Lua 到 Redis 服務器去執行,另外一種是先把 Lua 發送給 Redis,Redis 會對 Lua 腳本進行緩存,而後返回一個 SHA1 的32位編碼回來,以後只須要發送 SHA1 和相關參數給 Redis 即可以執行了。這裏須要解釋的是爲何會存在經過32位編碼執行的方法。若是 Lua 腳本很長,那麼就須要經過網絡傳遞腳本給 Redis 去執行了,而現實的狀況是網絡的傳遞速度每每跟不上 Redis 的執行速度,因此網絡就會成爲 Redis 執行的瓶頸。若是隻是傳遞32位編碼和參數,那麼須要傳遞的消息就少了許多,這樣就能夠極大地減小網絡傳輸的內容,從而提升系統的性能。
爲了支持 Redis 的 Lua 腳本,Spring 提供了 RedisScript 接口,與此同時也有一個 DefaultRedisScript 實現類。
public interface RedisScript<T> { // 獲取腳本的Sha1 String getSha1(); // 獲取腳本返回值 Class<T> getResultType(); // 獲取腳本的字符串 String getScriptAsString(); }
這裏 Spring 會將 Lua 腳本發送到 Redis 服務器進行緩存,而此時 Redis 服務器會返回一個32位的 SHA1 編碼,這時候經過 getSha1 方法就能夠獲得 Redis 返回的這個編碼了;getResultType 方法是獲取 Lua 腳本返回的 Java 類型;getScriptAsString 是返回腳本的字符串.
@RequestMapping("/lua") @ResponseBody public Map<String, Object> testLua() { DefaultRedisScript<String> rs = new DefaultRedisScript<String>(); // 設置腳本 rs.setScriptText("return 'Hello Redis'"); // 定義返回類型。注意:若是沒有這個定義,Spring 不會返回結果 rs.setResultType(String.class); RedisSerializer<String> stringSerializer = redisTemplate.getStringSerializer(); // 執行 Lua 腳本 String str = (String) redisTemplate.execute( rs, stringSerializer, stringSerializer, null); Map<String, Object> map = new HashMap<String, Object>(); map.put("str", str); return map; }
上面代碼執行了一個很是簡單的Lua腳本 ,就是返回Hello Redis字符串。
redisTemplate 中,execute 方法執行腳本的方法有兩種
public <T> T execute(RedisScript<T> script, List<K> keys, Object... args) public <T> T execute(RedisScript<T> script, RedisSerializer<?> argsSerializer, RedisSerializer<T> resultSerializer, List<K> keys, Object... args)
從參數的名稱能夠知道,script 就是咱們定義的 RedisScript 接口對象,keys 表明 Redis 的鍵,args 是這段腳本的參數。兩個方法最大區別是一個存在序列化器的參數,另一個不存在。對於不存在序列化參數的方法,Spring 將採用 RedisTemplate 提供的 valueSerializer 序列化器對傳遞的鍵和參數進行序列化。這裏咱們採用了第二個方法調度腳本,而且設置爲字符串序列化器,其中第一個序列化器是鍵的序列化器,第二個是參數序列化器,這樣鍵和參數就在字符串序列化器下被序列化了。
下面咱們再考慮存在參數的狀況。例如,咱們寫一段 Lua 腳本用來判斷兩個字符串是否相同
redis.call('set', KEYS[1], ARGV[1]) redis.call('set', KEYS[2], ARGV[2]) local str1 = redis.call('get', KEYS[1]) local str2 = redis.call('get', KEYS[2]) if str1 == str2 then return 1 end return 0
@RequestMapping("/lua2") @ResponseBody public Map<String, Object> testLua2(String key1, String key2, String value1, String value2) { // 定義Lua腳本 String lua = "redis.call('set', KEYS[1], ARGV[1]) \n" + "redis.call('set', KEYS[2], ARGV[2]) \n" + "local str1 = redis.call('get', KEYS[1]) \n" + "local str2 = redis.call('get', KEYS[2]) \n" + "if str1 == str2 then \n" + "return 1 \n" + "end \n" + "return 0 \n"; System.out.println(lua); // 結果返回爲Long DefaultRedisScript<Long> rs = new DefaultRedisScript<Long>(); rs.setScriptText(lua); rs.setResultType(Long.class); // 採用字符串序列化器 RedisSerializer<String> stringSerializer = redisTemplate.getStringSerializer(); // 定義key參數 List<String> keyList = new ArrayList<>(); keyList.add(key1); keyList.add(key2); // 傳遞兩個參數值,其中第一個序列化器是key的序列化器,第二個序列化器是參數的序列化器 Long result = (Long) redisTemplate.execute( rs, stringSerializer, stringSerializer, keyList, value1, value2); Map<String, Object> map = new HashMap<String, Object>(); map.put("result", result); return map; }
Redis在web開發中最重要的做用大概就是用來做爲緩存存儲數據,加快查詢速度。
首先緩存處理器 CacheManager有不少的實現類,它並非爲Redis特別定製的。可是因爲咱們使用Redis,因此天然咱們的緩存就會選擇RedisCacheManager這個實現類。
在Spring Boot中有如下配置項能夠用於CacheManager配置
# SPRING CACHE (CacheProperties) spring.cache.cache-names= # 若是由底層的緩存管理器支持建立,以逗號分隔的列表來緩存名稱 spring.cache.caffeine.spec= # caffeine 緩存配置細節 spring.cache.couchbase.expiration=0ms # couchbase 緩存超時時間,默認是永不超時 spring.cache.ehcache.config= # 配置 ehcache 緩存初始化文件路徑 spring.cache.infinispan.config= #infinispan 緩存配置文件 spring.cache.jcache.config= #jcache 緩存配置文件 spring.cache.jcache.provider= #jcache 緩存提供者配置 spring.cache.redis.cache-null-values=true # 是否容許 Redis 緩存空值 spring.cache.redis.key-prefix= # Redis 的鍵前綴 spring.cache.redis.time-to-live=0ms # 緩存超時時間戳,配置爲0則不設置超時時間 spring.cache.redis.use-key-prefix=true # 是否啓用 Redis 的鍵前綴 spring.cache.type= # 緩存類型,在默認的狀況下,Spring 會自動根據上下文探測
就使用Redis來講,咱們只須要關注這些
spring.cache.cache-names= # 若是由底層的緩存管理器支持建立,以逗號分隔的列表來緩存名稱 spring.cache.redis.cache-null-values=true # 是否容許 Redis 緩存空值 spring.cache.redis.key-prefix= # Redis 的鍵前綴 spring.cache.redis.time-to-live=0ms # 緩存超時時間戳,配置爲0則不設置超時時間 spring.cache.redis.use-key-prefix=true # 是否啓用 Redis 的鍵前綴 spring.cache.type= # 緩存類型,在默認的狀況下,Spring 會自動根據上下文探測
對於剛開始使用,咱們先簡單配置下,好比
spring.cache.type=REDIS
spring.cache.cache-names=redisCache
這裏的 spring.cache.type 配置的是緩存類型,爲 Redis,Spring Boot 會自動生成 RedisCacheManager 對象,而 spring.cache.cache-names 則是配置緩存名稱,多個名稱可使用逗號分隔,以便於緩存註解的引用。
另外爲了啓用緩存管理器,須要在XXXApplication中,須要添加@EnableCaching註解
咱們使用mybatis章節中使用過的demo進行,擴展咱們的 MyBatisUserDao
@Repository public interface MyBatisUserDao { // 獲取單個用戶 User getUser(Long id); // 保存用戶 int insertUser(User user); // 修改用戶 int updateUser(User user); // 查詢用戶,指定MyBatis的參數名稱 List<User> findUsers(@Param("userName") String userName, @Param("note") String note); // 刪除用戶 int deleteUser(Long id); }
而後須要在userMapper.xml中註冊相關操做接口
<select id="getUser" parameterType="long" resultType="user"> select id, user_name as userName, sex, note from t_user where id = #{id} </select> <insert id="insertUser" useGeneratedKeys="true" keyProperty="id" parameterType="user"> insert into t_user(user_name, note,sex) values(#{userName}, #{note},#{sex}) </insert> <update id="updateUser"> update t_user <set> <if test="userName != null">user_name =#{userName},</if> <if test="note != null">note =#{note}</if> </set> where id = #{id} </update> <select id="findUsers" resultType="user"> select id, user_name as userName, note from t_user <where> <if test="userName != null"> and user_name = #{userName} </if> <if test="note != null"> and note = #{note} </if> </where> </select> <delete id="deleteUser" parameterType="long"> delete from t_user where id = #{id} </delete>
經過將屬性 useGeneratedKeys 設置爲 true,表明將經過數據庫生成主鍵,而將 keyProperty 設置爲 POJO 的 id 屬性,MyBatis 就會將數據庫生成的主鍵回填到 POJO 的 id 屬性中。
再而後修改咱們的MyBatisService接口和實現
@Service public class MyBatisUserServiceImpl implements MyBatisUserService { @Autowired private MyBatisUserDao myBatisUserDao = null; @Override @Transactional public User getUser(Long id) { return myBatisUserDao.getUser(id); } @Override @Transactional public User insertUser(User user) { myBatisUserDao.insertUser(user); return user; } @Override @Transactional public User updateUserName(Long id, String userName) { // 此處調用 getUser 方法,該方法緩存註解失效, // 因此這裏還會執行 SQL,將查詢到數據庫最新數據 User user =this.getUser(id); if (user == null) { return null; } user.setUserName(userName); myBatisUserDao.updateUser(user); return user; } @Override @Transactional public List<User> findUsers(String userName, String note) { return myBatisUserDao.findUsers(userName, note); } @Override @Transactional public int deleteUser(Long id) { return myBatisUserDao.deleteUser(id); } }
最後修改MyBatisController
@Controller @RequestMapping("/mybatis") public class MyBatisController { @Autowired private MyBatisUserService myBatisUserService = null; @RequestMapping("/getUser") @ResponseBody public User getUser(Long id) { return myBatisUserService.getUser(id); } @RequestMapping("/insertUser") @ResponseBody public User insertUser(String userName, String note) { User user = new User(); user.setUserName(userName); user.setNote(note); user.setSex(SexEnum.FEMALE); myBatisUserService.insertUser(user); return user; } @RequestMapping("/findUsers") @ResponseBody public List<User> findUsers(String userName, String note) { return myBatisUserService.findUsers(userName, note); } @RequestMapping("/updateUserName") @ResponseBody public Map<String, Object> updateUserName(Long id, String userName) { User user = myBatisUserService.updateUserName(id, userName); boolean flag = user != null; String message = flag? "更新成功" : "更新失敗"; return resultMap(flag, message); } @RequestMapping("/deleteUser") @ResponseBody public Map<String, Object> deleteUser(Long id) { int result = myBatisUserService.deleteUser(id); boolean flag = result == 1; String message = flag? "刪除成功" : "刪除失敗"; return resultMap(flag, message); } private Map<String, Object> resultMap(boolean success, String message) { Map<String, Object> result = new HashMap<String, Object>(); result.put("success", success); result.put("message", message); return result; } }
準備工做完成,接下去就開始添加咱們的緩存。首先引入依賴,添加application.properties配置(上面有列出,這裏不細說了)。
而後再Application中添加@EnableCaching
修改MyBatisUserServiceImpl的insert方法
// 插入用戶,最後 MyBatis 會回填 id,取結果 id 緩存用戶 @Override @Transactional @CachePut(value ="redisCache", key = "'redis_user_'+#result.id") public User insertUser(User user) { userDao.insertUser(user); return user; }
@CachePut表示將方法結果返回存放到緩存中。 value表示要存入的緩存名,着咱們在application.properties中配置了。key固然表示建值,其中的寫法是Spring EL中定義的寫法,好比#result表示返回值的id字段。
而後修改getUser方法
@RequestMapping("/getUser") @ResponseBody @Cacheable(value ="redisCache", key = "'redis_user_'+#id") public User getUser(Long id) { return myBatisUserService.getUser(id); }
@Cacheable 表示先從緩存中經過定義的鍵查詢,若是能夠查詢到數據,則返回,不然執行該方法,返回數據,而且將返回結果保存到緩存中。
PS:這裏可能會遇到錯誤 DefaultSerializer requires a Serializable payload but received an object of type 緣由在於咱們的User類沒法被序列化,因此User類須要繼承 Serializable 接口
修改deleteUser
@Override @Transactional @CacheEvict(value ="redisCache", key = "'redis_user_'+#id", beforeInvocation = false) public int deleteUser(Long id) { return userDao.deleteUser(id); }
@CacheEvict 經過定義的鍵移除緩存,它有一個 Boolean 類型的配置項 beforeInvocation,表示在方法以前或者以後移除緩存。由於其默認值爲 false,因此默認爲方法以後將緩存移除。
在 updateUserName 方法裏面咱們先調用了 getUser 方法,由於是更新數據,因此須要慎重一些。通常咱們不要輕易地相信緩存,由於緩存存在髒讀的可能性,這是須要注意的,在須要更新數據時咱們每每考慮先從數據庫查詢出最新數據,然後再進行操做。所以,這裏使用了 getUser 方法。可是這裏有個無解,有人任務因爲getUser使用了@Cacheable註解,因此會先從緩存中讀取數據,這就致使了髒數據的可能。實際上這裏的@Cacheable是失效了的,由於 Spring 的緩存機制也是基於 Spring AOP 的原理,而在 Spring 中 AOP 是經過動態代理技術來實現的,這裏的 updateUserName 方法調用 getUser 方法是類內部的自調用,並不存在代理對象的調用,這樣便不會出現 AOP,也就不會使用到標註在 getUser 上的緩存註解去獲取緩存的值了,這是須要注意的地方。
PS 解決類內部自調用問題可使用雙服務互相調用的方法克服。
使用緩存可使得系統性能大幅度地提升,可是也引起了不少問題,其中最爲嚴重的問題就是髒數據問題,好比
時 刻 | 動 做 1 | 動 做 2 | 備 注 |
---|---|---|---|
T1 | 修改 id 爲1的用戶 | ||
T2 | 更新數據庫數據 | ||
T3 | 使用 key_1 爲鍵保存數據 | ||
T4 | 修改 id 爲1的用戶 | 與動做1操做同一數據 | |
T5 | 更新數據庫數據 | 此時修改數據庫數據 | |
T6 | 使用 key_2 爲鍵保存數據 | 這樣 key_1爲鍵的緩存就已是髒數據 |
對於數據的讀操做,通常而言是容許不是實時數據,如一些電商網站還存在一些排名榜單,而這個排名每每都不是實時的,它會存在延遲,其實對於查詢是能夠存在延遲的,也就是存在髒數據是容許的。可是若是一個髒數據始終存在就說不通了,這樣會形成數據失真比較嚴重。通常對於查詢而言,咱們能夠規定一個時間,讓緩存失效,在 Redis 中也能夠設置超時時間,當緩存超過超時時間後,則應用再也不可以從緩存中獲取數據,而只能從數據庫中從新獲取最新數據,以保證數據失真不至於太離譜。
咱們能夠經過設置屬性 spring.cache.redis.time-to-live=600000 來設置超時時間,好比這裏設置超時時間10分鐘。
對於數據的寫操做,每每採起的策略就徹底不同,須要咱們謹慎一些,通常會認爲緩存不可信,因此會考慮從數據庫中先讀取最新數據,而後再更新數據,以免將緩存的髒數據寫入數據庫中,致使出現業務問題。
有時候,在自定義時可能存在比較多的配置,也能夠不採用 Spring Boot 自動配置的緩存管理器,而是使用自定義的緩存管理器。
// 注入鏈接工廠,由Spring Boot自動配置生成 @Autowired private RedisConnectionFactory connectionFactory = null; // 自定義Redis緩存管理器 @Bean(name = "redisCacheManager" ) public RedisCacheManager initRedisCacheManager() { // Redis加鎖的寫入器 RedisCacheWriter writer= RedisCacheWriter.lockingRedisCacheWriter(connectionFactory); // 啓動Redis緩存的默認設置 RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig(); // 設置JDK序列化器 config = config.serializeValuesWith( SerializationPair.fromSerializer(new JdkSerializationRedisSerializer())); // 禁用前綴 config = config.disableKeyPrefix(); //設置10 min超時 config = config.entryTtl(Duration.ofMinutes(10)); // 建立緩Redis存管理器 RedisCacheManager redisCacheManager = new RedisCacheManager(writer, config); return redisCacheManager; }
這裏首先注入了 RedisConnectionFactory 對象,該對象是由 Spring Boot 自動生成的。在建立 Redis 緩存管理器對象 RedisCacheManager 的時候,首先建立了帶鎖的 RedisCacheWriter 對象,而後使用 RedisCacheConfiguration 對其屬性進行配置,這裏設置了禁用前綴,而且超時時間爲 10 min;最後就經過 RedisCacheWriter 對象和 RedisCacheConfiguration 對象去構建 RedisCacheManager 對象了,這樣就完成了 Redis 緩存管理器的自定義。
就Reids,雖然上面貼的代碼不少,demo也比較依賴mybatis章節的原有demo,可是總得來講知識點仍是相對完整的。註解不是惟一的選擇,可是註解確實有不錯的收益。
Redis的註解也是經過AOP生效的。