JAVA關聯學習(一)

    本着關聯學習(問題關聯什麼學習什麼)的原則,這一篇講的是Redis。看了諸位大神的解釋後詳細的查了一些東西,記錄下來,也感謝各位在網絡上的分享!!!

    博客分享:

    https://www.jianshu.com/p/65765dd10671

    https://blog.csdn.net/u011692780/article/details/81213010

    1.什麼是Redis

    Redis是一個完全開源的遵守BSD協議的高性能(NoSQL)的key-value型數據庫。它使用C語言編寫(所以也不需要其他編譯器),是基於內存進行數據存儲的一種數據庫。在處理極大量的數據時,需要對數據庫進行大量數據的讀寫操作,或者通過併發對數據庫進行訪問請求時,可以通過使用Redis來緩解數據庫壓力,避免服務器宕機的情況出現。Redis在使用中多用於使用緩存來快速提取數據,提供高速的讀寫能力等應用場景。

    2.什麼是NoSQL數據庫

    NoSQL數據庫的出現是爲了解決關係型數據庫無法滿足對於海量大數據的數據管理要求,並且無法滿足數據的高併發高可用等特需求。我們平日中所接觸的網絡應用都是基於數據庫的增刪改查的過程,單體對於數據的訪問次數在面對極大數據量的用戶時就會成比例增長,那麼對於常見的關係型數據庫的訪問壓力就會非常大。並且NoSQL拋棄了關係型數據庫的關係型特性。數據之間的關聯關係的減少增強了數據的可拓展性。NoSQL數據庫大體可以分爲四類:

    (1).鍵值型數據庫:例如Redis,使用哈希表,表中有一個特定的鍵和一個指針指向特定的數據,key-value鍵值對形式。通常使用內存進行緩存,並且具有哈希的查找速度快的特性。數據沒有特定的結構,一般用來存儲各種格式的字符串或二進制數據。

    (2).列存儲數據庫:例如HBase,通常用來應對分佈式存儲的海量數據,鍵仍然存在,但是指向了多個列,列由列簇進行管理。查找速度快,並且更容易進行分佈式拓展。

    (3).文檔型數據庫:例如MangoDB,依然使用key-value比鍵值對形式,但是value爲結構化的數據,如JSON格式的數據。文檔型數據庫可以看做是鍵值型數據庫的升級,所以數據庫查詢效率較之鍵值型數據庫也更高。

     (4).圖形型數據庫:例如Neo4J,使用圖形模型來管理數據,利用圖結構的關聯算法進行數據查詢等,但是對於大量數據的查詢可能需要對整個圖進行計算才能得出信息。

    NoSQL適用於:

    NoSQL的適用場景一般具有對數據庫性能要求高,數據可以簡單且靈活使用的特點,對於給定的key可以接受不同類型的複雜之的環境。

    3.爲什麼使用Redis

    (1).Redis通過RDB或AOF來支持數據持久化,可以將數據存儲在內存中做緩存,也可以將內存中的數據存儲在硬盤中,以便在重啓服務器時可以再次加載使用。有很好的分佈式集羣方案和支持,也可以做消息隊列。

    (2).Redis不僅僅支持簡單的key-value類型數據,還提供list,set,zset,hash等數據結構存儲。在數據存儲類型上根據不同的應用場景可選擇的類型豐富。

    (3).Redis能滿足大數據量的性能問題,也能保證高併發情況下緩解數據庫壓力。

    (4).Redis是單線程的,能保證操作的原子性。

    4.Redis的缺點:

    由於Redis的數據是直接存儲在內存中,所以需要定時快照(SnapShot)或者語句追加(AOF)來保證數據持久化。消耗內存,佔用內存高。Redis由於是基於內存進行的存儲,所以在操作上會更加關注內存大小,並且長時間的操作會大量消耗內存空間,所以需要定時的刪除。

    Redis由於數據都存儲在內存中,可以通過修改配置文件中的save配置項,該配置項規定Redis在多長時間內有多少次更新操作的情況下,生成RDB文件。以此來通過配置方式來避免斷電等應急情況發生,另外還應該即時整理內存,否則會一直增加使得內存佔滿。Redis服務器針對過期鍵的刪除策略主要有惰性刪除和定期刪除兩種策略配合管理內存空間。惰性刪除策略的出現是因爲,在Redis數據庫中每個鍵都有其對應的過期時間,若在創建時設定了創建時間,則可以通過對過期時間的檢查來判斷其是否過期,若過期則刪除。定期刪除策略則在數據庫中的過期key中隨機檢查一部分key的過期時間,並刪除其中的過期鍵。所以在使用時需要:

    1.設置超時時間(設定最大內存空間,建議不要超過1G,如maxmemory 1024MB)

    2.採用LRU(Least recently used,最近就少使用)算法動態刪除key

    volatile-lru:在設定過期時間的key中,刪除最近最少使用的key。

    allkeys-lru:查詢所有的key,並刪除最近最少使用的key,應用最廣泛策略。

    volatile-random:在設定了過期時間的key中,隨機刪除某個key。

    allkeys-random:查詢所有的key,隨機刪除。

    volatile-ttl:查詢全部設定過期時間的key,之後排序,將最早要過期的key優先刪除。

    Noeviction:如果設定該屬性,則不會淘汰,在內存使用到閾值時,所有申請內存空間的命令都會報錯返回。

    LFU(Least frequently used,最不常使用)算法動態刪除key

    volatile-lfu:從所有配置了過期時間的鍵中驅逐使用頻率最少的鍵。

    allkeys-lfu:從所有鍵中驅逐使用頻率最少的鍵。

    5.Redis的常用通用命令:

    TYPE KEY:返回key所存儲的值的類型。

    DEL KEY:刪除已存在的key,若該key不存在則不進行操作。

    DUMP KEY:返回給定key的值的序列化,若該key不存在,則返回nil。

    EXPIRE KEY SECOND:爲給定的key設置過期時間(若不設置,則默認永久保存),單位爲秒(PEXPIRE KEY MILLISECOND:具有相同的功能,但是以毫秒爲單位)。可以用在有限時操作的應用場景中,如限時優惠等。

    EXISTS KEY:用於檢查給定的key是否存在。若key存在則返回1,否則返回0。

    TTL KEY:以秒爲單位,返回給定key的剩餘生命時間,若key存在但沒設置過期時間,則返回-1。若key不存在,則返回-2。(PTTL KEY:具有相同的功能,但是以毫秒爲單位)。

    PERSIST KEY:移除給定的key的過期時間,key將永久保持。

    KEYS PATTERN:尋找所有符合給定模式的key。pattern處可以使用通配符。

    RENAME OLD_KEY_NAME NEW_KEY_NAME:將給定key的名字進行修改。原有key被修改後,相關的過期時間會轉移到新key上。

    在Redis中數據庫並不具體包含一個名稱,而是通過整數索引來進行標識。客戶端默認連接到數據庫0,也可以通過修改配置文件中的(database 16)對數據庫數量進行控制,可見默認數據庫數量爲16個,標識爲0~15。

    SELECT 數據庫:切換數據庫。

    MOVE KEY 數據庫:將當前數據庫中的指定KEY移動到指定數據庫下。

    FLUSHDB:清除當前數據庫下所有KEY。

    FLUSHALL:清除整個REDIS數據庫所有KEY。

    6.Redis的數據類型及支持的命令:

    在Redis底層使用一個RedisObject對象來存儲所有的key和value,該對象是在使用Redis進行數據存儲時實際在內存中保存的對象。RedisObject的關鍵字段有數據類型(String,Hash,List,Set,Zset),編碼方式(RAW,INT,HT,ZIPMAP,LINKEDLIST,ZIPLIST,INTSET,SKIPLIST,EMBSTER,QUICKLIST,STREAM),數據指針,虛擬內存等,下面按順序學習一下Redis支持五種數據類型:String(字符串),Hash(哈希),List(列表),Set(集合),Zset(有序集合)。

    1.String:

    String類型是二進制安全的(在數據傳輸時,保證信息安全,不被篡改破譯,若被攻擊,能夠被即時檢測,特點是編碼解碼都發生在客戶端完成,執行效率高,不需要頻繁解碼,不會出現亂碼),也就是說可以存儲任何數據,如jpg圖片或者序列化對象。也可以爲了提高網站運行速度,將一些靜態文件以String類型緩存在Redis中以供使用。String類型的value值最大可以容納的數據長度是512MB。

    String命令:

    SET KEY_NAME VALUE:無關數據類型,並且key重複新值會覆蓋舊值

    SETNX KEY VALUE(解決分佈式鎖的方案1):只有在key不存在時才能設置key的值,若存在則不作操作。

    GET KEY_NAME:取值。若key不存在,則返回nil。如果key存儲的不是string類型,返回一個錯誤。

    GERANGE KEY START END:根據start和end來取得key值中的value的對應部分。

    GETSET KEY VALUE:若key存在則返回對應的值,若key不存在則返回nil並set一個key值爲value,下次使用時便存在

    INCR KEY_NAME:將key中存儲的數字值增加1。若key不存在,則會先被初始化爲0,而後再執行incr操作。(incrby key_name能夠自己設定步長)

    DECR KEY_NAME:將key中存儲的數字值減少1。(decrby key_name能夠自己設定步長)

    APPEND KEY_NAME VALUE:用於爲指定的key追加字符串到末尾,若該key不存在則爲其賦值。

    String應用場景:

    1.用來存儲單個字符串或JSON字符串數據。在底層爲關係型數據庫作爲存儲層時,使用Redis作爲緩存,進行數據預熱後大量的數據便可以通過Redis快速的獲得響應。可以通過降低數據庫層面的讀寫操作次數以降低數據庫壓力。

    2.二進制安全,可以用來存放圖片文件內容,方便Web應用快速調用。

    3.計數器:常用key-value緩存應用,記錄如微博數粉絲數等會及時更改的數據,統計計數,等待時機一起更新到數據庫中。使用incr,decr等操作方便數值增減。當使用incr,decr等操作時,RedisObject對象引用的字符串就會轉爲數值型進行計算,並且此時字段編碼爲int。且incr,decr等指令本身就具有原子性,不需要考慮線程安全。

    4.session共享:通過設置key及key的過期時間來存儲多應用的共享session。每次獲取便可以直接從Redis緩存中直接獲取。

    2.Hash:

    Hash類似於JAVA BEAN,是一個String類型的field和value的映射表,特別適合用於存儲對象。每一個hash中可以存儲2^32-1個鍵值對,可以看成是key和value的map容器,非常適合於存儲值對象信息,該類型數據僅佔用很少的磁盤空間(相比於JSON)。存儲類型格式如Users(id,name,age,remark)

    Hash命令:

    HSET KEY FIELD VALUE:將哈希表key中的域field值設置爲value。HSET存儲操作 KEY對象名 FIELD屬性名 VALUE值。(如:hset user username "Mike")如果key不存在,則一個新的哈希表會被創建並進行該操作,若該field已經存在,則舊值會被覆蓋,雖然返回值是0,但是值已經被覆蓋。

    HMSET KEY FIELD VALUE [FIELD1,VALUE1]:同時將多個field-value(域-值)對設置在哈希表key中,屬性和值每組之間用空格間隔。

    HGET KEY FIELD:返回哈希表key中給定域field的值,當給定值不存在或key不存在時返回nil。

    HMGET KEY FIELD:返回哈希表key中所有給定的域的對應值,若給定的域不存在則會返回nil。

