1.Redis 基本介紹
1.1Redis設置外網訪問
- 註釋bind並且把protected-mode no
- 使用bind 0.0.0.0
- 設置密碼 protected-mode它啓用的條件有兩個,第一是沒有使用bind,第二是沒有設置訪問密碼。
1.2Redis通用命令
- keys * :獲取所有的key(*代表任意多個字符,?代表任意1個字符)
ps:生產環境一定不用,用scan--根據遊標查詢一定數量,可匹配的key值。
- del key key2... :刪除key
- exists key:判斷是否存在
- rename key newkey
- expire key 20:設置20秒後過期
- ttl key:獲取key的剩餘過期時間(-1代表沒設置,-2代表過期)
- type key
- move key 1:將本庫的key剪切到1號庫中
1.3string對象常用命令(字符串)
賦值
- set key value
- append key value 如果key存在,則在指定的key末尾添加,如果key存在則類似set
- strlen key 返回此key的長度
- setrange key 1(開始位置,從哪裏開始設置) 具體值 設置(替換)指定區間範圍內的值
- setex 鍵 秒值 真實值 設置帶過期時間的key,動態設置。
- setnx key value 只有在 key 不存在時設置 key 的值
- mset key1 value key2 value 同時設置一個或多個 key-value 對。
- msetnx key1 value key2 value 同時設置一個或多個 key-value 對,當且僅當所有給定 key 都不存在。
取值
- get key
- getset key value :先獲取原值,在設置新值
- getrange key 0(開始位置) -1(結束位置) 獲取指定區間範圍內的值,(0 -1)表示全部
- mget key1 key 2 獲取所有(一個或多個)給定 key 的值。
- getset key value 將給定 key 的值設爲 value ,並返回 key 的舊值(old value)。
刪除
- del key
數值增減
- incr key :key存在,就在原值上加一,不存在創建一個0然後加1
- decr key
- incrby key increment
- decrby key increment
1.4hash對象常用命令:(字典--散列/hash表+鏈表結構--兩個哈希表用於擴容/縮容)
賦值:
- hset key field value
- hmset key field value [field2 value2...]
取值:
- hget key field
- hmget key fields
- hgetall key
刪除:
- hdel key field [field2 field3...]
- hdel key:刪除整個field
增加:
- hincrby key field increment
- hincrdyfloat key key1 數量(浮點數,小數) 執定hash表中某個字段加 數量
- hsetnx key key1 value1 與hset作用一樣,區別是不存在賦值,存在了無效。
其他:
- hkeys key
- hvals key
- hlen key
- hexists key field:判斷field是否存在
1.5list對象常用命令:(雙向鏈表)
如果鍵不存在,創建新的鏈表;如果鍵已存在,新增內容;如果值全移除,對應的鍵也就消失了。
- lpush mylist 1 3 5 7:頭插
- rpush mylist 1 3 5 7:尾插
- lrange mylist 0 -1:順序遍歷到最後一個元素(0和-1都代表元素下標)
- lindex key index 通過索引獲取列表中的元素
- lpop mylist :返回頭元素並彈出
- rpop mylist:返回尾部元素並彈出
- llen mylist:獲取元素個數
- lrem key 0(數量) 值,表示刪除全部給定的值。零個就是全部值 從left往right刪除指定數量個值等於指定值的元素,返回的值爲實際刪除的數量
- lset mylist 2 x:下標爲2的元素設置爲x
- linsert mylist before|after x 1:在x元素前或者後插入1
- rpoplpush mylist mylist:把mylist的尾部彈出(會返回元素),插入到頭部
1.6set常用命令:(字典-值爲null)
同list,如果不存在會自動創建
- sadd key value1 value2 向集合中添加一個或多個成員
- smembers myset:獲取key的集合成員
- srem myset a b:刪除成員
- scard key:獲取成員數量
- sismember key member:判斷成員是否在key中,返回0/1
- sdiff key1 key2:差集運算key1-key2 = key1-key1 & key2
- sinter key1 key2:交集運算
- suntion key1 key2:並集運算
- sdiffstore key key1 key2:把key1和key2的差集成員存儲在key
- srandmember key 數值 從set集合裏面隨機取出指定數值個元素 如果超過最大數量就全部取出
- spop key 隨機移出並返回集合中某個元素
1.7zset常用命令:(字典+跳錶)
zset 和 set 一樣也是string類型元素的集合,且不允許重複的成員。 通過分數來爲集合中的成員進行從小到大的排序。zset的成員是唯一的,但分數(score)卻可以重複。
- zadd key score val1 score val2
- zscore key member:返回指定成員的分數(分數存在會進行替換)
- zcard key:返回集合數量
- zcount key 開始score 結束score 獲取分數區間內元素個數
- zrank key vlaue 獲取value在zset中的下標位置(根據score排序)
- zrem key score某個對應值(value),可以是多個值 刪除元素
- zrange key start end:返回排序後的
- zrange key start end with scores:加上分數一起返回
- zrangebyscore key 開始score 結束score 返回指定score間的值
- zrevrange key start end with scores:從大到小排序
- zremrangebyrank key start end:根據排名範圍進行刪除
- zremrangebyscore key start end:根據分數範圍進行刪除
1.8其他特性
持久化的兩種機制AOF和RDB
集羣數據的複製同步(全量同步和部分同步),主從備份(主只寫可讀,從只讀),哨兵節點進行故障轉移
2.緩存問題
2.1緩存穿透
概念:
- 緩存穿透是指查詢一個一定不存在的數據,由於緩存不命中,並且出於容錯考慮, 如果從存儲層查不到數據則不寫入緩存,這將導致這個不存在的數據每次請求都要到存儲層去查詢,失去了緩存的意義。
可能造成原因:
- 1.業務代碼自身問題
- 2.惡意攻擊。爬蟲等等
解決方案(緩存空對象):
public class NullValueResultDO implements Serializable{ private static final long serialVersionUID = -6550539547145486005L; } public class UserManager { UserDAO userDAO; LocalCache localCache; public UserDO getUser(String userNick) { Object object = localCache.get(userNick); if(object != null) { if(object instanceof NullValueResultDO) { return null; } return (UserDO)object; } else { User user = userDAO.getUser(userNick); if(user != null) { localCache.put(userNick,user); } else { localCache.put(userNick, new NullValueResultDO()); } return user; } } }
2.3緩存擊穿
概念:
- 熱點key突然過期,併發量特別高導致同時去數據庫取數據重建緩存,數據庫在重建緩存的時候,會出現很多線程同時重建的情況,顯然只需要一個線程去重建緩存即可。
解決方案(分佈式互斥鎖):
public String getKey(String key){ String value = redis.get(key); if(value == null){ String mutexKey = "mutex:key:"+key; //設置互斥鎖的key if(redis.set(mutexKey,"1","ex 180","nx")){ //給這個key上一把鎖,ex表示只有一個線程能執行,過期時間爲180秒 value = db.get(key); redis.set(key,value); redis.delete(mutexKety); }else{ // 其他的線程休息100毫秒後重試 Thread.sleep(100); getKey(key); }else{ // 其他的線程休息100毫秒後重試 Thread.sleep(100); getKey(key); } } return value; }
2.4緩存雪崩
概念:
- 機器宕機或在我們設置緩存時採用了相同的過期時間,導致緩存在某一時刻同時失效,請求全部轉發到DB,DB瞬時壓力過重雪崩。
解決方案:
- key的過期時間設置的方差太小,可以優化方差來弱化該問題。
2.5數據一致性問題
問題:
- 緩存的數據和數據庫的數據不一致;
- 更新數據庫和更新緩存,不管是先更新誰,由於不是原子操作所以在多線程場景下一定會出現數據不一致的情況;
解決方案:
- 延時雙刪--寫線程會先刪除緩存,然後更新了數據庫,再刪除一次緩存就是爲了防止中間的讀操作緩存了舊數據;
- 假設沒有併發寫,如果有併發寫情況,一定會出現數據版本覆蓋問題;
- 嚴格串行化,使用單線程;
3.Redis網絡模型
3.1Java--NIO對應的I/O模型
根據操作系統來判斷
- 如果是windows用的是select多路複用模型,
- 如果是linux用的是epoll模型,
- 如果是solaris用的是poll模型,
實現的一個非阻塞IO
- 包含:一個服務器端口監聽套接字、一個服務器數據傳輸套接字、一個客戶端數據傳輸套接字
List<SocketChannel> list = new ArrayList(); ByteBuffer byteBuffer = ByteBuffer.allocate(1024); try { ServerSocketChannel ssc = ServerSocketChannel.open(); ssc.bind(new InetSocketAddress(9091));//端口默認可複用 ssc.configureBlocking(false); while (true){ //設置服務器的監聽套接字非阻塞 SocketChannel socketChannel = ssc.accept(); if(socketChannel==null){ Thread.sleep(1000); System.out.println("沒人連接"); for (SocketChannel channel : list) { int k =channel.read(byteBuffer); System.out.println(k); if(k!=0){ byteBuffer.flip(); System.out.println(new String(byteBuffer.array())); } } }else{ //設置客戶端read數據傳輸套接字非阻塞 socketChannel.configureBlocking(false); list.add(socketChannel); //得到套接字,循環所有的套接字,通過套接字獲取數據 for (SocketChannel channel : list) { int k =channel.read(byteBuffer); System.out.println(k+"======================================="); if(k!=0){ byteBuffer.flip(); System.out.println(new String(byteBuffer.array())); } } } } } catch (IOException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); }
3.2NIO實現一之select多路複用模型
文件描述符與FILE結構體介紹
- 文件描述符是文件描述符表這個數組的索引,FILE結構體相當於文件內容,比如socket就是ip+端口的文件,創建成功會返回一個文件描述符(listenfd = socket(AF_INET,SOCK_STREAM,0))。
- 當我們創建一個進程時,會創建文件描述符表,進程控制塊PCB中的fs指針指向文件描述符表,
當我們創建一個文件時,會爲指向該文件的指針FILE*關聯一個文件描述符並添加在文件描述符表中。- 對於文件描述符表來說fd相當於數組的索引,FILE*相當於數組的內容,指向一個文件結構體。文件描述符表默認設置大小爲1024,所以select最多隻能建立1024個socket連接。
- 文件描述符表前三位被佔用了,分別是標準輸入,標準輸出和標準錯誤;第四位爲服務端的監聽套接字,所以最多隻有1024-4=1020個客戶端同時連接。
select源碼分析
void start(){ //定義一個結構體(網絡地址結構體)主要在listenfd建立的時候用 struct sockaddr_in my_addr; my_addr.sin_family = AF_INET; // ipv4 my_addr.sin_port = htons(8080); my_addr.sin_addr.s_addr = htonl(INADDR_ANY); //定義一個結構體(網絡地址結構體)主要在accept的時候傳出客戶端的信息 struct sockaddr_in client_addr; char cli_ip[INET_ADDRSTRLEN] = ""; //fd,與客戶端通信 int clientfd = 0; //得到一個listen的fd文件描述符 int listenfd = socket(AF_INET,SOCK_STREAM,0); //綁定一個端口+ip,所以我們可以認爲一個socket就是一個ip+端口的網絡進程或者文件 bind(listenfd, (struct sockaddr*)&my_addr, sizeof(my_addr)); //設置同時能夠接受請求爲128個,注意是同一時間 listen(listenfd, 128); printf("listen client @port=%d...\n",8080); //參數定義 int lastfd = listenfd; int i; //位圖,姑且理解爲集合,能夠監聽到R事件 fd_set readset,totalSet; //先清空把位圖的值都改爲0 FD_ZERO(&readset); //listenfd 表示listenfd我需要監聽他的R事件 FD_SET(listenfd, &totalSet); while(1) { readset = totalSet; //setup 1 把監聽的套接字和客戶端的套接字給select int z = select(lastfd+1,&readset,NULL,NULL,NULL); //判斷是否有事件發生 if(z>0){ //如果有事件發生,其實我並不知道是什麼事件,但是有兩種情況 if(FD_ISSET(listenfd,&readset)){ socklen_t cliaddr_len = sizeof(client_addr); //因爲他是傳入傳出,會覆蓋 clientfd = accept(listenfd, (struct sockaddr*)&client_addr, &cliaddr_len); inet_ntop(AF_INET, &client_addr.sin_addr, cli_ip, INET_ADDRSTRLEN); printf("----------------------------------------------\n"); printf("client ip=%s,port=%d\n", cli_ip,ntohs(client_addr.sin_port)); FD_SET(clientfd, &totalSet); lastfd=clientfd; if(0==--z){ continue; } } for(i=listenfd+1;i<=lastfd;i++){//處理客戶端發過來的消息 if(FD_ISSET(i,&readset)){//如果第I個fd有R事件,那麼直接把內容讀出來 char recv_buf[512] = ""; int rs=read(i,recv_buf,sizeof(recv_buf)); if(rs==0){//客戶端關閉 close(i); FD_CLR(i,&totalSet); }else{ printf("%s\n",recv_buf); //write(0,recv_buf,rs); } } } } } }