如何假裝成一個服務端開發(八) -- Redis

目錄

如何假裝成一個服務端開發(一)html

如何假裝成一個服務端開發(二)java

如何假裝成一個服務端開發(三) c++

如何假裝成一個服務端開發(四)web

如何假裝成一個服務端開發(五)redis

如何假裝成一個服務端開發(六)spring

如何假裝成一個服務端開發(七)sql

 

前言

    若是你想在網上再找一個這麼詳細的入門 Spirng Boot + redis的項目,那你可得費點力氣了……由於我就嘗試過……數據庫

NoSQL

    咱們知道數據庫鏈接和調用是耗時的(包括鏈接,查詢等操做)。並且在高併發的狀況下會出現明顯的瓶頸。因此如何減小數據庫訪問就逐漸成爲互聯網系統加速的重要優化點。爲此NoSQL誕生了,其中使用普遍的就是Redis和MongoDB。這裏先介紹一下Redis。centos

    Redis 是一種運行在內存的數據庫,不少時候咱們都會把從數據庫查詢出來的數據放入Redis,當用戶再次查詢相同數據的時候,優先使用Redis中存在的數據,由於是存放在內存中,因此速度很快。另外Redis還能夠將數據持久化到磁盤中,不少網站甚至放棄了後臺數據庫,徹底使用Redis來進行數據存儲。緩存

安裝Redis

    筆者爲了學(fan)習(qiang),特意買了一個廉價的VPS,這裏正好利用起來,在服務器上安裝了mariadb 和 redis。這裏不詳細介紹安裝流程,你們能夠本機安裝,網上資料不少。

    PS 筆者使用的是centos7 對於安全的限制很嚴,安裝完成mariadb和redis以後,若是須要遠程訪問,須要開啓防火牆端口。

    PS2 這兩個東西須要遠程訪問都須要作一些設置,好比redis須要去掉bind 127.0.0.1的配置等。

 

Spring 中引入redis

<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,因此固然跟隨大衆科技了。

 

第一個入門demo

    老夫寫代碼就是一把梭!開個玩笑……先來一個列子,固然你大概會  「???」

    首先在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;
}

 

SessionCallback和RedisCallback 接口

    和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是同步執行。

 

Pipeline

    不管是使用事務,仍是使用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最遊手好閒的功能,由於無論在使用上仍是過程當中這都和數據沒有太大的直接聯繫。(頗有多是我沒有深刻學習原理)

    首先是 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);    

 

Lua腳本

    爲了加強 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;
}

 

在Spring中使用註解操做Redis

    Redis在web開發中最重要的做用大概就是用來做爲緩存存儲數據,加快查詢速度。

啓用緩存和CacheManager

    首先緩存處理器 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註解

Demo

    咱們使用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生效的。

相關文章
相關標籤/搜索