redis概念及註解

簡介
Redis 是一個開源的內存中的數據結構存儲系統,它可以用作數據庫、緩存和消息中間件。
我們平時在項目中設計數據訪問的時候往往都是採用直接訪問數據庫,採用數據庫連接池來實現,但是如果我們的項目訪問量過大或者訪問過於頻繁,將會對我們的數據庫帶來很大的壓力。爲了解決這個問題從而redis數據庫脫穎而出,redis數據庫出現時是以非關係數據庫的光環展示在廣大程序猿的面前的,後來redis的迭代版本支持了緩存數據、登錄session狀態(分佈式session共享)等。所以又被作爲內存緩存的形式應用到大型企業級項目中。

Spring從3.1開始定義了org.springframework.cache.Cache和org.springframework.cache.CacheManager接口來統一不同的緩存技術;並支持使用JCache(JSR-107)註解 簡化我們開發

幾個重要概念&緩存註解
Cache 緩存接口,定義緩存操作。實現有:RedisCache、EhCacheCac、ConcurrentMapCache等
CacheManager 緩存管理器,管理各種緩存(Cache)組件
keyGenerator 緩存數據時key生成策略
serialize 緩存數據時value序列化策略
@Cacheable 主要針對方法配置,能夠根據方法的請求參數對其結果進行緩存
@CacheEvict 清空緩存
@CachePut 保證方法被調用,又希望結果被緩存。
@EnableCaching 開啓基於註解的緩存