127.0.0.1:6379> hmget user username age
1) "Vic"
2) (nil)

    HGETALL KEY:以列表形式返回哈希表中所有的域field和值value。若哈希表key不存在則返回空列表。

127.0.0.1:6379> hgetall user1
(empty list or set)

    HLEN KEY:返回哈希表key中域數量。當哈希表key不存在時返回0。

    HDEL KEY FIELD:刪除哈希表key指定的一個或多個域值,返回被成功移出的域的數量。也可使用公共命令del直接刪除整個哈希表key。

    HSETNX KEY FIELD VALUE:當且僅當域field不存在時,纔將哈希表key中的域field值設置爲value,若域field已經存在則該操作無效。

    HINCRBY KEY FIELD INCREMENT:爲哈希表key中指定域field的value值加上步長。增量也可爲負數進行減法操作。儘可以在存儲數值型域中執行該操作,否則會返回一個錯誤。

    HEXISTS KEY VALUE:查看哈希表key中,指定域field是否存在。若不存在或不被包含則返回0。

    Hash應用場景:

    1.通過對對象及屬性的存儲更加直觀和方便的獲取value值。

    2.常用於存儲對象信息(爲什麼不用String存儲一個對象?)Hash是最接近關係型數據庫結構的數據結構,可以將數據庫一條記錄或程序中一個對象轉換成HashMap直接存放在Redis中存儲。若用普通的key/value形式存儲,主要有兩種存儲方式,(key:id,value:JSON數據)用戶的id設爲查找的key,其他信息封裝成一個對象以序列化的方式存儲,缺點就在於增加了序列化/反序列化的開銷,並且在需要修改其中一項信息便將整個對象取出,會有CAS操作併發問題,即轉換對象和修改值問題。或者有多少成員就存儲多少key/value鍵值對,用用戶名稱+用戶id(如:username:1)作爲唯一標識,雖然省去了序列化開銷和併發問題,但是屬性越多,數據越多,就會容易出現內存不足問題。所以使用Hash存儲來解決這個問題。

    3.List:

    Redis列表是簡單的字符串列表,按照插入順序排序。是一個雙向鏈表結構,所以在頭尾元素操作上效率非常高。最多可以包含2^32-1個元素,類似於java中的LinkedList。

    List指令:

    LPUSH KEY VALUE:將一個或多個值插入到列表的頭部(左側)。若有多個值則是從左到右按順序插入。類似於棧,最左邊的值將先被壓入隊列中。

    RPUSH KEY VALUE:將一個或多個值插入到列表的尾部(右側)。

    LPUSHX KEY VALUE:當且僅當列表key存在且是一個列表(空列表也不行),纔將一個值插入到已存在的列表頭部,若列表不存在,則操作無效

    RPUSHX KEY VALUE:當且僅當列表key存在且是一個列表(空列表也不行),纔將一個值插入到已存在的列表尾部,若列表不存在,則操作無效

