1. 程式人生 > >【redis】使用redisTemplate優雅地操作redis及使用redis實現分散式鎖

【redis】使用redisTemplate優雅地操作redis及使用redis實現分散式鎖

前言:

上篇已經介紹了redis及如何安裝和叢集redis,這篇介紹如何通過工具優雅地操作redis.

Long Long ago,程式猿們還在通過jedis來操作著redis,那時候的猿類,一個個累的沒日沒夜,重複的造著輪子,忙得沒時間陪家人,終於有一天猿類的春天來了,spring家族的redis template 解放了程式猿的雙手,於是猿類從使用Jedis石器時代的進入自動化時代...

redis template是對jedis的高度封裝,讓java對redis的操作更加簡單,甚至連小學生都可以駕馭...

在正式進入學習前,先給大家介紹一款Redis視覺化工具,個人感覺比

Redis Desktop Manager這類工具好用很多,而且是國產的,如果公司有伺服器的話,可以部署上去,然後今後大家都可以直接去使用,比較方便.

傳送門:http://www.treesoft.cn/dms.html

亦可百度搜treesoft,我不是託...


在正式學習之前,我們再來回顧一下Redis的支援儲存的五大資料型別:

分別為String(字串)、List(列表)、Set(集合)、Hash(雜湊)和 Zset(有序集合)

RedisTemplate中封裝了對5種資料結構的操作:

redisTemplate.opsForValue();//操作字串
redisTemplate.opsForHash();//操作hash
redisTemplate.opsForList();//操作list
redisTemplate.opsForSet();//操作set
redisTemplate.opsForZSet();//操作有序set

StringRedisTemplate與RedisTemplate

  • 兩者的關係是StringRedisTemplate繼承RedisTemplate。

  • 兩者的資料是不共通的;也就是說StringRedisTemplate只能管理StringRedisTemplate裡面的資料,RedisTemplate只能管理RedisTemplate中的資料。

  • SDR預設採用的序列化策略有兩種,一種是String的序列化策略,一種是JDK的序列化策略。

    StringRedisTemplate預設採用的是String的序列化策略,儲存的key和value都是採用此策略序列化儲存的。

    RedisTemplate預設採用的是JDK的序列化策略,儲存的key和value都是採用此策略序列化儲存的。

     以上兩種方式,根據實際業務需求靈活去選擇,操作字串型別用StringRedis Template,操作其它資料型別用Redis Template.


Redis Template的使用分為三步:引依賴,配置,使用...

第一步:引入依賴

     <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
     </dependency>

第二步:配置Redis Template(redisTemplate或StringRedisTemlate根據業務任選一種)

/**
 * redis配置類
 **/
@Configuration
@EnableCaching//開啟註解
public class RedisConfig {
     //以下兩種redisTemplate自由根據場景選擇,優先推薦使用StringRedisTemplate
    /**redisTemplate方式*/
    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);

        //使用Jackson2JsonRedisSerializer來序列化和反序列化redis的value值(預設使用JDK的序列化方式)
        Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Object.class);

        ObjectMapper mapper = new ObjectMapper();
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        serializer.setObjectMapper(mapper);

        template.setValueSerializer(serializer);
        //使用StringRedisSerializer來序列化和反序列化redis的key值
        template.setKeySerializer(new StringRedisSerializer());
        template.afterPropertiesSet();
        return template;
    }
      /**StringRedisTemplate方式*/
//    @Bean
//    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory) {
//        StringRedisTemplate stringRedisTemplate = new StringRedisTemplate();
//        stringRedisTemplate.setConnectionFactory(factory);
//        return stringRedisTemplate;
//    }

}

配置application.yml:

spring:
  redis:
    host: 192.168.1.1
    password: 123456 # 沒密碼的話不用配
    port: 6379
    database: 10 #我這裡因為從視覺化工具裡發現10這個庫比較空,為了方便演示,所以配了10.

第三步:使用

為了今後使用方便,其實你可以封裝一個RedisService,其功能有點類似JPA或者MyBatis這種,把需要對redis的存取操作封裝進去,當然這一步只是建議,封不封由你...

由於之前配置了redisTemplate及其子類,故需要使用@Resource註解進行呼叫.

@Resource
private RedisTemplate<String, Object> redisTemplate;//型別可根據實際情況走

然後就可以根據redisTemplate進行各種資料操作了:

使用:redisTemplate.opsForValue().set("name","tom");
結果:redisTemplate.opsForValue().get("name")  輸出結果為tom

更多的我就不演示了,只要你對redis的5大資料型別的基本操作掌握即可輕鬆使用,,比較簡單,沒啥意思,如果感興趣可以參考這篇部落格,寫得十分詳細:

https://blog.csdn.net/ruby_one/article/details/79141940

下面我主要說一下前面提到的封裝RedisService,二話不說我先上程式碼為敬:

先寫介面RedisService:

/**Redis存取操作*/
public interface RedisService {
    void set(String key,Object value);//無過期時間
    void set(String key,Object value,Long timeOutSec);//帶過期時間,單位是秒,可以配.
    Object get(String key);
}

再寫實現類:
 

@Service
public class RedisServiceImpl implements RedisService {
    
    @Resource
    RedisTemplate<String, Object> redisTemplate;

    @Override
    public void set(String key, Object value) {
        ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();
        valueOperations.set(key, value);
    }

    @Override
    public void set(String key, Object value, Long timeOutSec) {
        ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();
        valueOperations.set(key, value, timeOutSec, TimeUnit.SECONDS);
    }

    @Override
    public Object get(String key) {
        ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();
        return valueOperations.get(key);
    }
}

呼叫:

隨便寫了兩個頁面,第一個頁面表單傳Key過來,第二個頁面對Key的value進行封裝並存入redis,再取出來作展現:

    @RequestMapping("getValue")
    public ModelAndView getValue(@RequestParam("key") String key, ModelAndView modelAndView) {
        modelAndView.addObject("key", key);
        User user = new User("老漢",18);
        redisService.set(key,user,10L);
        Object value = redisService.get(key);
        modelAndView.addObject("value",value);
        modelAndView.setViewName(PREFIX + "hello.html");
        return modelAndView;
    }
}

效果:

然後我們進入TreeSoft來看一下redis中的資料是否有存進來:

可以看到,沒有問題,資料已經進來,10秒後再次重新整理頁面,資料已經過期,從redis資料庫中正常消失,完全符合預期.

前面提到了redisTemplate和StringRedisTemplate,下面我們看看他們除了我前面提到的那些差別,還有哪些地方不一樣:

重啟專案後,同樣的資料,看下效果:

結果未變,但redis中的資料變成了這樣...檢視不了,刪除不了,修改不了,因為亂碼了...看上去這種序列化方式似乎更加安全,但事實上,只是因為這款工具不支援顯示這樣的序列化方式編碼,換一個視覺化工具結果就不一樣了,所以不要被表面現象矇蔽了,要多文件及原始碼,兩者真正的差別是在操作資料型別上,StringRedisTemplate只適合操作String型別的,其他型別一律用RedisTemplate.

關於redis Template已是高度封裝了,對各種資料型別的操作都比較簡單,其他資料型別的操作我就不一一演示了,其實自從有了json,StringRedis Template 也可以用來儲存其他資料型別了,萬物皆字串,管你是什麼型別,都可以用Json字串來表示,所以大家重點掌握String型別的資料存取即可.


分散式鎖:

在單體應用架構中,遇到併發安全性問題時我們可以通過同步鎖Synchronized,同步程式碼塊,ReentrantLock等方式都可以解決,但在分散式系統中,JDK提供的這些併發鎖都失效了,我們需要一把"全域性的鎖",所有的分散式系統共享這把鎖,這把鎖同一時間內只能被一個系統擁有,擁有鎖的系統獲得一些相應的許可權,其它系統需要等待擁有鎖的系統釋放鎖,然後去競爭這把鎖,只有擁有這把鎖的系統才具有相應許可權.

分散式鎖目前比較常見的有3種實現方式,一種是基於Redis實現的,一種是基於zookeeper實現的,還有一種是基於資料庫層面的樂觀鎖和悲觀鎖.

本篇只介紹基於Redis的實現方式,其它兩種請翻閱本博,均有介紹和實現.

學之前先來了解一個將會用到的Redis命令

setNX(set if not exist):意思是如果不存在才會設定值,否則啥也不做,如果不存在,設定成功後返回值為1,失敗則返回0;

下面說一下實現原理:

1.所有系統在接收到請求後都去建立一把鎖,這把鎖的key均相同,但只有一個系統能最終建立成功,其他系統建立失敗.
2.建立鎖成功的系統繼續進行後續操作,比如下單,儲存資料至資料庫...未獲得鎖的系統等待,直到該系統操作完成後把鎖釋放,繼續開始競爭該鎖.
為了確保分散式鎖可用,我們至少要確保鎖的實現同時滿足以下四個條件:

1.互斥性。在任意時刻,只有一個客戶端能持有鎖。
2.不會發生死鎖。即使有一個客戶端在持有鎖的期間崩潰而沒有主動解鎖,也能保證後續其他客戶端能加鎖。
3.具有容錯性。只要大部分的Redis節點正常執行,客戶端就可以加鎖和解鎖。
4.解鈴還須繫鈴人。加鎖和解鎖必須是同一個客戶端,客戶端自己不能把別人加的鎖給解了

邏輯比較簡單,我直接上程式碼:

/**
*初始化Jedis連線池
*/
public class JedisPoolConfig {
    private static JedisPool pool = null;

    /**
     * 靜態程式碼塊 構建redis連線池
     */
    static {
        if (pool == null) {
            redis.clients.jedis.JedisPoolConfig config = new redis.clients.jedis.JedisPoolConfig();
            //控制一個pool可分配多少個jedis例項,通過pool.getResource()來獲取;
            //如果賦值為-1,則表示不限制;如果pool已經分配了maxActive個jedis例項,則此時pool的狀態為exhausted(耗盡)。
            config.setMaxTotal(50);
            //控制一個pool最多有多少個狀態為idle(空閒的)的jedis例項。
            config.setMaxIdle(10);
            //表示當borrow(引入)一個jedis例項時,最大的等待時間,如果超過等待時間,則直接丟擲JedisConnectionException;單位毫秒
            //小於零:阻塞不確定的時間,  預設-1
            config.setMaxWaitMillis(1000 * 100);
            //在borrow(引入)一個jedis例項時,是否提前進行validate操作;如果為true,則得到的jedis例項均是可用的;
            config.setTestOnBorrow(true);
            //return 一個jedis例項給pool時,是否檢查連線可用性(ping())
            config.setTestOnReturn(true);
            //connectionTimeout 連線超時(預設2000ms)
            //soTimeout 響應超時(預設2000ms)
            pool = new JedisPool(config, "192.168.1.1", 6379, 10000);
        }
    }

    /**
     * 方法描述 獲取Jedis例項
     *
     * @return
     */
    public static Jedis getJedis() {
        return pool.getResource();
    }

    /**
     * 方法描述 釋放jedis連線資源
     *
     * @param jedis
     */
    public static void returnResource(Jedis jedis) {
        if (jedis != null) {
            jedis.close();
        }
    }

}
public class DistributeLock {

    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";
    private static final Long RELEASE_SUCCESS = 1L;

    /**
     * 嘗試獲取分散式鎖
     * @param jedis Redis客戶端
     * @param lockKey 鎖
     * @param requestId 請求標識
     * @param expireTime 超期時間
     * @return 是否獲取成功
     */
    public static boolean acquire(Jedis jedis, String lockKey, String requestId, int expireTime) {

        String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);

        if (LOCK_SUCCESS.equals(result)) {
            return true;
        }
        return false;

    }

    /**
     * 釋放分散式鎖
     * @param jedis Redis客戶端
     * @param lockKey 鎖
     * @param requestId 請求標識
     * @return 是否釋放成功
     */
    public static boolean release(Jedis jedis, String lockKey, String requestId) {

        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));

        if (RELEASE_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }

}

在鎖的建立中,建立和設定過期時間必須保持原子性操作,否則萬一伺服器在建立鎖時宕機了,該節點變為永久節點,會造成死鎖.

在鎖的釋放中,判斷當前鎖是否有效和刪除該鎖也必須保持原子性操作,否則萬一伺服器在判斷鎖是否有效後發生GC或者其它卡頓,可能會造成誤刪,所以這裡用了Lua指令碼去執行,確保原子性.

另外上面有提到解鈴還須繫鈴人,故需要一個requestId來區分不同的請求.

原本想用redisTemplate來實現的,事實上我也確實用redisTemplate寫了一個,但因為自己不會寫lua指令碼,在鎖的釋放這裡不能做到原子性操作,所以借鑑了別人用Jedis方式的實現.

參考資料:https://www.cnblogs.com/linjiqin/p/8003838.html

https://blog.csdn.net/g6U8W7p06dCO99fQ3/article/details/81073892