@Cacheable/@CachePut/@CacheEvict 主要的參數
value 緩存的名稱,在 spring 配置文件中定義,必須指定至少一個
例如:@Cacheable(value=”mycache”) 或者 @Cacheable(value={”cache1”,”cache2”}

key 緩存的 key,可以爲空,如果指定要按照 SpEL 表達式編寫,如果不指定,則缺省按方法的所有參數進行組合
例如:@Cacheable(value=”testcache”,key=”#userName”)

condition 緩存的條件,可以爲空,使用 SpEL 編寫,返回 true 或者 false,只有爲 true 才進行緩存/清除緩存,在調用方法之前之後都能判斷
例如:@Cacheable(value=”testcache”,condition=”#userName.length()>2”)

allEntries(@CacheEvict )是否清空所有緩存內容,缺省爲 false,如果指定爲true,則方法調用後將立即清空所有緩存
例如:@CachEvict(value=”testcache”,allEntries=true)

beforeInvocation(@CacheEvict)是否在方法執行前就清空,缺省爲 false,如果指定爲 true,則在方法還沒有執行的時候就清空緩存,缺省情況下,如果方法執行拋出異常,則不會清空緩存
例如:@CachEvict(value=”testcache”,beforeInvocation=true)

unless(@CachePut)(@Cacheable)用於否決緩存的,不像condition,該表達式只在方法執行之後判斷,此時可以拿到返回值result進行判斷。條件爲true不會緩存,fasle才緩存
例如:@Cacheable(value=”testcache”,unless=”#result == null”)

SpEL表達式

在這裏插入圖片描述
redis應用

登錄互踢: 不刷新token的情況下,每次請求比較前端傳來的token與redis裏所存的token,相同則表示是同一次登錄,不同則表示此次傳來的被後面登錄的踢了,雖然token未失效,不允許訪問接口,直接攔截,讓其重新登錄。
若每次攔截過濾的時候刷新token,監聽登錄,每次登錄會檢查當前用戶有沒有登陸過,如果登錄過T掉上個登錄用戶,刪除key,如果沒有則寫入redis

 @Override
    public void saveMobileUserLoginInfo(UserClientDetail userDetails) {
        JWTToken token = userDetails.getToken();
        String tokenid = tokenKey(token);
        logger.info("寫入用戶登錄信息: saveMobileUserLoginInfo" + tokenid);
        String redisKey = CommonConst.PROJECT_NAME + ":LOGINSUCCESS:" + userDetails.getToken().getEnterpCode() + ":" + userDetails.getUsername();
        //判斷是否已有用戶登錄
        String oldTokenId = stringRedisTemplate.opsForValue().get(redisKey);

        if (oldTokenId != null) {
            //T掉上個登錄用戶
            stringRedisTemplate.delete(oldTokenId);
            //記錄被T用戶
            stringRedisTemplate.opsForValue().set(getKickoutKey(token), "0",OVERDUE_TIMEOUT, TimeUnit.SECONDS);
        }
        //寫入當前登錄用戶
        stringRedisTemplate.opsForValue().set(redisKey, tokenid, propertyPlaceUtil.getMobileTokenExpireTime(), TimeUnit.SECONDS);

    }

分佈式鎖:
基於 REDIS 的 SETNX()、GET()、GETSET()方法做分佈式鎖

SETNX()

/**
	 * SETNX 的含義就是 SET if Not Exists,其主要有兩個參數 setnx(key, value)。該方法是原子的,如果 key 不存在,
	 * 則設置當前 key 成功,返回 true;如果當前 key 已經存在,則設置當前 key 失敗,返回 false。
	 * @param key
	 * @param value
	 * @return
	 */

	public static Boolean setNx(final String key, final String value) {
		Boolean b = false;
		try {
			b = redisUtil.redisTemplate.execute((final RedisConnection c) -> {
				final StringRedisSerializer serializer = new StringRedisSerializer();
				final Boolean success = c.setNX(serializer.serialize(key), serializer.serialize(value));
				c.close();
				return success;
			});
		} catch (Exception e) {
			log.error("setNX redis error, key : {}", key);
		}
		return b;
	}

GETSET()

	/**
	 * 這個命令主要有兩個參數 getset(key,newValue)。該方法是原子的,對 key 設置 newValue 這個值,
	 * 並且返回 key 原來的舊值。假設 key 原來是不存在的,那麼多次執行這個命令,會出現下邊的效果:
	 * getset(key, “value1”) 返回 null 此時 key 的值會被設置爲 value1
	 * getset(key, “value2”) 返回 value1 此時 key 的值會被設置爲 value2
	 * 依次類推!
	 * @param key在這裏插入代碼片
	 * @param value
	 * @return
	 */
	public static String getSet(final String key, final String value) {
		String obj = null;
		try {
			obj =redisUtil.redisTemplate.execute((final RedisConnection c) -> {
				final StringRedisSerializer serializer = new StringRedisSerializer();
				final byte[] ret = c.getSet(serializer.serialize(key), serializer.serialize(value));
				c.close();
				return serializer.deserialize(ret);
			});
		} catch (Exception ex) {
			log.error("setNX redis error, key : {}", key);
		}
		return obj;
	}

加鎖方法

 //加鎖
    public static boolean lock(String key, int expire) {

        RedisUtil redisUtil = SpringUtil.getBean(RedisUtil.class);

        long value = System.currentTimeMillis() + expire;

        //獲得鎖
        if(redisUtil.setNx(key, String.valueOf(value))) {
            return true;
        }

        //未獲得鎖則判斷是否超時,如果此時key被刪了,則返回0
        long oldExpireTime = Long.parseLong(redisUtil.get(key,"0"));
        if(oldExpireTime < System.currentTimeMillis()) {//超時或key被釋放
        
            long newExpireTime = System.currentTimeMillis() + expire;
            //設置新值並返回舊值
            String val=redisUtil.getSet(key, String.valueOf(newExpireTime));
            if(val==null){
                val="0";
            }
            System.out.println(Thread.currentThread().getName()+"-"+oldExpireTime+"-"+val);
            long currentExpireTime = Long.parseLong(val);
            //如果返回的currentExpireTime和舊值相等說明是這次設置的
            if(currentExpireTime == oldExpireTime) {
                return true;
            }
        }
        return false;
    }

釋放鎖

 //釋放鎖
    public static void unLock(String key) {
        RedisUtil redisUtil = SpringUtil.getBean(RedisUtil.class);
        long oldExpireTime = Long.parseLong(redisUtil.get(key).toString());
        if(oldExpireTime > System.currentTimeMillis()) {
            redisUtil.del(key);
        }
    }

注:此方案存在瑕疵

  • 由於是客戶端自己生成過期時間,所以需要強制要求分佈式下每個客戶端的時間必須同步。
  • 當鎖過期的時候,如果多個客戶端同時執行jedis.getSet()方法,那麼雖然最終只有一個客戶端可以加鎖,但是這個客戶端的鎖的過期時間可能被其他客戶端覆蓋。
  • 鎖不具備擁有者標識,即任何客戶端都可以解鎖。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章