redis> LLEN emptyList
(integer) 0
redis> LPUSHX emptyList "a"
(integer) 0

    LLEN KEY:獲取列表key的長度,若key不存在,則該key被解釋爲是一個空列表。

    LINDEX KEY INDEX:通過索引獲取列表key元素。類似於數組下標,也可以使用負數下標表示反向獲取,即獲取排序倒數的元素。如-1表示最後一個元素。若index參數的值超出了列表key的範圍,則返回nil。

    LRANGE KEY START END:返回指定範圍內的列表key元素(0表示第一個元素,-1表示最後一個元素)

    LPOP KEY:移除並獲取列表的第一個元素(左側),當列表key不存在時,返回nil。

    RPOP KEY:移除並獲取列表的第一個元素(右側),當列表key不存在時,返回nil。

    BLPOP KEY TIMEOUT:阻塞式彈出,移除並獲取列表的第一個元素(左側),如果列表沒有元素會阻塞列表直到等待超時或者發現可被彈出的元素爲止,否則彈出nil。

    BRPOP KEY TIMEOUT:阻塞式彈出,移除並獲取列表的第一個元素(右側),如果列表沒有元素會阻塞列表直到等待超時或者發現可被彈出的元素爲止,否則彈出nil。

    LTRIM KEY START END:對一個列表進行截取,不在範圍內的元素都將被刪除(包含邊界元素)。

    LSET KEY INDEX VALUE:通過索引設置列表key下標爲index的元素value值。對頭元素或尾元素操作很快。

    LINSERT KEY BEFORE|AFTER WORD VALUE:在列表key的word元素前|後插入元素,若元素不存在在key中或key不存在時均不進行任何操作。

    RPOPLPUSH SOURCE DESTINATION:移除列表最後一個元素,並且將該元素添加到另一個列表並返回,也可以當做是將循環列表中的最後元素移到最左側(兩個列表變量均爲一個即可)

    BRPOPLPUSH SOURCE DESTINATION TIMEOUT:從列表中彈出一個值,將彈出的元素插入到另外一個列表中並返回,如果列表中沒有元素會阻塞列表直到等待超時或發現可彈出元素。

    List應用場景:

    1.對數據量大的集合數據刪減(如:列表數據顯示,關注列表,粉絲列表,留言),可以通過lrange命令很好的實現分頁功能

    2.可以組合命令以實現隊列模型,如使用lpush和brpop實現消息隊列模型,使用lpush和lpop實現棧模型等。

    4.Set:

    Set類型是String類型的無序集合。集合元素是唯一的,不允許重複。Redis中的集合是通過哈希表實現的,所以添加刪除查找複雜度都是O(1)。類似於HashTable。

    Set命令:

    SADD KEY MEMBER:向集合添加一個或多個成員。

    SCARD KEY:獲取集合成員數,集合長度。

    SMEMBERS KEY:返回集合中的所有成員

    SISMEMBER KEY MEMBER:判斷member元素是否是集合key的成員。

    SRANDMEMBER KEY:返回集合中一個或多個隨機成員。

    SREM KEY MEMBER:移除集合中一個或多個成員,返回被成功移除的成員數量。

    SPOP KEY:移除並返回集合中的一個隨機元素。

    SMOVE SOURCE DESTINATION MEMBER:將member元素從SOURCE集合移動到DESTINATION,若該元素不存在則不執行任何操作。

    SDIFF KEY1 KEY2:返回給定的所有集合的差集(以左側數據爲根據)。

    SDIFFSTORE DESTINATION KEY1:返回給定的所有集合的差集並存儲在DESTINATION中。

    SINTER KEY1 KEY2:返回給定的所有集合的交集。

    SINTERSTORE DESTINATION KEY1:返回給定的所有集合的交集並存儲在DESTINATION中。

    SUNION KEY1 KEY2:返回給定的所有集合的並集。

    SUNIONSTORE DESTINATION KEY1:返回給定的所有集合的並集並存儲在destination中。

    Set應用場景:

    1.通過集合操作返回共同關注,共同喜好。

    2.利用唯一性,可以統計訪問網站的所有獨立IP,唯一性驗證。

    5.Zset:

    Zset類型是String類型的有序集合。每個元素都會關聯一個double類型的分數,通過分數來爲集合中的元素排序,有序集合中的元素還是不可以重複,但是分數是可以重複的。

    Zset命令:

    ZADD KEY SCORE MEMBER:向有序集合添加一個或多個成員,或者更新已存在的成員的分數。

    ZCARD KEY:獲取有序集合的成員數,集合長度。

    ZCOUNT KEY MIN MAX:計算在有序集合中指定區間分數的成員數。

    ZRANK KEY MEMBER:返回有序集合中指定成員的索引。

    ZRANGE KEY START END:通過索引區間返回有序集合或指定區間內的成員(低到高)。

    ZREVRANGE KEY START END:返回有序集合中指定區間的成員,通過索引,分數從高到低。

    ZREM KEY MEMBER:移除有序集合中的一個或多個成員。

    ZREMRANGEBYRANK KEY START END:移除有序集合中給定的排名區間的所有成員。

    ZREMRANGEBYSCORE KEY START END:移除有序集合中給定的分數區間的所有成員(包含邊界值)。

    Zset應用場景:

    1.排行榜。https://www.jianshu.com/p/6d1991fef1d7

    2.權重隊列。http://doc.redisfans.com/sorted_set/zadd.html

    7.普通JAVA連接Redis方式:

    在使用普通的方式進行Redis連接時,需要引入Jedis依賴。我省略了get/set方法。

package com.day_8.excercise_1;

public class Person {

	private Integer id;
	private String name;
	private Integer age;
	private String sex;
	......
}
package com.day_8.excercise_1;

import java.util.HashMap;
import java.util.Map;
import org.junit.Test;
import redis.clients.jedis.Jedis;
public class TryConnectRedis1 {
	
	public static void main(String[] args) {
		Jedis jedis = new Jedis("127.0.0.1",6379);// 默認就是127.0.0.1:6379
		//若有密碼則可以通過auth進行驗證登錄,默認無密碼
//		jedis.auth("redis");
		System.out.println(jedis.ping());
	}
	@Test
	public void setValueAndGet() {
		Jedis jedis = new Jedis("127.0.0.1",6379);
//		jedis.auth("redis");
		jedis.set("TryConnect","20191222");
		String getString = jedis.get("TryConnect");
		System.out.println(getString);
		jedis.close();
	}	
	/*
	 * 可以通過對數據庫查詢和Redis查詢的切換來緩存機制緩解數據庫查詢壓力
	 */
	@Test
	public void getValueWithExist() {
		Jedis jedis = new Jedis("127.0.0.1",6379);
//		jedis.auth("redis");
		String keyName = "TryConnect";
		if (jedis.exists(keyName)) {
			String keyValue = jedis.get(keyName);
			System.out.println("Redis查詢:"+keyValue);
		}else {
			String keyValue = "nope";
			jedis.set(keyName, keyValue);
			System.out.println("MySQL查詢:"+keyValue);
		}
		jedis.close();
	}
}

    也可以通過使用Jedis內提供的JedisPool連接池來管理對Redis的連接。

package com.day_8.excercise_1;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

public class RedisPoolUtils {
	private static JedisPool pool;
	static {
		String host = "127.0.0.1";
		Integer port = 6379;
		// Redis連接池基本配置信息
		JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
		jedisPoolConfig.setMaxIdle(1);
		// Redis連接池
		pool = new JedisPool(jedisPoolConfig,host,port);
	}
	public static Jedis getJedis() {
		Jedis jedis = pool.getResource();
//		jedis.auth("redis");
		return jedis;
	}
	public static void close(Jedis jedis) {
		jedis.close();
	}
}
package com.day_8.excercise_1;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

public class TryConnectRedis2 {

	public static void main(String[] args) {
		Jedis jedisFromUtils = RedisPoolUtils.getJedis();
		System.out.println(jedisFromUtils.get("TryConnect"));
		RedisPoolUtils.close(jedisFromUtils);
		
	}
	
}

    8.SpringDataRedis:

    更多時間是在使用Spring連接Redis,一般不會出現自己連接Redis的場景,而在使用Spring連接時,可以使用Spring-data-redis,其中提供了很多在spring應用中配置訪問Redis服務的工具,並對redis底層開發包(Jedis,JRedis,RJC)進行了高度封裝。提供的RedisTemplate提供了對Redis的各種操作,異常處理和默認的序列化。

    先實現一個簡單的SpringDataRedis的一個Demo:

    (1).構建 Maven 工程,並引入 JUnit 依賴,引入 Jedis 依賴和 SpringDataRedis 依賴。與此同時要導入commons-pool2依賴,因爲在使用JedisPoolConfig時需要對連接池進行設置,若沒有導入則沒有set方法存在。我這裏貼出我Demo中全部,應該可以拿來就用。

<dependencies>

	<dependency>
		<groupId>junit</groupId>
		<artifactId>junit</artifactId>
		<version>4.12</version>
		<scope>test</scope>
	</dependency>

	<!-- JACKSON -->
	<dependency>
		<groupId>com.fasterxml.jackson.core</groupId>
		<artifactId>jackson-core</artifactId>
		<version>2.8.9</version>
	</dependency>

	<dependency>
		<groupId>com.fasterxml.jackson.core</groupId>
		<artifactId>jackson-annotations</artifactId>
		<version>2.9.0</version>
	</dependency>

	<dependency>
		<groupId>com.fasterxml.jackson.core</groupId>
		<artifactId>jackson-databind</artifactId>
		<version>2.8.9</version>
	</dependency>

	<!-- <dependency>
		<groupId>org.springframework.data</groupId>
		<artifactId>spring-data-redis</artifactId>
		<version>2.2.0.RELEASE</version>
	</dependency> -->
	

	<!-- JEDIS -->
	<dependency>
		<groupId>redis.clients</groupId>
		<artifactId>jedis</artifactId>
		<version>2.9.0</version>
	</dependency>
	
	<!-- Spring Boot Cache -->
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-cache</artifactId>
	</dependency>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-data-redis</artifactId>
	</dependency>
	<dependency>
	    <groupId>org.springframework.boot</groupId>
	    <artifactId>spring-boot-starter-web</artifactId>
	</dependency>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-devtools</artifactId>
		<scope>runtime</scope>
		<optional>true</optional>
	</dependency>
	<dependency>
		<groupId>org.projectlombok</groupId>
		<artifactId>lombok</artifactId>
		<optional>true</optional>
	</dependency>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-test</artifactId>
	</dependency>
</dependencies>

    此時會注意到我使用的是Spring-boot-starter-data-Redis,而不是網絡上Redis項目導入更多的Spring-data-redis。這裏我也仔細的去看了一下,原來在前者的pom.xml文件中也存在Spring-data-redis,也就是說前者是包含後者的。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"...">
	......
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.data</groupId>
			<artifactId>spring-data-redis</artifactId>
		</dependency>
		<dependency>
			<groupId>redis.clients</groupId>
			<artifactId>jedis</artifactId>
		</dependency>
	</dependencies>
	......
</project>

    (2).在application.properties文件中進行對Redis的配置,在配置文件中可以配置很多基本Redis的適應性設置。

# Redis數據庫索引
spring.redis.database=0  
# Redis服務器地址
spring.redis.host=127.0.0.1
# Redis服務器連接端口
spring.redis.port=6379  
# Redis服務器連接密碼(默認爲空)
spring.redis.password=
# 連接池最大連接數
spring.redis.pool.max-active=8  
# 連接池最大阻塞等待時間
spring.redis.pool.max-wait=-1  
# 連接池中的最大空閒連接
spring.redis.pool.max-idle=8  
# 連接池中的最小空閒連接
spring.redis.pool.min-idle=0  
# 連接超時時間(毫秒)
spring.redis.timeout=0  

    (3).創建RedisConfig配置

package com.SpringRedis.api;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.*;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        // 配置連接工廠
        template.setConnectionFactory(factory);

        //由於key值一般直接爲字符串,故直接使用StringRedisSerializer進行序列化與反序列化key值
        //1.
        //使用Jackson2JsonRedisSerializer來序列化和反序列化value值
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new Jackson2JsonRedisSerializer(Object.class));
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(new Jackson2JsonRedisSerializer(Object.class));

        //2.
//        //使用GenericJackson2JsonRedisSerializer來序列化和反序列化value值
//        template.setKeySerializer(new StringRedisSerializer());
//        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
//        template.setHashKeySerializer(new StringRedisSerializer());
//        template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());

        // 初始化RedisTemplate
        template.afterPropertiesSet();

        return template;
    }

    @Bean
    public HashOperations<String, String, Object> hashOperations(RedisTemplate<String, Object> redisTemplate) {
        return redisTemplate.opsForHash();
    }
    @Bean
    public ValueOperations<String, Object> valueOperations(RedisTemplate<String, Object> redisTemplate) {
        return redisTemplate.opsForValue();
    }
    @Bean
    public ListOperations<String, Object> listOperations(RedisTemplate<String, Object> redisTemplate) {
        return redisTemplate.opsForList();
    }
    @Bean
    public SetOperations<String, Object> setOperations(RedisTemplate<String, Object> redisTemplate) {
        return redisTemplate.opsForSet();
    }
    @Bean
    public ZSetOperations<String, Object> zSetOperations(RedisTemplate<String, Object> redisTemplate) {
        return redisTemplate.opsForZSet();
    }
}

    在該配置文件中對RedisTemplate進行了一些設置的初始化,並在序列化的選擇上出現了可選項,RedisTemplate默認使用的是JdkSerializationRedisSerializer。使用默認設置則被序列化的對象必須實現Serializable接口,並且在存儲時可讀性差,內容冗長。

public class RedisTemplate<K, V> extends RedisAccessor implements RedisOperations<K, V>, BeanClassLoaderAware {
	......
	private RedisSerializer keySerializer = null;
	private RedisSerializer valueSerializer = null;
	private RedisSerializer hashKeySerializer = null;
	private RedisSerializer hashValueSerializer = null;
	private RedisSerializer<String> stringSerializer = new StringRedisSerializer();
	......
	public void afterPropertiesSet() {
		super.afterPropertiesSet();
		boolean defaultUsed = false;
		// 若沒有設置統一的默認序列化對象,則使用JdkSerializationRedisSerializer
		if (defaultSerializer == null) {
			defaultSerializer = new JdkSerializationRedisSerializer(
					classLoader != null ? classLoader : this.getClass().getClassLoader());
		}
		if (enableDefaultSerializer) {
			if (keySerializer == null) {
				keySerializer = defaultSerializer;
				defaultUsed = true;
			}
			if (valueSerializer == null) {
				valueSerializer = defaultSerializer;
				defaultUsed = true;
			}
			if (hashKeySerializer == null) {
				hashKeySerializer = defaultSerializer;
				defaultUsed = true;
			}
			if (hashValueSerializer == null) {
				hashValueSerializer = defaultSerializer;
				defaultUsed = true;
			}
		}
	......
	}
	......
}

    所以我們可以根據需要靈活地修改序列化的設置。如使用StringRedisTemplate使用的序列化方式StringRedisSerializer,Jackson2JsonRedisSerializer或者GenericJackson2JsonRedisSerializer等。在使用後兩者時,返回值略有不同。上方是使用了Jackson2JsonRedisSerializer下方是使用了GenericJackson2JsonRedisSerializer。前者需要在使用時傳入一個序列化對象Class,我們可以統一使用Object.class。而使用GenericJackson2JsonRedisSerializer則會結果信息中存儲一個該對象的class信息。但是通過在網上的瞭解,貌似都在某些場景中存在問題,具體問題點等我測試出來再看哈。

127.0.0.1:6379> get UserInfo
"{\"name\":\"Vic\",\"sex\":\"\xe7\x94\xb7\",\"age\":23}"
127.0.0.1:6379> get UserInfo
"{\"@class\":\"com.SpringRedis.api.UserInfo\",\"name\":\"Vic\",\"sex\":\"\xe7\x94\xb7\",\"age\":23}"

    (4).進行測試。

    在有了上述的配置後一般的場景就可以進行簡單的測試了,但是一般都會通過RedisTemplate來進一步實現一些命令的封裝以形成工具類,工具類網上還是有很多的,就不在這裏貼出來了,這個測試中不會出現工具類內內容,也請放心測試簡單場景。同樣省略了get/set方法。

package com.SpringRedis.api;

public class UserInfo {
    private String name;
    private String sex;
    private Integer age;
	......
}
package com.SpringRedis.api;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.*;
import org.springframework.test.context.junit4.SpringRunner;
import javax.annotation.Resource;
import java.util.Set;
import java.util.concurrent.TimeUnit;

@RunWith(SpringRunner.class)
@SpringBootTest
public class TestRedis {
    @Resource
    private ValueOperations<String,Object> valueOperations;
    @Test
    public void testValueOption( )throws  Exception{
        UserInfo userInfo = new UserInfo();
        userInfo.setSex("男");
        userInfo.setName("Vic");
        userInfo.setAge(23);
        valueOperations.set("UserInfo",userInfo);

        System.out.println(valueOperations.get("UserInfo"));
    }
}

    9.Redis訂閱與發佈:

    Redis通過publish/subscribe等命令來實現一種消息通信模式,即發佈者發送消息,接受者接收消息。在這種模式中,客戶端可以訂閱任意數量的頻道,並且每次發佈者發佈新的消息時,所有的訂閱者均能獲取到該信息。

    Redis訂閱與發佈相關命令:

    訂閱頻道

    SUBSCRIBE CHANNEL:訂閱給定的一個或多個頻道的信息。

    PSUBSCRIBE PATTERN:訂閱一個或多個符合給定模式的頻道。

    發佈頻道:

    PUBLISH CHANNEL MESSAGE:將信息發送到指定的頻道。

    退訂頻道:

    UNSUBSCRIBE CHANNEL:退訂指定的頻道。

    PUNSUBSCRIBE CHANNEL:退訂所有給定模式的頻道。

    在Redis底層擁有兩個結構體,redisClient和redisServer,均擁有一個字典類型的pubsub_channels屬性。在服務端用來保存訂閱頻道的信息。在該字典中,鍵便爲正在被訂閱的頻道,值爲一個鏈表,鏈表中保存了所有訂閱該頻道的客戶端。故訂閱的操作實質上就是將客戶端加入到服務器的該pubsub_channels字典對應頻道的鏈表中,並且在客戶端的頻道訂閱字典pubsub_channels中加入訂閱的頻道。退訂操作正相反。

    10.Redis事務:

    Redis事務實際上就是一組命令的集合。在事務中可以一次執行多個命令,按順序的串行化執行,事務中所有的命令都會被序列化。並且不會受其他客戶端提交的命令的影響,這裏的意思是一個事務執行過程中不會被其他命令插入其中。事務的三個階段就是開始事務——命令入隊——執行事務。

    Redis事務命令:

    DISCARD:取消事務,放棄執行事務塊中的所有命令。

    EXEC:執行所有事務塊內的命令。

    MULTI:標記一個事務塊的開始。

    UNWATCH:取消WATCH命令對KEY的監視,若在執行WATCH命令後,EXEC命令或DISCARD命令先被執行的話,就不需要再執行。

    WATCH KEY:監視一個或多個KEY,如果在事務執行之前這個(或這些)KEY被其他命令改動,那麼事務被打斷。WATCH的聲明週期在事務執行後結束。

    Redis事務到底能不能保證原子性?

    有關於這個問題我查了網上的挺多的資料,但是由於畢竟網絡我也沒有找到一個官方地方給出具體的解釋。問題關注的點在於原子性的定義。在百度百科上對於數據庫事務的原子性的定義是“原子性(Atomicity):事務中的全部操作在數據庫中是不可分割的,要麼全部完成,要麼全部不執行。”。就這一點看來的話,Redis的事務是具備原子性的,但是從某些其他地方找到的關於原子性的定義則會多兩句,如“原子性( “ACID” 特性)聲明,對於一系列的數據庫操作,要麼所有操作一起提交,要麼全部回滾;不允許中間狀態存在。”,也就是“回滾”兩個字。在Redis中是不存在回滾的,並且在Redis事務中任意命令執行失敗,其餘命令仍會被執行。如下,我給定了一個TEST的key值爲k1,很明顯不能對該key使用incr命令。而後我書寫了一個事務,由結果可知所有的key/value的設定都是執行了的,而實際上“incr TEST”語句是報錯的。當然若我將某一個給定key/value的設定放在incr語句之前,也是可以執行的,也就說明Redis事務也沒有“回滾”。如果以後看到了更加清晰明確的解釋我會再聲明,也希望有關於這個問題的有理有據的解釋能夠回覆給我,多謝!

    Redis事務的錯誤處理:

    1.如果在事務中執行某個命令報錯,則只有報錯的命令不會被執行,而其他的命令都會執行,不會回滾。

    2.如果在事務隊列中的某個命令出現了命令性錯誤,執行時整個的所有隊列都會被取消。實質上這個情況就是在書寫事務的時候沒有得到正確的命令入隊(QUEUED)的反饋,而是出現了“error”字樣。

package com.day_8.excercise_1;

import com.alibaba.fastjson.JSONObject;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;

public class TryTransaction {
	public static void main(String args[]) {
		Jedis jedis = new Jedis("127.0.0.1", 6379);
		String result = "test";
		// 開啓事務
		Transaction multi = jedis.multi();
		try{
			multi.set("test1".getBytes(), result.getBytes());
			multi.set("test2".getBytes(), result.getBytes());
			// 這裏引發了異常,用0作爲被除數
			//1.
//			int i = 1 / 0;
			multi.exec();
		}catch(Exception e){
			e.printStackTrace();
			multi.discard();
		}finally{
			jedis.close();
		}
	}
}

    當不使用場景1時,則可以直接設定該key/value。當使用場景1時,則會引發異常,當然key/value的設定也沒有執行。

    11.Redis持久化:

    Redis爲持久化提供了兩種方式,即RDB和AOF。

    RDB:RDB是Redis的默認持久化機制,相當於是快照。指在指定時間間隔內將內存中的數據集全部寫入磁盤中,並且是重新記錄整個數據集的所有信息。由於RDB能將內存中的數據以快照的方式寫入到二進制文件中,所以快照保存數據極快,同樣恢復數據也極快,適用於容災備份。配置文件的默認RDB策略是每隔900秒,在這期間至少變化了一個鍵值就做快照;每隔300秒,至少變化了10個鍵值就做快照;每隔60秒,至少變化了10000個鍵值就做快照。除了配置文件的默認策略,還可以使用手動持久化命令SAVE和BGSAVE,前者會阻塞Redis服務器,直至持久化完成;後者會調用Fork產生一個子進程,則不會影響整個服務器,但是由於子進程在進行數據快照,所以在子進程運行期間的數據不會進行持久化,而是被複制一份等待進入共享內存,而在這期間若發生宕機等情況,則會丟失一部分數據。

    AOF:由於快照方式是在一定間隔時間做一次,而一旦宕機rdb文件內存儲的會就是最後一次快照後的所有操作。所以如果系統要求不能丟失任何修改的話,應該使用AOF持久化方式。在使用AOF持久化操作時,Redis會將每一個收到的寫命令都通過write函數追加到aof文件中。當Redis服務器重啓時則會通過重新執行文件中保存的寫命令來在內存中重建整個數據庫的內容。在配置AOF的持久化策略上,Redis默認使用everysec,也就是每一秒持久化一次,也提供always(每次操作都立即寫入)和no(不註定進行同步操作,默認30s一次)。也就是說當宕機或者誤操作時,只需要重啓服務器,或者在aof文件中移除尾部的誤操作,便可以重新恢復數據。

    RDB持久化機制不適合小內存機器使用,因爲RDB機制符合要求就會照快照,並且非常消耗CPU也無法進行如AOF的秒級別的持久化操作。但是整個數據庫只會包含一個文件,方便備份,且RDB在恢復大數據集時速度比AOF快。而AOF除了回覆速度較慢外,整個AOF文件的體積較之RDB會大。但是在使用AOF時仍可以保證Redis性能和數據安全,且AOF文件較爲容易恢復和更改。

    不過Redis也支持同時開啓 RDB 和 AOF兩種策略,而在Redis服務器重啓後,會優先使用 AOF來恢復數據。

    12.Redis緩存和數據庫的一致性

    使用緩存獲取數據是提升應用性能的常見手段,但是也會面臨需要解決的問題,如數據庫數據和緩存中數據的一致性問題。要想保證數據一致性則可以:

    1.實時同步:

    對強一致性要求比較高的,應採用實時同步機制。但是實時同步也存在問題。我們在讀取信息時,可以先查詢緩存內信息,若緩存查詢不到再從數據庫中查詢,而後保存到緩存中。但是更新數據時則會出現一系列的問題,如:

    (1).數據庫/緩存更新成功,緩存/數據庫更新失敗。

    (2).數據庫更新成功,清除緩存失敗。

    (3).清除緩存成功,更新數據庫失敗。

    出現上述問題的主要原因就是,在多線程操作中會存在各種原因導致的順序問題,從而就會影響數據質量,形成髒數據。推薦使用更新緩存時,先更新數據庫,再將緩存的設置過期。也可以使用鎖機制保證數據運行過程,但是會相應的增大一些壓力。

    在使用SpringBoot進行開發時也可以使用註解方式來保證多線程併發訪問,將數據庫壓力轉移給Redis。

    @Cacheable:在執行該註解標註的方法前會先從緩存中查詢是否有數據存在,若存在則直接返回緩存內數據,若不存在則執行該方法並將返回值放入內存中。

    @CachePut:無論數據在緩存中是否存在都會執行該註解標註方法,並且將方法返回值記錄在緩存中,實現緩存與數據庫的同步更新。

    @CacheEvict:方法執行成功後會從緩存中移除滿足條件的緩存數據。

    @Caching:組合多個Cache註解使用。

    2.異步隊列:對於併發程度高的程序,可以採用異步隊列的方式同步,可採用Kafka等消息中間件處理消息的生產和消費。也就是說先更新數據庫數據,而後將要刪除或要更新的key發送至消息隊列中,程序本身先消費該消息,而後再不斷重複嘗試原操作直至成功。

    3.同步工具:canal是阿里巴巴的一個開源項目,通過監聽mysql的binlog日誌獲取數據,能夠高性能的獲取mysql數據庫的數據變更,而後形成主從複製讀寫分離。

    4.利用UDF自定義函數的方式:通過書寫mysql的UDF函數使mysql操作後主動刷新,回寫Redis緩存。    

    13.使用Redis緩存會存在的問題:

    緩存穿透:指查詢一個數據庫內一定不存在的數據,由於緩存時不命中時需要從數據庫中查詢,但是每次都查不到數據則也就不會寫入緩存,從而導致這個不存在的數據每次請求都要求數據庫中查詢。

    解決辦法:持久層查詢不到就緩存空結果(如空""),查詢時先判斷緩存中是否exists(key),如果有則直接返回空,沒有則查詢後返回。

    緩存雪崩:緩存集中在一段時間內失效,會發生大量的緩存穿透,而導致所有的查詢都落在數據庫上,造成了緩存雪崩。

    解決辦法:沒有完美解決辦法,但是可以儘量讓失效時間分佈均勻。

    緩存擊穿:指有一個key被經常訪問使用,稱爲熱點key。在高併發的訪問下,一旦key失效,所有的併發請求就會直接落在數據庫上,形成緩存擊穿。

    解決辦法:可以設置熱點key爲永不過期。

    14.Redis高併發問題

    針對Redis的高併發可能會出現的單臺Redis可能的單點故障問題,請求負載壓力過大問題,內存容量限制問題都可以通過垂直拓展和水平拓展的方式來相應的解決。垂直拓展就是提升單臺Redis服務器的性能,但是這種方式畢竟還是有着很容易達到的上限,所以最好的方式就是使用水平拓展,增加Redis服務器的數量,減輕每臺Redis服務器的壓力。

    主從複製,讀寫分離:由於在應用場景中,一般寫少讀多,所以在主從複製上,一般主服務器只負責寫的請求,從服務器負責讀的請求。這樣做不僅可以提高服務器負載能力,也可以根據讀請求的規模自由增加或減少從庫的數量。此外,由於使用了主從複製,數據也會被複制多份,就算有一臺機器出現故障也可以使用其他機器進行數據恢復。

    15.Redis集羣

    同一些其他數據庫相同,Redis也存在集羣配置,在解決高併發時的水平拓展中也提到了。

    1.主從模式:

    通過在從節點的配置文件中設置slaveof+ip指定master節點的ip和端口號即可。主從模式可以備份數據,同時也可以降低某一個節點的壓力,達到負載均衡。默認配置中,主從模式就遵循着讀寫分離,主節點可讀可寫,從節點只能進行讀操作。主節點修改數據後從節點的數據會被覆蓋,從節點宕機不會影響其他節點讀寫操作,並且重啓後還會從其對應的master節點同步。主節點宕機後不影響讀操作,但是不能提供寫操作了。

    2.哨兵模式

    哨兵模式是建立在主從模式的基礎上,用來解決master節點宕機後slave節點無法成爲master節點的問題,那麼便通過設置哨兵模式設置另一個節點成爲哨兵,在master節點宕機後重新選舉一個master。在master節點宕機後,哨兵模式將在slave中選擇一個作爲master並修改配置文件。一個sentinel或sentinel集羣可以管理多個主從Redis。當使用哨兵模式時最好不要直接連接Redis服務器,而是連接哨兵節點,以防止master節點宕機產生問題。

    3.集羣模式

    哨兵模式下雖然保證了高可用和讀寫分離,但是每臺Redis服務器都存儲相同的數據浪費空間,故可以使用集羣模式。使用集羣模式再每臺Redis節點上存儲着不同的內容。Redis集羣同Zookeeper等相同,存在投票機制,所以在集羣中要有2n+1個節點,至少要有3個master節點,並且每個節點至少有1個slave節點,才能建立集羣,採用無中心結構,每個節點保存數據和整個集羣狀態,每個節點都有和其他節點的連接。

    集羣投票:投票過程是集羣中所有master節點參與,如果半數以上master節點與master節點通信超時,則認爲當前master節點宕機。如果集羣任意master宕機,且當前master沒有slave,集羣進入fail狀態。如果集羣超過半數以上的master節點宕機,無論有沒有slave節點,集羣進入fail狀態。

    redis集羣節點分配:採用哈希槽(hashslot)的方式來分配16384個插槽(slot),當要存取操作時Redis會根據算法將結果與插槽相對應,而後直接跳轉到該節點進行存取操作。並且每增加一個節點,都是在之前創建的各個節點的前面各取一部分插槽作爲分配給新節點的插槽。如下:

    節點A:0-5460

    節點B:5461-10922

    節點C:10923-16383

    新增節點D

    節點A:1365-5460

    節點B:6827-10922

    節點C:12288-16383

    節點D:0-1364,5461-6826,10923-12287

    針對Redis的部分知識都瞭解了一通,比較淺顯,很多知識點都是一帶而過,但是也算是有了Redis的一個較爲完整的知識體系。並且也寫了一些代碼,以後針對特定問題再進行特定的代碼書寫和知識學習就清晰多了。感謝各位在網絡上的分享,多謝!

 

 

 

 

發佈了19 篇原創文章 · 獲贊 6 · 訪問量 4934
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章