日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > 数据库 >内容正文

数据库

Redis6——入门介绍

發布時間:2023/12/14 数据库 31 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Redis6——入门介绍 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

目錄

  • 1.NoSQL數據庫介紹
    • 1.1.NoSQL數據庫概述
    • 1.2.NoSQL適用場景
    • 1.3.NoSQL不適用場景
    • 1.4.常見的NoSQL數據庫
      • 1.4.1.Memcache
      • 1.4.2.Redis
      • 1.4.3.MongoDB
  • 2.Redis介紹
    • 2.1.Redis概述
    • 2.2.Redis應用場景
      • 2.2.1.配合關系型數據庫做高速緩存
      • 2.1.2.多樣的數據結構存儲持久化數據
    • 2.3.Redis安裝
      • 2.3.1.下載Redis安裝包
      • 2.3.2.安裝步驟
    • 2.4.Redis啟動
      • 2.4.1.前臺啟動(不推薦)
      • 2.4.2.后臺啟動(推薦)
  • 3.常用的五大數據類型
    • 3.1.Redis字符串(String)
      • 3.1.1.介紹
      • 3.1.2.常用命令
      • 3.1.3.數據結構
    • 3.2.Redis列表(List)
      • 3.2.1.介紹
      • 3.2.2.常用命令
      • 3.2.3.數據結構
    • 3.3.Redis集合(Set)
      • 3.3.1.介紹
      • 3.3.2.常用命令
      • 3.3.3.數據結構
    • 3.4.Redis哈希(Hash)
      • 3.4.1.介紹
      • 3.4.2.常用命令
      • 3.4.3.數據結構
    • 3.5.Redis有序集合Zset(sorted set)
      • 3.5.1.介紹
      • 3.5.2.常用命令
      • 3.5.3.數據結構
  • 4.Redis配置文件
    • 4.1.Units 單位
    • 4.2.INCLUDES包含
    • 4.3.網絡相關配置
      • 4.3.1.bind
      • 4.3.2.protected-mode
      • 4.3.3.Port
      • 4.3.4.tcp-backlog
      • 4.3.5.timeout
      • 4.3.6.tcp-keepalive
    • 4.4.GENERAL 通用
      • 4.4.1.daemonize
      • 4.4.2.pidfile
      • 4.4.3.loglevel
      • 4.4.4.logfile
      • 4.4.5.databases
  • 5.Redis的發布和訂閱
    • 5.1.發布和訂閱
    • 5.2.發布訂閱命令行實現
  • 6.Redis6新數據類型
    • 6.1.Bitmaps
      • 6.1.1.簡介
      • 6.1.2.常用命令
        • 6.1.2.1.setbit
        • 6.1.2.2.getbit
        • 6.1.2.3.bitcount
        • 6.1.2.4.bitop
      • 6.1.3.Bitmaps與set對比
    • 6.2.HyperLogLog
      • 6.2.1.簡介
      • 6.2.2.常用命令
        • 6.2.2.1.pfadd
        • 6.2.2.2.pfcount
        • 6.2.2.3.pfmerge
    • 6.3.Geospatial
      • 6.3.1.簡介
      • 6.3.2.常用命令
        • 6.3.2.1.geoadd
        • 6.3.2.2.geopos
        • 6.3.2.3.geodist
        • 6.3.2.4.georadius
  • 7.Jedis
    • 7.1.Jedis介紹
    • 7.2.Jedis常用操作
      • 7.2.1.連接測試
      • 7.2.2.操作相關數據類型
    • 7.3.Jedis實例——手機驗證碼
      • 7.3.1.功能需求
      • 7.3.2.功能實現
  • 8.Redis與SpringBoot整合
    • 8.1.整合步驟
    • 8.2.測試
  • 9.Redis事務
    • 9.1.Redis事務簡介
    • 9.2.Redis事務相關命令
    • 9.3.事務的錯誤處理
    • 9.4.事務沖突問題
      • 9.4.1.例子
      • 9.4.2.悲觀鎖
      • 9.4.3.樂觀鎖
  • 10.Redis事務——秒殺案例
    • 10.1.簡單秒殺
    • 10.2.秒殺并發模擬
      • 10.2.1.安裝工具ab
      • 10.2.2.使用ab進行測試
      • 10.2.3.配置Jedis連接池
    • 10.3.解決超賣問題
    • 10.4.解決庫存遺留問題
      • 10.4.1.問題演示
      • 10.4.2.問題解決
  • 11.Redis持久化——RDB
    • 11.1.RDB介紹
    • 11.2.RDB持久化流程
    • 11.3.fork()
    • 11.4.RDB相關配置
      • 11.4.1.dump.rdb快照文件
      • 11.4.2.快照配置
      • 11.4.3.快照配置命令
  • 12.Redis持久化——AOF
    • 12.1.AOF介紹
    • 12.2.AOF持久化流程
    • 12.3.AOF恢復
    • 12.4.AOF與RDB的選擇
  • 13.Redis主從復制
    • 13.1.概述
    • 13.2.實現
    • 13.3.復制原理
    • 13.4.三種特殊情況
      • 13.4.1.一主二仆
      • 13.4.2.薪火相傳
      • 13.4.3.反客為主
    • 13.5.哨兵模式
      • 13.5.1.介紹
      • 13.5.2.使用步驟
      • 13.5.4.故障恢復
  • 14.Redis集群
    • 14.1.概述
    • 14.2.搭建集群
    • 14.3.集群操作
    • 14.4.故障恢復
    • 14.5.集群的Jedis開發
  • 15.Redis應用問題解決
    • 15.1.緩存穿透
      • 15.1.1.問題描述
      • 15.1.2.解決方案
    • 15.2.緩存擊穿
      • 15.2.1.問題描述
      • 15.2.2.解決方案
    • 15.3.緩存雪崩
      • 15.3.1.問題描述
      • 15.3.2.解決方案
    • 15.4.分布式鎖
      • 15.4.1.問題描述
      • 15.4.2.使用Redis命令實現分布式鎖
      • 15.4.3.使用Java代碼實現分布式鎖
      • 15.4.4.總結
  • 16.Redis6新功能
    • 16.1.ACL
      • 16.1.1.介紹
      • 16.1.2.相關命令
    • 16.2.I/O多線程
      • 16.2.1.介紹
      • 16.2.2.原理架構
    • 16.3.工具支持 Cluster
    • 16.4.Redis6其它新功能

本文章筆記整理來自黑馬尚硅谷視頻https://www.bilibili.com/video/BV1Rv41177Af,相關資料可以在評論區進行獲取。

1.NoSQL數據庫介紹

1.1.NoSQL數據庫概述

(1)NoSQL(Not Only SQL),即“不僅僅是SQL”,它泛指非關系型的數據庫。
(2)NoSQL 不依賴業務邏輯方式存儲,而以簡單的key-value模式存儲。因此大大的增加了數據庫的擴展能力。
(3)NoSQL特點:不遵循SQL標準、不支持ACID、遠超于SQL的性能。

1.2.NoSQL適用場景

(1)對數據高并發的讀寫
(2)海量數據的讀寫
(3)對數據高可擴展性的

1.3.NoSQL不適用場景

(1)需要事務支持
(2)基于sql的結構化查詢存儲,處理復雜的關系,需要即席查詢
(3)用不著sql的和用了sql也不行的情況

1.4.常見的NoSQL數據庫

1.4.1.Memcache


(1)很早就出現的NoSql數據庫
(2)數據都在內存中,一般不持久化
(3)支持簡單的key-value模式,支持類型單一
(4)一般是作為緩存數據庫輔助持久化的數據庫

1.4.2.Redis


(1)幾乎覆蓋了Memcached的絕大部分功能
(2)數據都在內存中,支持持久化,主要用作備份恢復
(3)除了支持簡單的key-value模式,還支持多種數據結構的存儲,比如 list、set、hash、zset等。
(4)一般是作為緩存數據庫輔助持久化的數據庫

1.4.3.MongoDB


(1)高性能、開源、模式自由(schema free)的文檔型數據庫
(2)數據都在內存中, 如果內存不足,把不常用的數據保存到硬盤
(3)雖然是key-value模式,但是對value(尤其是json)提供了豐富的查詢功能
(4)支持二進制數據及大型對象
(5)可以根據數據的特點替代RDBMS ,成為獨立的數據庫。或者配合RDBMS,存儲特定的數據。

2.Redis介紹

2.1.Redis概述

(1)Redis是一個開源的key-value存儲系統,其功能和Memcached類似,但是它支持存儲的value類型相對更多,包括string(字符串)、list(鏈表)、set(集合)、zset(sorted set --有序集合)和hash(哈希類型等)。這些數據類型都支持push/pop、add/remove及取交集并集和差集及更豐富的操作,而且這些操作都是原子性的。在此基礎上,Redis支持各種不同方式的排序。
(2)Redis與memcached一樣,為了保證效率,數據都是緩存在內存中。區別的是Redis會周期性的把更新的數據寫入磁盤或者把修改操作寫入追加的記錄文件,并且在此基礎上實現了master-slave(主從)同步
(3)Redis是單線程+多路I/O復用技術
多路復用是指使用一個線程來檢查多個文件描述符(Socket)的就緒狀態,比如調用select和poll函數,傳入多個文件描述符,如果有一個文件描述符就緒,則返回,否則阻塞直到超時。得到就緒狀態后進行真正的操作可以在同一個線程里執行,也可以啟動線程執行(比如使用線程池)。

2.2.Redis應用場景

2.2.1.配合關系型數據庫做高速緩存

(1)高頻次,熱門訪問的數據,降低數據庫I/O操作;
(2)分布式架構,做session共享;

2.1.2.多樣的數據結構存儲持久化數據

2.3.Redis安裝

2.3.1.下載Redis安裝包

(1)Redis官方網站:https://redis.io/
(2)Redis中文官方網站:http://www.redis.cn/
(3)下載安裝包(下面演示安裝的版本為6.2.1 for Linux)

2.3.2.安裝步驟

(1)在Linux的/opt目錄下準備好Redis的安裝包(此處使用的是CentOS7.7)

(2)安裝 C 語言的編譯環境,即下載安裝最新版的gcc編譯器(如果需要卸載舊版本的gcc,可以參考這篇文章)

# 查看gcc版本命令 gcc --version # 安裝命令 yun install gcc



安裝過程中出現的y/n選擇,一律輸入y即可。
(3)解壓安裝包

# 解壓命令 tar -zxvf redis-6.2.1.tar.gz


(4)解壓完成后進入目錄redis-6.2.1,并使用make命令進行編譯

cd redis-6.2.1/ make


(5)使用命令make install進行安裝

make install


(6)驗證Redis是否安裝成功

# /usr/local/bin為安裝目錄 cd /usr/local/bin ll

redis-benchmark性能測試工具
redis-check-aof修復有問題的AOF文件
redis-check-dump修復有問題的dump.rdb文件
redis-sentinelRedis集群使用
redis-serverRedis服務器啟動命令
redis-cli客戶端操作入口

2.4.Redis啟動

2.4.1.前臺啟動(不推薦)

# 前臺啟動命令,其缺點在于命令行窗口不能關閉,否則服務器停止 redis-server

2.4.2.后臺啟動(推薦)

(1)備份redis.conf,拷貝一份/opt/redis-6.2.1/目錄下的redis.conf到其他目錄

(2)將后臺啟動設置daemonize no 改成 yes


(3)啟動Redis

cd /usr/local/bin # 啟動Redis redis-server /etc/redis.conf # 查看Redis運行信息 ps -ef | grep redis


(4)用客戶端訪問Redis

redis-cli # 測試驗證 ping


(5)關閉Redis
① 使用命令shutdown進行關閉

shutdown


② 通過殺死Redis進程來關閉Redis

ps -ef | grep redis # Redis進程ID需要先查出來 kill -9 9498

3.常用的五大數據類型

在了解Redis常用的五大數據類型之前,需要先知道一些于Redis鍵相關的命令,此外,想查看Redis更多的命令,可以參考http://doc.redisfans.com/。

# 查看當前庫所有key((也可以使用模糊查詢,例如keys k*,即查詢以字母k開頭的鍵) keys *# 添加鍵值對 set <key> <value># 判斷某個key是否存在 exists <key># 查看key的類型 type <key># 刪除指定的key數據 del <key># 根據value選擇非阻塞刪除(僅將keys從keyspace元數據中刪除,真正的刪除會在后續異步操作) unlink <key># 為給定的key設置過期時間,單位為秒 expire <key> <sec># 查看key還有多少秒過期,-1表示永不過期,-2表示已過期 ttl <key># 切換數據庫,Redis默認有16個數據庫,類似于數組其下標從0開始,初始默認使用0號庫 select <dbid># 查看當前數據庫的key的數量 dbsize# 清空當前庫 flushdb# 清空所有庫 flushall

3.1.Redis字符串(String)

3.1.1.介紹

(1)String是Redis最基本的類型,可以理解成與Memcached一模一樣的類型,一個key對應一個value。
(2)String類型是二進制安全的,這意味著Redis的string可以包含任何數據,比如jpg圖片或者序列化的對象。
(3)String類型是Redis最基本的數據類型,一個Redis中字符串value的大小最大可以是512M。

3.1.2.常用命令

# 添加鍵值對(如果key存在,則覆蓋原來的value,如果key不存在,則添加該鍵值對) set <key> <value># 查詢對應鍵值 get <key># 將給定的<value>追加到原值的末尾 append <key> <value># 只有key不存在時,才能設置key的值 setnx <key> <value># 獲得key對應value值的長度 strlen <key># 將key中儲存的數字值增1(只能對數字值操作,如果為空,新增值為1) incr <key># 將key中儲存的數字值減1(只能對數字值操作,如果為空,新增值為-1) decr <key>#將key中儲存的數字值增減,自定義步長 incrby / decrby <key> <步長># 同時設置一個或多個key-value對 mset <key1> <value1> <key2> <value2> ......# 同時獲取一個或多個 value mget <key1> <key2> <key3> .....# 同時設置一個或多個 key-value對,當且僅當所有給定key都不存在時才能設置成功(原子性,有一個失敗則都失敗) msetnx <key1> <value1> <key2> <value2> ......# 獲得值的范圍 getrange <key> <起始位置> <結束位置># 用<value> 覆寫<key>所儲存的字符串值,從<起始位置>開始(索引從0開始) setrange <key> <起始位置> <value># 設置鍵值的同時,設置過期時間,單位為秒 setex <key> <過期時間> <value># 以新換舊,設置了新值同時獲得舊值 getset <key> <value>

3.1.3.數據結構

(1)String的數據結構為簡單動態字符串(Simple Dynamic String,縮寫為SDS),是可以修改的字符串,內部結構實現上似于Java中的ArrayList,采用預分配冗余空間的方式來減少內存的頻繁分配。

(2)如上圖中所示,內部為當前字符串實際分配的空間capacity一般要高于實際字符串長度len。當字符串長度小于1M時,擴容都是加倍現有的空間,如果超過1M,擴容時一次只會多擴1M的空間。此外,需要注意的是字符串最大長度為512M。

3.2.Redis列表(List)

3.2.1.介紹

(1)Redis 列表是簡單的字符串列表,按照插入順序排序。可以添加一個元素到列表的頭部(左邊)或者尾部(右邊),此外,Redis中的列表是單鍵多值的。
(2)列表的底層實際是個雙向鏈表,對兩端的操作性能很高,通過索引下標的操作中間的節點性能會較差。

3.2.2.常用命令

# 從左邊/右邊插入一個或多個值 lpush/rpush <key> <value1> <value2> <value3> ......# 從左邊/右邊彈出一個值 lpop/rpop <key># 按照索引下標獲得元素(從左到右),其順序與插入時相反 lrange <key> <start> <stop># 從<key1>列表右邊彈出一個值,插到<key2>列表左邊 rpoplpush <key1> <key2># 獲取列表key中的所有值(0左邊第一個,-1右邊第一個,0-1表示所有) lrange <key> 0 -1# 按照索引下標獲得元素(從左到右) lindex <key> <index># 獲得列表長度 llen <key># 在<value>的前面/后面插入<newvalue> linsert <key> before/after <value> <newvalue># 從左邊刪除n個value(從左到右) lrem <key> <n> <value># 將列表key下標為index的值替換成value lset <key> <index> <value>

3.2.3.數據結構

(1)List的數據結構為快速鏈表quickList。
(2)首先在列表元素較少的情況下會使用一塊連續的內存存儲,這個結構是ziplist,也即是壓縮列表。它將所有的元素緊挨著一起存儲,分配的是一塊連續的內存。當數據量比較多的時候才會改成quicklist。因為普通的鏈表需要的附加指針空間太大,會比較浪費空間。比如這個列表里存的只是int類型的數據,結構上還需要兩個額外的指針prev和next。

(3)Redis將鏈表和ziplist結合起來組成了quicklist。也就是將多個ziplist使用雙向指針串起來使用。這樣既滿足了快速的插入刪除性能,又不會出現太大的空間冗余。

3.3.Redis集合(Set)

3.3.1.介紹

(1)Redis集合對外提供的功能與List類似,都是一個列表的功能,但它的特殊之處在于Set是可以自動排重的,當需要存儲一個列表數據,又不希望出現重復數據時,Set是一個很好的選擇,并且set提供了判斷某個成員是否在一個set集合內的重要接口,這個也是List所不能提供的。
(2)Redis的Set是string類型的無序集合,它底層其實是一個value為null的hash表,所以添加、刪除、查找的復雜度都是O(1)。

3.3.2.常用命令

# 將一個或多個元素加入到集合<key>中,已經存在的元素將被忽略 sadd <key> <value1> <value2> ......# 取出集合<key>的所有值 smembers <key># 判斷集合<key>是否為含有該<value>值,有返回1,沒有則返回0 sismember <key> <value># 返該集合<key>中的元素個數 scard <key># 刪除集合<key>中的某個或多個元素 srem <key> <value1> <value2> ......# 隨機從集合<key>中吐出一個值 spop <key># 隨機從該集合<key>中取出n個值(不會從集合中刪除) srandmember <key> <n># 把集合中一個值從一個集合移動到另一個集合 smove <source> <destination> value# 返回兩個集合的交集元素 sinter <key1> <key2># 返回兩個集合的并集元素 sunion <key1> <key2># 返回兩個集合的差集元素(屬于key1但不屬于key2) sdiff <key1> <key2>

3.3.3.數據結構

(1)Set數據結構是dict字典,字典是用哈希表實現的。
(2)Java中HashSet的內部實現使用的是HashMap,只不過所有的value都指向同一個對象。Redis的Set結構也是一樣,它的內部也使用hash結構,所有的value都指向同一個內部值。

3.4.Redis哈希(Hash)

3.4.1.介紹

(1)Redis哈希是一個鍵值對集合。
(2)Redis哈希是一個string類型的field和value的映射表,hash特別適合用于存儲對象,它類似Java里面的Map<String,Object>。
(3)用戶ID為查找的key,存儲的value用戶對象包含姓名,年齡,生日等信息,如果用普通的key/value結構來存儲,則主要有以下2種存儲方式:
① 每次修改用戶的某個屬性需要,先反序列化改好后再序列化回去,開銷較大。

② 用戶ID數據冗余

(4)通過 key(用戶ID) + field(屬性標簽) 就可以操作對應屬性數據了,既不需要重復存儲據,也不會帶來序列化和并發修改控制的問題。

3.4.2.常用命令

# 給<key>集合中的<field>鍵賦值<value>,例如:hset user:1001 id 1 hset <key> <field> <value># 從<key>集合的<field>取出value hget <key> <field># 批量設置hash的值 hmset <key1> <field1> <value1> <field2> <value2>......# 查看哈希表<key>中,給定域field是否存在 hexists <key> <field># 列出該hash集合的所有field hkeys <key># 列出該hash集合的所有value hvals <key># 為哈希表<key>中的域field的值加上增量1/-1 hincrby <key> <field> <increment># 將哈希表key中的域field的值設置為value(當且僅當域field不存在) hsetnx <key> <field> <value>

3.4.3.數據結構

Hash類型對應的數據結構是兩種:ziplist(壓縮列表)和hashtable(哈希表)。當field-value長度較短且個數較少時,使用ziplist,否則使用hashtable。

3.5.Redis有序集合Zset(sorted set)

3.5.1.介紹

(1)Redis有序集合Zset與普通集合set非常相似,是一個沒有重復元素的字符串集合。不同之處是有序集合的每個成員都關聯了一個評分(score),這個評分(score)被用來按照從最低分到最高分的方式排序集合中的成員。而集合的成員是唯一的,但是評分可以是重復了。
(2)因為元素是有序的,所以也可以很快的根據評分(score)或者次序(position)來獲取一個范圍的元素。訪問有序集合的中間元素也是非常快的,因此能夠使用有序集合作為一個沒有重復成員的智能列表。

3.5.2.常用命令

# 將一個或多個元素及其score值加入到有序集<key>當中 zadd <key> <score1> <value1> <score2> <value2>......# 返回有序集<key>中,下標在<start>~<stop>之間的元素(帶WITHSCORES,可以讓分數一起和值返回到結果集) zrange <key> <start> <stop> [WITHSCORES] # 返回有序集<key>中,所有score值介于min和max之間(包括等于min或max)的成員,有序集成員按score值遞增(從小到大)次序排列。 zrangebyscore <key> minmax [withscores] [limit offset count]# 同上,改為從大到小排列。 zrevrangebyscore key maxmin [withscores] [limit offset count]

3.5.3.數據結構

(1)Zset是Redis提供的一個非常特別的數據結構,一方面它等價于Java的數據結構Map<String,Double>,可以給每一個元素value賦予一個權重score,另一方面它又類似于TreeSet,內部的元素會按照權重score進行排序,可以得到每個元素的名次,還可以通過score的范圍來獲取元素的列表。
(2)Zset底層使用了兩個數據結構:
① hash,其作用就是關聯元素value和權重score,保障元素value的唯一性,可以通過元素value找到相應的score值。
② 跳躍表,跳躍表的目的在于給元素value排序,根據score的范圍獲取元素列表。

4.Redis配置文件

之前自定義的配置文件目錄為:/etc/redis.conf,下面將對部分配置進行介紹。

4.1.Units 單位

配置文件開頭定義了一些基本的度量單位,Redis只支持bytes,不支持bit,并且對大小寫不敏感

4.2.INCLUDES包含

類似于jsp中的include,多實例的情況可以把公用的配置文件提取出來。

4.3.網絡相關配置

4.3.1.bind

默認配置為bind=127.0.0.1,這表示只能接受本機的訪問請求。如果不寫該配置,則表示無限制接受任何IP地址的訪問。而在生產環境中肯定要寫應用服務器的地址,且服務器是需要遠程訪問的,所以需要將其注釋掉

4.3.2.protected-mode

protected-mode,即保護模式,如果開啟了protected-mode,那么在沒有設定bind ip且沒有設密碼的情況下,Redis只允許接受本機的響應,此處需要將protected-mode設置no。

4.3.3.Port

Redis默認的端口號為6379,不需要修改。

4.3.4.tcp-backlog

設置tcp的backlog,backlog是一個連接隊列,backlog隊列總和=未完成三次握手隊列 + 已經完成三次握手隊列。在高并發環境下需要一個高backlog值來避免慢客戶端連接問題。注意Linux內核會將這個值減小到/proc/sys/net/core/somaxconn的值(128),所以需要確認增大/proc/sys/net/core/somaxconn和/proc/sys/net/ipv4/tcp_max_syn_backlog(128)兩個值來達到想要的效果。

4.3.5.timeout

一個空閑的客戶端維持多少秒會關閉,0表示關閉該功能,即永不關閉。

4.3.6.tcp-keepalive

對訪問客戶端的一種心跳檢測,每個n秒檢測一次,單位為秒,如果設置為0,則不會進行Keepalive檢測,建議設置成60。

4.4.GENERAL 通用

4.4.1.daemonize

是否為后臺進程,設置為yes(守護進程,后臺啟動)

4.4.2.pidfile

存放pid文件的位置,每個實例會產生一個不同的pid文件。

4.4.3.loglevel

指定日志記錄級別,Redis總共支持四個級別:debug、verbose、notice、warning,默認為notice。

4.4.4.logfile

日志文件名稱

4.4.5.databases

設定庫的數量默認16,默認數據庫為0,可以使用SELECT < dbid >命令在連接上指定數據庫id。

5.Redis的發布和訂閱

5.1.發布和訂閱

(1)Redis 發布訂閱 (pub/sub) 是一種消息通信模式:發送者 (pub) 發送消息,訂閱者 (sub) 接收消息。
(2)Redis 客戶端可以訂閱任意數量的頻道。
客戶端可以訂閱頻道如下圖:

當給這個頻道發布消息后,消息就會發送給訂閱的客戶端

5.2.發布訂閱命令行實現

(1)打開一個客戶端訂閱channel1

SUBSCRIBE channel1


(2)打開另一個客戶端,給channel1發布消息hello

publish channel1 hello


(3)打開第一個客戶端可以看到發送的消息

6.Redis6新數據類型

6.1.Bitmaps

6.1.1.簡介

現代計算機用二進制(位) 作為信息的基礎單位, 1個字節等于8位, 例如“abc”字符串是由3個字節組成, 但實際在計算機存儲時將其用二進制表示, “abc”分別對應的ASCII碼分別是97、 98、 99, 對應的二進制分別是01100001、 01100010和01100011,如下圖所示:

合理地使用操作位能夠有效地提高內存使用率和開發效率,不過需要注意的是:
(1)Bitmaps本身不是一種數據類型, 實際上它就是字符串(key-value) ,但是它可以對字符串的位進行操作。
(2)Bitmaps單獨提供了一套命令, 所以在Redis中使用Bitmaps和使用字符串的方法不太相同。 可以把Bitmaps想象成一個以位為單位的數組, 數組的每個單元只能存儲0和1, 數組的下標在Bitmaps中叫做偏移量。

6.1.2.常用命令

6.1.2.1.setbit

(1)格式

# 設置Bitmaps中某個偏移量的值(0或1),且偏移量(offset)從0開始 setbit <key> <offset> <value>

(2)示例
每個獨立用戶是否訪問過網站存放在Bitmaps中, 將訪問的用戶記做1, 沒有訪問的用戶記做0, 用偏移量作為用戶的id。設置鍵的第offset個位的值(從0算起) ,假設現在有20個用戶,userid=1, 6, 11, 15, 19的用戶對網站進行了訪問, 那么當前Bitmaps初始化結果如圖:


此外,需要注意以下幾點:
① 很多應用的用戶id以一個指定數字(例如10000) 開頭, 直接將用戶id和Bitmaps的偏移量對應勢必會造成一定的浪費,通常的做法是每次做setbit操作時將用戶id減去這個指定數字。
② 在第一次初始化Bitmaps時,假如偏移量非常大,那么整個初始化過程執行會比較慢,可能會造成Redis的阻塞。

6.1.2.2.getbit

(1)格式

# 獲取Bitmaps中某個偏移量的值 getbit <key> <offset>

(2)實例
獲取id=8的用戶是否在2021-01-01這天訪問過, 返回0說明沒有訪問過:

6.1.2.3.bitcount

(1)格式

# 統計字符串從start字節到end字節比特值為1的數量 bitcount <key> [start end]

(2)示例
① 計算2021-01-01這天的獨立訪問用戶數量

② start和end代表起始和結束字節數,下面操作計算用戶id在第1個字節到第3個字節之間的獨立訪問用戶數,對應的用戶id是11,15,19。

6.1.2.4.bitop

(1)格式

# bitop是一個復合操作,它可以做多個Bitmaps的and(交集)、or(并集)、not(非)、xor(異或)操作并將結果保存在destkey中 bitop and(or/not/xor) <destkey> [key…]

(2)示例
2020-11-03 日訪問網站的userid=0,1,4,9。

2020-11-04 日訪問網站的userid=1,2,5,9。

① 計算出兩天都訪問過網站的用戶數量

# unique:users:and:20201104_03為目標Bitmaps bitop and unique:users:and:20201104_03 unique:users:20201103 unique:users:20201104



② 計算出任意一天都訪問過網站的用戶數量(例如月活躍就是類似這種),可以使用or求并集

# unique:users:or:20201104_03為目標Bitmaps bitop or unique:users:or:20201104_03 unique:users:20201103 unique:users:20201104

6.1.3.Bitmaps與set對比

(1)假設網站有1億用戶,每天獨立訪問的用戶有5千萬,如果每天用集合類型和Bitmaps分別存儲活躍用戶可以得到表

數據類型每個用戶id占用空間需要存儲的用戶量全部內存量
集合類型64位5000000064位*50000000 = 400MB
Bitmaps1位1000000001位*100000000 = 12.5MB
(2)很明顯, 這種情況下使用Bitmaps能節省很多的內存空間, 尤其是隨著時間推移節省的內存還是非常可觀的。
數據類型一天一個月一年
集合類型400MB12GB144GB
Bitmaps12.5MB375MB4.5GB
(3)但Bitmaps并不是萬金油, 假如該網站每天的獨立訪問用戶很少, 例如只有10萬(大量的僵尸用戶) ,那么兩者的對比如下表所示, 很顯然這時候使用Bitmaps就不太合適了, 因為基本上大部分位都是0。
數據類型每個userid占用空間需要存儲的用戶量全部內存量
集合類型64位10000064位*100000 = 800KB
Bitmaps1位1000000001位*100000000 = 12.5MB

6.2.HyperLogLog

6.2.1.簡介

(1)在工作當中,我們經常會遇到與統計相關的功能需求,比如統計網站PV(PageView頁面訪問量),可以使用Redis的incr、incrby輕松實現。但像UV(UniqueVisitor,獨立訪客)、獨立IP數、搜索記錄數等需要去重和計數的問題如何解決?這種求集合中不重復元素個數的問題稱為基數問題。
(2)解決基數問題有很多種方案:
① 數據存儲在MySQL表中,使用distinct count計算不重復個數
② 使用Redis提供的hash、set、bitmaps等數據結構來處理
以上的方案結果精確,但隨著數據不斷增加,導致占用空間越來越大,對于非常大的數據集是不切實際的。
(3)能否能夠降低一定的精度來平衡存儲空間?Redis推出了HyperLogLog。Redis HyperLogLog 是用來做基數統計的算法,HyperLogLog 的優點是在輸入元素的數量或者體積非常非常大時,計算基數所需的空間總是固定的、并且是很小的
(4)在 Redis 里面,每個 HyperLogLog 鍵只需要花費 12 KB 內存,就可以計算接近 2^64 個不同元素的基數。這和計算基數時,元素越多耗費內存就越多的集合形成鮮明對比。但是,因為 HyperLogLog 只會根據輸入元素來計算基數,而不會儲存輸入元素本身,所以 HyperLogLog 不能像集合那樣,返回輸入的各個元素。
(5)什么是基數?比如數據集 {1, 3, 5, 7, 5, 7, 8}, 那么這個數據集的基數集為 {1, 3, 5 ,7, 8},基數(不重復元素)為5。基數估計就是在誤差可接受的范圍內,快速計算基數。

6.2.2.常用命令

6.2.2.1.pfadd

(1)格式

# 添加指定元素到HyperLogLog中 pfadd <key> <element> [element...]

(2)示例
將所有元素添加到指定HyperLogLog數據結構中,如果執行命令后HLL估計的近似基數發生變化,則返回1,否則返回0。

6.2.2.2.pfcount

(1)格式

# 計算HLL的近似基數,可以計算多個HLL,比如用HLL存儲每天的UV,計算一周的UV可以使用7天的UV合并計算即可 pfcount <key> [key ...]

(2)示例

6.2.2.3.pfmerge

(1)格式

# 將一個或多個HLL合并后的結果存儲在另一個HLL中,比如每月活躍用戶可以使用每天的活躍用戶來合并計算可得 pfmerge <destkey> <sourcekey> [sourcekey ...]

(2)示例

6.3.Geospatial

6.3.1.簡介

Redis 3.2中增加了對GEO類型的支持。GEO即Geographic,是地理信息的縮寫。該類型就是元素的二維坐標,在地圖上就是經緯度。redis基于該類型,提供了經緯度設置查詢、范圍查詢、距離查詢、經緯度Hash等常見操作。

6.3.2.常用命令

6.3.2.1.geoadd

(1)格式

# 添加地理位置(經度,緯度,名稱) geoadd <key> <longitude> <latitude> <member> [longitude latitude member...]

(2)示例

此外,需要注意以下幾點:
① 兩極無法直接添加,一般會下載城市數據,直接通過 Java 程序一次性導入。
② 有效的經度從 -180 度到 180 度。有效的緯度從 -85.05112878 度到 85.05112878 度。
③ 當坐標位置超出指定范圍時,該命令將會返回一個錯誤。
④ 已經添加的數據,是無法再次往里面添加的。

6.3.2.2.geopos

(1)格式

# 獲得指定地區的坐標值 geopos <key> <member> [member...]

(2)示例

6.3.2.3.geodist

(1)格式

# 獲取兩個位置之間的直線距離 # 單位: # m:表示單位為米[默認值] # km:表示單位為千米 # mi:表示單位為英里 # ft:表示單位為英尺 如果用戶沒有顯式地指定單位參數, 那么 GEODIST 默認使用米作為單位 geodist <key> <member1> <member2> [m|km|ft|mi ]

(2)示例

6.3.2.4.georadius

(1)格式

# 以給定的經緯度為中心,找出某一半徑內的元素 georadius <key> <longitude> <latitude> radius m|km|ft|mi

(2)示例

7.Jedis

7.1.Jedis介紹

Jedis是Redis官方推薦的Java連接開發工具。

7.2.Jedis常用操作

7.2.1.連接測試

(1)在IDEA中創建一個名為jedis_redisdemo的maven工程


(2)在pom.xml中導入相關依賴

<dependencies><!--Jedis--><dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId><version>3.2.0</version></dependency><!--單元測試--><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.12</version><scope>compile</scope></dependency> </dependencies>

(3)測試代碼如下:

package com.atguigu.jedis;import redis.clients.jedis.Jedis;public class JedisDemo1{public static void main(String[] args) {/*創建Jedis對象(1)."192.168.88.100":主機IP地址(2).6379:Redis端口號* */Jedis jedis = new Jedis("192.168.88.100",6379);//測試連接String value = jedis.ping();//輸出PONG則表示連接成功System.out.println(value);} }

(4)連接Redis的注意事項:
① 在Redis配置文件redis.conf中,要注釋掉bind 127.0.0.1,并且將protected-mode設置為no。
② 如果Linux中的防火墻處于開啟狀態,也可能連接不成功

此時關閉防火墻,命令如下:

systemctl stop firewalld

7.2.2.操作相關數據類型

(1)Jedis-API:Key

@Test public void keyDemo(){//創建Jedis對象Jedis jedis = new Jedis("192.168.88.100",6379);jedis.set("k1", "v1");jedis.set("k2", "v2");jedis.set("k3", "v3");Set<String> keys = jedis.keys("*");System.out.println(keys.size());for (String key : keys) {System.out.println(key);}System.out.println(jedis.exists("k1"));System.out.println(jedis.ttl("k1"));System.out.println(jedis.get("k1"));//關閉Jedisjedis.close(); }

(2)Jedis-API:String

@Test public void stringDemo(){//創建Jedis對象Jedis jedis = new Jedis("192.168.88.100",6379);jedis.mset("str1","v1","str2","v2","str3","v3");System.out.println(jedis.mget("str1","str2","str3"));//關閉Jedisjedis.close(); }

(3)Jedis-API:List

@Test public void listDemo(){//創建Jedis對象Jedis jedis = new Jedis("192.168.88.100",6379);jedis.lpush("mylist","v1","v2","v3","v4");List<String> list = jedis.lrange("mylist",0,-1);for (String element : list) {System.out.println(element);}//關閉Jedisjedis.close(); }

(4)Jedis-API:Set

@Test public void setDemo(){//創建Jedis對象Jedis jedis = new Jedis("192.168.88.100",6379);jedis.sadd("orders", "order01");jedis.sadd("orders", "order02");jedis.sadd("orders", "order03");jedis.sadd("orders", "order04");Set<String> smembers = jedis.smembers("orders");jedis.srem("orders", "order02");for (String order : smembers) {System.out.println(order);}//關閉Jedisjedis.close(); }

(5)Jedis-API:Hash

@Test public void hashDemo(){//創建Jedis對象Jedis jedis = new Jedis("192.168.88.100",6379);jedis.hset("hash1","userName","lisi");System.out.println(jedis.hget("hash1","userName"));Map<String,String> map = new HashMap<String,String>();map.put("telphone","13810169999");map.put("address","atguigu");map.put("email","abc@163.com");jedis.hmset("hash2",map);List<String> result = jedis.hmget("hash2", "telphone","email");for (String element : result) {System.out.println(element);}//關閉Jedisjedis.close(); }

(6)Jedis-API:Zset

@Test public void zsetDemo(){//創建Jedis對象Jedis jedis = new Jedis("192.168.88.100",6379);jedis.zadd("zset01", 100d, "z3");jedis.zadd("zset01", 90d, "l4");jedis.zadd("zset01", 80d, "w5");jedis.zadd("zset01", 70d, "z6");Set<String> zrange = jedis.zrange("zset01", 0, -1);for (String e : zrange) {System.out.println(e);}//關閉Jedisjedis.close(); }

7.3.Jedis實例——手機驗證碼

7.3.1.功能需求

(1)輸入手機號,點擊發送后隨機生成6位數字碼,2分鐘內有效;
(2)輸入驗證碼,點擊驗證,返回成功或失敗;
(3)每個手機號每天只能輸入3次;

7.3.2.功能實現

package com.atguigu.jedis;import org.junit.Test; import redis.clients.jedis.Jedis;import java.util.Random;public class PhoneCode {//模擬驗證碼發送@Testpublic void testVerifyCode(){//模擬向手機號為139731795的手機發送驗證碼sendCode("139731795");}//模擬驗證碼校驗@Testpublic void testSendCode(){//當向手機號為139731795的手機發送驗證碼后,可在Redis中查詢其驗證碼,并帶回verifyCode()中進行驗證verifyCode("139731795","609706");}//1.隨機生成6位數字驗證碼public String getCode(){String code = "";Random random = new Random();for(int i=-0;i<6;i++){//nextInt(int num):隨機返回一個值在[0,num)的int類型的整數int rand = random.nextInt(10);//將生成的數字拼接到code尾部code+=rand;}return code;}//2.每個手機每天只能發送三次,驗證碼放到redis中,設置過期時間120spublic void sendCode(String phone) {//連接redisJedis jedis = new Jedis("192.168.88.100",6379);//拼接key//手機發送次數keyString countKey = "VerifyCode"+phone+":count";//驗證碼keyString codeKey = "VerifyCode"+phone+":code";//每個手機每天只能發送三次String count = jedis.get(countKey);if(count == null) {//沒有發送次數,第一次發送//設置發送次數是1jedis.setex(countKey,24*60*60,"1");} else if(Integer.parseInt(count)<=2) {//發送次數+1jedis.incr(countKey);} else if(Integer.parseInt(count)>2) {//發送三次,不能再發送System.out.println("今天發送次數已經超過三次");jedis.close();return;}//發送驗證碼放到redis里面String vcode = getCode();jedis.setex(codeKey,120,vcode);//關閉Jedisjedis.close();}//3.校驗驗證碼public void verifyCode(String phone,String code) {//從redis獲取驗證碼Jedis jedis = new Jedis("192.168.88.100",6379);//驗證碼keyString codeKey = "VerifyCode"+phone+":code";String redisCode = jedis.get(codeKey);//判斷if(redisCode.equals(code)) {System.out.println("成功");}else{System.out.println("失敗");}//關閉Jedisjedis.close();} }

8.Redis與SpringBoot整合

8.1.整合步驟

(1)在IDEA中創建一個SpringBoot工程



(2)在pom.xml中導入相關依賴

<!-- redis --> <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- spring2.X集成redis所需common-pool2--> <dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId><version>2.6.0</version> </dependency>

(3)在SpringBoot全局配置文件application.properties中進行相關配置

# Redis服務器地址(根據實際情況進行配置) spring.redis.host=192.168.88.100 # Redis服務器連接端口 spring.redis.port=6379 # Redis數據庫索引(默認為0) spring.redis.database= 0 # 連接超時時間(毫秒) spring.redis.timeout=1800000 # 連接池最大連接數(使用負值表示沒有限制) spring.redis.lettuce.pool.max-active=20 # 最大阻塞等待時間(負數表示沒限制) spring.redis.lettuce.pool.max-wait=-1 # 連接池中的最大空閑連接 spring.redis.lettuce.pool.max-idle=5 # 連接池中的最小空閑連接 spring.redis.lettuce.pool.min-idle=0

(4)創建配置類RedisConfig(寫法較為固定)

package com.atguigu.redis_springboot.config;import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.CachingConfigurerSupport; import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.cache.RedisCacheConfiguration; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.RedisSerializationContext; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer;import java.time.Duration;@EnableCaching @Configuration public class RedisConfig extends CachingConfigurerSupport {@Beanpublic RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {RedisTemplate<String, Object> template = new RedisTemplate<>();RedisSerializer<String> redisSerializer = new StringRedisSerializer();Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);ObjectMapper om = new ObjectMapper();om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);jackson2JsonRedisSerializer.setObjectMapper(om);template.setConnectionFactory(factory);//key序列化方式template.setKeySerializer(redisSerializer);//value序列化template.setValueSerializer(jackson2JsonRedisSerializer);//value hashmap序列化template.setHashValueSerializer(jackson2JsonRedisSerializer);return template;}@Beanpublic CacheManager cacheManager(RedisConnectionFactory factory) {RedisSerializer<String> redisSerializer = new StringRedisSerializer();Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);//解決查詢緩存轉換異常的問題ObjectMapper om = new ObjectMapper();om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);jackson2JsonRedisSerializer.setObjectMapper(om);// 配置序列化(解決亂碼的問題),過期時間600秒RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofSeconds(600)).serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer)).serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer)).disableCachingNullValues();RedisCacheManager cacheManager = RedisCacheManager.builder(factory).cacheDefaults(config).build();return cacheManager;} }

8.2.測試

(1)編寫控制器方法進行測試

package com.atguigu.redis_springboot.controller;import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;@RestController @RequestMapping("/redisTest") public class RedisController {@Autowiredprivate RedisTemplate redisTemplate;@GetMapping()public String testRedis(){//向Redis中添加鍵值對redisTemplate.opsForValue().set("name","lucy");String name = (String)redisTemplate.opsForValue().get("name");return name;} }

(2)在SpringBoot啟動類中啟動SpringBoot,然后在瀏覽器地址欄輸入http://localhost:8080/redisTest進行測試,結果如下:

9.Redis事務

9.1.Redis事務簡介

(1)Redis事務定義
Redis事務是一個單獨的隔離操作:事務中的所有命令都會序列化、按順序地執行。事務在執行的過程中,不會被其他客戶端發送來的命令請求所打斷。Redis事務的主要作用就是串聯多個命令防止別的命令插隊。
(2)Redis事務的三特性
① 單獨的隔離操作:事務中的所有命令都會序列化、按順序地執行。事務在執行的過程中,不會被其他客戶端發送來的命令請求所打斷。
② 沒有隔離級別的概念:隊列中的命令沒有提交之前都不會實際被執行,因為事務提交前任何指令都不會被實際執行。
③ 不保證原子性:事務中如果有一條命令執行失敗,其后的命令仍然會被執行,沒有回滾操作。

9.2.Redis事務相關命令

(1)Redis事務相關命令有multiexecdiscard
(2)從輸入Multi命令開始,輸入的命令都會依次進入命令隊列中,但不會執行,直到輸入Exec后,Redis會將之前的命令隊列中的命令依次執行。組隊的過程可以通過discard來放棄組隊。

(3)下面看一下案例

9.3.事務的錯誤處理

Redis事務的錯誤處理一般分為以下兩種情況:
(1)組隊中某個命令出現了報告錯誤,執行時整個的所有隊列都會被取消。


(2)執行階段某個命令報出了錯誤,則只有報錯的命令不會被執行,而其他的命令都會執行,不會回滾。

9.4.事務沖突問題

9.4.1.例子

場景:有2個人有你的賬戶(賬戶中有10000元),你們3人同時去參加雙十一搶購,并且每人分別準備花費8000元、5000元、1000元,如果對該過程不加控制,則會出現以下情況(假設):

9.4.2.悲觀鎖

悲觀鎖(Pessimistic Lock),顧名思義,就是很悲觀,每次去拿數據的時候都認為別人會修改,所以每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會block直到它拿到鎖。傳統的關系型數據庫里邊就用到了很多這種鎖機制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖。

9.4.3.樂觀鎖

(1)樂觀鎖(Optimistic Lock),顧名思義,就是很樂觀,每次去拿數據的時候都認為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,可以使用版本號等機制。樂觀鎖適用于多讀的應用類型,這樣可以提高吞吐量。Redis就是利用這種check-and-set機制實現事務的。

(2)下面演示樂觀鎖

# 對一個或多個key進行監視,如果在事務執行之前被監視的key被其他命令所修改,那么事務將被打斷 watch key [key...] # 取消watch命令對所有key的監視,如果在執行watch命令之后,exec命令或discard命令先被執行了的話,那么就不需要再執行unwatch命令了 unwatch

10.Redis事務——秒殺案例

10.1.簡單秒殺

前端秒殺頁面

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><title>Insert title here</title></head> <body><h1>iPhone 13 Pro !!! 1元秒殺!!!</h1><form id="msform" action="${pageContext.request.contextPath}/doseckill" enctype="application/x-www-form-urlencoded"><input type="hidden" id="prodid" name="prodid" value="0101"><input type="button" id="miaosha_btn" name="seckill_btn" value="秒殺點我"/></form></body><script type="text/javascript" src="${pageContext.request.contextPath}/script/jquery/jquery-3.1.0.js"></script><script type="text/javascript">$(function(){$("#miaosha_btn").click(function(){var url=$("#msform").attr("action");$.post(url,$("#msform").serialize(),function(data){if(data=="false"){alert("搶光了" );$("#miaosha_btn").attr("disabled",true);}} );})})</script> </html>

后端核心代碼

package com.atguigu;import java.io.IOException;import redis.clients.jedis.Jedis;public class SecKill_redis {//秒殺過程public static boolean doSecKill(String uid,String prodid) throws IOException {//1.uid和prodid非空判斷if(uid == null || prodid == null) {return false;}//2.連接redisJedis jedis = new Jedis("192.168.88.100",6379);//3.拼接key//3.1 庫存keyString kcKey = "sk:"+prodid+":qt";//3.2 秒殺成功用戶keyString userKey = "sk:"+prodid+":user";//4.獲取庫存,如果庫存null,秒殺還沒有開始String kc = jedis.get(kcKey);if(kc == null) {System.out.println("秒殺還沒有開始,請等待");jedis.close();return false;}// 5.判斷用戶是否重復秒殺操作if(jedis.sismember(userKey, uid)) {System.out.println("已經秒殺成功了,不能重復秒殺");jedis.close();return false;}//6.判斷如果商品數量,庫存數量小于1,秒殺結束if(Integer.parseInt(kc)<=0) {System.out.println("秒殺已經結束了");jedis.close();return false;}//7.秒殺過程//7.1 庫存-1jedis.decr(kcKey);//7.2 把秒殺成功用戶添加清單里面jedis.sadd(userKey,uid);System.out.println("秒殺成功了..");jedis.close();return true;} }

在Redis中將庫存設置為10

set sk:0101:qt 10

測試結果如下:


10.2.秒殺并發模擬

10.2.1.安裝工具ab

# 安裝工具ab yum install httpd-tools # 查看幫助手冊 ab --help


10.2.2.使用ab進行測試

# 測試命令(需要提前在/opt目錄下準備好postfile文件,里面的內容是"prodid=0101&",模擬表單提交參數,以&符號結尾) ab -n 1000 -c 100 -p ~/opt/postfile -T application/x-www-form-urlencoded http://192.168.88.1:8080/Seckill/doseckill



此次并發模擬至少出現了以下兩種錯誤現象:


需要注意的是,在使用ab工具進行模擬時,可能會出現連接失敗或超時的現象,其主要可能有以下幾點:
① windows中的防火墻功能處于開啟狀態,可能會攔截Linux的請求;
② 模擬的請求數量過多,從而導致超時(配置Jedis連接池可以在一定程度上解決該問題);

10.2.3.配置Jedis連接池

(1)連接池工具類的代碼如下:

package com.atguigu;import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPoolConfig;//節省每次連接redis服務帶來的消耗,把連接好的實例反復利用 public class JedisPoolUtil {private static volatile JedisPool jedisPool = null;private JedisPoolUtil() {}//采用單例模式public static JedisPool getJedisPoolInstance() {if (null == jedisPool) {synchronized (JedisPoolUtil.class) {if (null == jedisPool) {JedisPoolConfig poolConfig = new JedisPoolConfig();poolConfig.setMaxTotal(200);poolConfig.setMaxIdle(32);poolConfig.setMaxWaitMillis(100*1000);poolConfig.setBlockWhenExhausted(true);poolConfig.setTestOnBorrow(true); // ping PONGjedisPool = new JedisPool(poolConfig, "192.168.88.100", 6379, 60000 );}}}return jedisPool;}public static void release(JedisPool jedisPool, Jedis jedis) {if (null != jedis) {jedisPool.returnResource(jedis);}} }

連接池參數如下:

MaxTotal控制一個pool可分配多少個jedis實例,通過pool.getResource()來獲取;如果賦值為-1,則表示不限制;如果pool已經分配了MaxTotal個jedis實例,則此時pool的狀態為exhausted
maxIdle控制一個pool最多有多少個狀態為idle(空閑)的jedis實例
MaxWaitMillis表示當borrow一個jedis實例時,最大的等待毫秒數,如果超過等待時間,則直接拋JedisConnectionException
testOnBorrow獲得一個jedis實例的時候是否檢查連接可用性(ping());如果為true,則得到的jedis實例均是可用的
(2)修改后端連接Redis的代碼如下:
//2.連接redis //Jedis jedis = new Jedis("192.168.88.100",6379); //通過連接池得到jedis對象 JedisPool jedisPoolInstance = JedisPoolUtil.getJedisPoolInstance(); Jedis jedis = jedisPoolInstance.getResource();

10.3.解決超賣問題


(1)這里可以考慮使用前面講到的樂觀鎖來解決超賣問題。

使用樂觀鎖后的代碼如下:

package com.atguigu;import java.io.IOException; import java.util.HashSet; import java.util.List; import java.util.Set;import org.apache.commons.pool2.impl.GenericObjectPoolConfig; import org.slf4j.LoggerFactory;import ch.qos.logback.core.rolling.helper.IntegerTokenConverter; import redis.clients.jedis.HostAndPort; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisCluster; import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPoolConfig; import redis.clients.jedis.ShardedJedisPool; import redis.clients.jedis.Transaction;public class SecKill_redis {public static void main(String[] args) {Jedis jedis =new Jedis("192.168.44.168",6379);System.out.println(jedis.ping());jedis.close();}//秒殺過程public static boolean doSecKill(String uid,String prodid) throws IOException {//1.uid和prodid非空判斷if(uid == null || prodid == null) {return false;}//2.連接redis//Jedis jedis = new Jedis("192.168.44.168",6379);//通過連接池得到jedis對象JedisPool jedisPoolInstance = JedisPoolUtil.getJedisPoolInstance();Jedis jedis = jedisPoolInstance.getResource();//3.拼接key//3.1.庫存keyString kcKey = "sk:"+prodid+":qt";//3.2.秒殺成功用戶keyString userKey = "sk:"+prodid+":user";//監視庫存jedis.watch(kcKey);//4.獲取庫存,如果庫存null,秒殺還沒有開始String kc = jedis.get(kcKey);if(kc == null) {System.out.println("秒殺還沒有開始,請等待");jedis.close();return false;}//5.判斷用戶是否重復秒殺操作if(jedis.sismember(userKey, uid)) {System.out.println("已經秒殺成功了,不能重復秒殺");jedis.close();return false;}//6.判斷如果商品數量,庫存數量小于1,秒殺結束if(Integer.parseInt(kc)<=0) {System.out.println("秒殺已經結束了");jedis.close();return false;}//7.秒殺過程//使用事務Transaction multi = jedis.multi();//組隊操作multi.decr(kcKey);multi.sadd(userKey,uid);//執行List<Object> results = multi.exec();if(results == null || results.size()==0) {System.out.println("秒殺失敗了....");jedis.close();return false;}//7.1 庫存-1//jedis.decr(kcKey);//7.2 把秒殺成功用戶添加清單里面//jedis.sadd(userKey,uid);System.out.println("秒殺成功了..");jedis.close();return true;} }

10.4.解決庫存遺留問題

10.4.1.問題演示

(1)之前的庫存都是設置為10,現在將庫存設置為500。

flushdb set sk:0101:qt 500

(2)再次進行測試

# 測試命令 ab -n 2000 -c 300 -p ~/opt/postfile -T application/x-www-form-urlencoded http://192.168.88.1:8080/Seckill/doseckill



(3)庫存遺留問題是由樂觀鎖導致的,因為

10.4.2.問題解決

(1)Lua腳本
Lua 是一種輕量小巧的腳本語言,用標準C語言編寫并以源代碼形式開放, 其設計目的是為了嵌入應用程序中,從而為應用程序提供靈活的擴展和定制功能。
(2)Lua腳本在Redis中的優勢:
① 將復雜的或者多步的redis操作,寫為一個腳本,一次提交給redis執行,減少反復連接redis的次數,提升性能。
② LUA腳本是類似redis事務,有一定的原子性,不會被其他命令插隊,可以完成一些redis事務性的操作。
③ 利用Lua腳本淘汰用戶,解決超賣問題。
④ Redis2.6版本以后,通過Lua腳本解決爭搶問題,實際上是Redis 利用其單線程的特性,用任務隊列的方式解決多任務并發問題。

(3)將Lua對應腳本嵌入到java代碼中:

package com.atguigu;import java.io.IOException; import java.util.HashSet; import java.util.List; import java.util.Set;import org.apache.commons.pool2.impl.GenericObjectPoolConfig; import org.slf4j.LoggerFactory;import ch.qos.logback.core.joran.conditional.ElseAction; import redis.clients.jedis.HostAndPort; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisCluster; import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPoolConfig; import redis.clients.jedis.ShardedJedisPool; import redis.clients.jedis.Transaction;public class SecKill_redisByScript {private static final org.slf4j.Logger logger =LoggerFactory.getLogger(SecKill_redisByScript.class) ;public static void main(String[] args) {JedisPool jedispool = JedisPoolUtil.getJedisPoolInstance();Jedis jedis=jedispool.getResource();System.out.println(jedis.ping());Set<HostAndPort> set=new HashSet<HostAndPort>();// doSecKill("201","sk:0101");}static String secKillScript ="local userid=KEYS[1];\r\n" + "local prodid=KEYS[2];\r\n" + "local qtkey='sk:'..prodid..\":qt\";\r\n" + "local usersKey='sk:'..prodid..\":usr\";\r\n" + "local userExists=redis.call(\"sismember\",usersKey,userid);\r\n" + "if tonumber(userExists)==1 then \r\n" + " return 2;\r\n" + "end\r\n" + "local num= redis.call(\"get\" ,qtkey);\r\n" + "if tonumber(num)<=0 then \r\n" + " return 0;\r\n" + "else \r\n" + " redis.call(\"decr\",qtkey);\r\n" + " redis.call(\"sadd\",usersKey,userid);\r\n" + "end\r\n" + "return 1" ;static String secKillScript2 = "local userExists=redis.call(\"sismember\",\"{sk}:0101:usr\",userid);\r\n" +" return 1";public static boolean doSecKill(String uid,String prodid) throws IOException {JedisPool jedispool = JedisPoolUtil.getJedisPoolInstance();Jedis jedis=jedispool.getResource();//String sha1= .secKillScript;String sha1= jedis.scriptLoad(secKillScript);Object result= jedis.evalsha(sha1, 2, uid,prodid);String reString=String.valueOf(result);if ("0".equals( reString ) ) {System.err.println("已搶空!!");}else if("1".equals( reString ) ) {System.out.println("搶購成功!!!!");}else if("2".equals( reString ) ) {System.err.println("該用戶已搶過!!");}else{System.err.println("搶購異常!!");}jedis.close();return true;} }

11.Redis持久化——RDB

11.1.RDB介紹

(1)RDB(Redis DataBase),即在指定的時間間隔內將內存中的數據集快照寫入磁盤, 也就是行話講的Snapshot快照,它恢復時是將快照文件直接讀到內存里。其它的具體細節可以查看中文官網:http://www.redis.cn/topics/persistence.html。

(2)Redis會單獨創建(fork)一個子進程來進行持久化,會先將數據寫入到一個臨時文件中,待持久化過程都結束了,再用這個臨時文件替換上次持久化好的文件。 整個過程中,主進程是不進行任何I/O操作的,這就確保了極高的性能 如果需要進行大規模數據的恢復,且對于數據恢復的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。RDB的缺點是最后一次持久化后的數據可能丟失。

(3)RDB的優點如下:
① 適合大規模的數據恢復
② 對數據完整性和一致性要求不高更適合使用
③ 節省磁盤空間
④ 恢復速度快

(4)RDB的缺點如下:
① fork()的時候,內存中的數據被克隆了一份,大致2倍的膨脹性需要考慮
② 雖然Redis在fork()時使用了寫時拷貝技術,但是如果數據龐大時還是比較消耗性能。
③ 在備份周期在一定間隔時間做一次備份,所以如果Redis出現異常不能工作的話,就會丟失最后一次快照后的所有修改。

(5)RDB默認是開啟的,動態停止RDB的命令為:

# save后給空值,表示禁用保存策略 redis-cli config set save ""

(6)小總結

11.2.RDB持久化流程

11.3.fork()

(1)fork()的作用是復制一個與當前進程一樣的進程。新進程的所有數據(變量、環境變量、程序計數器等) 數值都和原進程一致,但是是一個全新的進程,并作為原進程的子進程。
(2)在Linux程序中,fork()會產生一個和父進程完全相同的子進程,但子進程在此后多會exec系統調用,出于效率考慮,Linux中引入了“寫時復制技術”。
(3)一般情況父進程和子進程會共用同一段物理內存,只有進程空間的各段的內容要發生變化時,才會將父進程的內容復制一份給子進程。

11.4.RDB相關配置

11.4.1.dump.rdb快照文件

(1)dump.rdb是由Redis服務器自動生成的,在默認情況下,每隔一段時間Redis服務器程序會自動對數據庫做一次遍歷,把內存快照寫在一個叫做“dump.rdb”的文件里,該文件名稱可以在Redis配置文件redis.conf中進行查看和配置。

(2)dump.rdb文件的保存路徑默認為Redis啟動時命令行所在的目錄下。

11.4.2.快照配置

(1)快照保存策略

(2)stop-writes-on-bgsave-error
當Redis無法寫入磁盤的話,直接關掉Redis的寫操作,推薦設置為yes。

(3)rdbcompression——壓縮文件
對于存儲到磁盤中的快照,可以設置是否進行壓縮存儲。如果是的話,redis會采用LZF算法進行壓縮。如果不想消耗CPU來進行壓縮的話,可以設置為關閉此功能,推薦設置為yes。

(4)dbchecksum——檢查完整性
在存儲快照后,還可以讓redis使用CRC64算法來進行數據校驗,但是這樣做會增加大約10%的性能消耗,如果希望獲取到最大的性能提升,可以關閉此功能,推薦設置為yes。

11.4.3.快照配置命令

(1)save
只管保存,其它不管。當出現全部阻塞時,需要手動保存,不建議使用。
(2)gbsave
Redis會在后臺異步進行快照操作,同時還可以響應客戶端請求。
(3)lastsave
獲取最后一次成功執行快照的時間。

12.Redis持久化——AOF

12.1.AOF介紹

(1)AOF(Append Only File),即以日志的形式來記錄每個寫操作(增量保存),將Redis執行過的所有寫指令記錄下來(讀操作不記錄), 只許追加文件但不可以改寫文件,Redis啟動之初會讀取該文件重新構建數據,換言之,Redis 重啟的話就根據日志文件的內容將寫指令從前到后執行一次以完成數據的恢復工作。
(2)AOF默認不開啟,需要手動開啟(在配置文件redis.conf中將"appendonly no"中的"no"改為"yes"即可),此外AOF文件名默認為appendonly.aof,其保存路徑與RDB文件的路徑一致。
(3)當AOF和RDB同時開啟時,系統默認取AOF的數據(數據不會存在丟失)


(4)AOF的優點如下:
① 備份機制更穩健,丟失數據概率更低。
② 可讀的日志文本,通過操作AOF穩健,可以處理誤操作。

(5)AOF的缺點如下:
① 比起RDB占用更多的磁盤空間。
② 恢復備份速度要慢。
③ 每次讀寫都同步的話,有一定的性能壓力。
④ 存在個別不能Bug恢復的情況。

(6)小總結

12.2.AOF持久化流程

(1)客戶端的請求寫命令會被append追加到AOF緩沖區內;
(2)AOF緩沖區根據AOF持久化策略[always、everysec、no]將操作sync同步到磁盤的AOF文件中;
(3)AOF文件大小超過重寫策略或手動重寫時,會對AOF文件rewrite重寫,壓縮AOF文件容量;
(4)Redis服務重啟時,會重新load加載AOF文件中的寫操作達到數據恢復的目的;

12.3.AOF恢復

(1)AOF的備份機制和性能雖然和RDB不同,但是備份和恢復的操作同RDB一樣,都是拷貝備份文件,需要恢復時再拷貝到Redis工作目錄下,啟動系統即加載。
(2)正常恢復
① 將有數據的aof文件復制一份保存到對應目錄(查看目錄:config get dir)
② 重啟Redis,然后重新加載即可正常恢復
(3)異常恢復
① 如果遇到AOF文件損壞,通過/usr/local/bin/redis-check-aof–fix appendonly.aof進行恢復
② 備份被寫壞的AOF文件
③ 重啟Redis,然后重新加載即可正常恢復

12.4.AOF與RDB的選擇

(1)官方推薦兩個都啟用。
(2)如果對數據不敏感,可以選單獨用RDB。
(3)不建議單獨用 AOF,因為可能會出現Bug。
(4)如果只是做純內存緩存,可以都不用。

13.Redis主從復制

13.1.概述

(1)主從復制:主機數據更新后根據配置和策略, 自動同步到備機的master/slaver機制,其中,Master以寫為主,Slave以讀為主。
(2)功能:
① 讀寫分離,性能擴展;
② 容災快速恢復;

13.2.實現

下面實現的是一主兩從的情況,一主三從等其它情況可以參考下面的步驟自行實現。
(1)創建/myredis文件夾

cd /myredis

(2)將/etc目錄下的Redis配置文件redis.con復制到/myredis目錄下

cp /etc/redis.conf /myredis/redis.conf

(3)關閉/myredis/redis.conf中的AOF,即將 appendonly 設置為no

(4)配置一主兩從,創建三個配置文件(redis6379.conf、redis6380.conf、redis6381.conf)

# 在/myredis目錄下新建redis6379.conf vim redis6379.conf# 然后在redis6379.conf中寫入以下內容 include /myredis/redis.conf pidfile /var/run/redis_6379.pid port 6379 dbfilename dump6379.rdb# 為了方便起見,直接將redis6379.conf復制兩份,并分別改名為redis6380.conf和redis6381.conf,并且修改其中的內容 include /myredis/redis.conf pidfile /var/run/redis_6380.pid port 6380 dbfilename dump6380.rdbinclude /myredis/redis.conf pidfile /var/run/redis_6381.pid port 6381 dbfilename dump6381.rdb



(5)啟動三臺Redis服務器,并查看其運行情況

redis-server redis6379.conf redis-server redis6380.conf redis-server redis6381.confredis-cli -p 端口號 info replication





(6)配從(庫)不配主(庫)

# 當前服務器成為某個實例的從服務器 slaveof <ip> <port># 在6380和6381上執行下面命令(127.0.0.1為主服務器的ip,6379為其端口號) slaveof 127.0.0.1 6379



再次查看主機運行情況,此時發現,端口號為6379的主機已經有了兩臺從機(6380和6381)

(7)測試
① 在主機上寫數據,然后在從機上可以查看
② 在從機上不能寫數據

13.3.復制原理

(1)主從復制過程
① 當從連接上主服務器之后,從服務器向主服務發送進行數據同步消息;
② 主服務器接到從服務器發送過來同步消息,把主服務器數據進行持久化,把新的rdb文件發送從服務器,從服務器拿到rdb進行讀取;
③ 每次主服務器進行寫操作之后,和從服務器進行數據同步。

(2)復制延遲
由于所有的寫操作都是先在master上操作,然后同步更新到slave上,所以從master同步到slave機器有一定的延遲,當系統很繁忙的時候,延遲問題會更加嚴重,slave機器數量的增加也會使這個問題更加嚴重。

13.4.三種特殊情況

13.4.1.一主二仆

(1)主機如果宕機,重啟后會自動恢復到之前的狀態,不需要再做其它任何的修改,當再次寫數據時,從機仍然可以讀取到數據。
(2)從機如果宕機,再次重啟后會斷開其與之前主機的聯系,自己成為一臺獨立的主機,此時需要使用 slaveof 命令再次聲明所屬主機,聲明之后可以再次讀取數據。

13.4.2.薪火相傳

上一個slave可以是下一個slave的master,slave同樣可以接收其他 slaves的連接和同步請求,那么該slave作為了鏈條中下一個的master,可以有效減輕master的寫壓力,去中心化降低風險。但缺點也是非常明顯的,一旦某個從機宕機,后面的從機都無法備份。

13.4.3.反客為主

當一個master宕機后,后面的slave可以立刻升為master,其后面的slave不用做任何修改。使用命令 slaveof no one 可以將從機變為主機。

13.5.哨兵模式

13.5.1.介紹

哨兵模式可以理解為反客為主的自動版,能夠后臺監控主機是否故障,如果故障了根據投票數自動將從庫轉換為主庫。

13.5.2.使用步驟

(1)在/myredis目錄下新建sentinel.conf文件(名字不能錯)

cd /myreddis vim sentinel.conf# 在sentinel.conf中配置哨兵 sentinel monitor mymaster 127.0.0.1 6379 1 # 其中mymaster為監控對象起的服務器名稱,1為至少有多少個哨兵同意遷移的數量


(2)啟動哨兵

# 啟動命令 redis-sentinel /myredis/sentinel.conf



(3)測試
① 模擬主機(端口號為6379)宕機,即使用命令shutdown關閉主機。
② 稍等片刻,觀察哨兵打印的信息:

③ 觀察從機6381的運行信息,此時其主機從6379變為6380了

④ 當再次重啟6379時,它會變成6380的從機

13.5.4.故障恢復


(1)優先級在配置文件redis.conf中默認為replica-priority 100,值越小優先級越高;
(2)偏移量是指對原主機數據同步多少的度量;
(3)每個redis實例啟動后都會隨機生成一個40位的runid;
此外,在Java代碼中識別主從的示例如下:

private static JedisSentinelPool jedisSentinelPool=null;public static Jedis getJedisFromSentinel(){if(jedisSentinelPool==null){Set<String> sentinelSet=new HashSet<>();sentinelSet.add("192.168.11.103:26379");JedisPoolConfig jedisPoolConfig =new JedisPoolConfig();jedisPoolConfig.setMaxTotal(10); //最大可用連接數jedisPoolConfig.setMaxIdle(5); //最大閑置連接數jedisPoolConfig.setMinIdle(5); //最小閑置連接數jedisPoolConfig.setBlockWhenExhausted(true); //連接耗盡是否等待jedisPoolConfig.setMaxWaitMillis(2000); //等待時間jedisPoolConfig.setTestOnBorrow(true); //取連接的時候進行一下測試 ping pongjedisSentinelPool=new JedisSentinelPool("mymaster",sentinelSet,jedisPoolConfig);return jedisSentinelPool.getResource();}else{return jedisSentinelPool.getResource();} }

14.Redis集群

14.1.概述

(1)問題引入
當Redis容量不夠時,如何進行擴容?對于并發寫操作, Redis如何分攤?另外,主從模式,薪火相傳模式,主機宕機,導致ip地址發生變化,應用程序中配置需要修改對應的主機地址、端口等信息。之前通過代理主機來解決,但是redis3.0中提供了解決方案。就是無中心化集群配置
(2)集群定義
① Redis 集群就是實現了對 Redis 的水平擴容,即啟動N個Redis節點,將整個數據庫分布存儲在這N個節點中,每個節點存儲總數據的1/N。
② Redis 集群通過分區(partition)來提供一定程度的可用性(availability),即使集群中有一部分節點失效或者無法進行通訊, 集群也可以繼續處理命令請求。
(3)集群優缺點
優點: 實現擴容、分攤壓力、無中心配置相對簡單
缺點:
① 多鍵操作實現起來比較復雜;
② 多鍵的Redis事務是不被支持的;
③ Lua腳本也不被支持;
④ 由于集群方案出現較晚,很多公司已經采用了其他的集群方案,而代理或者客戶端分片的方案想要遷移至redis cluster,需要整體遷移而不是逐步過渡,復雜度較大;

14.2.搭建集群

注:由于實際情況的限制,下面的搭建只是一個模擬效果!
(1)使用上面主從復制的目錄/myredis,先刪除該目錄下所有的rdb文件(防止對后面的步驟產生干擾)

# 刪除當前目錄下所有以dump63開頭的文件 rm -rf dump63*


(2)修改redis6379.conf中的內容

# 將redis6379.conf中的內容改成下面的(注釋不用寫) include /myredis/redis.conf pidfile "/var/run/redis_6379.pid" port 6379 dbfilename "dump6379.rdb"cluster-enabled yes # 打開集群模式 cluster-config-file nodes-6379.conf # 設定節點配置文件名 cluster-node-timeout 15000 # 設定節點失聯時間,超過該時間(毫秒),集群自動進行主從切換。


(3)刪除redis6380.conf和redis6381.conf

rm -rf redis6380.conf rm -rf redis6381.conf


(4)將redis6379.conf文件復制5份到當前目錄,分別取名為redis6380.conf、redis6381.conf、redis6389.conf、redis6390.conf、redis6391.conf

(5)修改上面新復制的5份文件,這里以redis6380.conf舉例,將該文件內容中的6379批量改為6380,具體命令為 %s/6379/6380 (其余的4份文件按照這個規則進行修改即可)

%s/6379/6380 %s/6379/6381 %s/6379/6389 %s/6379/6390 %s/6379/6391



(6)啟動這6個redis服務

redis-server redis6379.conf redis-server redis6380.conf redis-server redis6381.conf redis-server redis6389.conf redis-server redis6390.conf redis-server redis6391.conf


(7)將這6個節點合成一個集群
① 在組合之前,請確保所有redis實例啟動后,nodes-xxxx.conf文件都生成正常。

② 進入到/opt/redis-6.2.1/src/目錄

cd /opt/redis-6.2.1/src/

在/opt/redis-6.2.1/src/目錄下執行以下命令

# --replicas 1:采用最簡單的方式配置集群,一臺主機,一臺從機,正好三組。 # 192.168.88.100為本機的IP地址 redis-cli --cluster create --cluster-replicas 1 192.168.88.100:6379 192.168.88.100:6380 192.168.88.100:6381 192.168.88.100:6389 192.168.88.100:6390 192.168.88.100:6391



(8)測試

# 采用集群策略連接(-c),端口號可以是6379、6380、6381中的任何一個 redis-cli -c -p 6379 # 查看集群信息 cluster nodes

14.3.集群操作

(1)Redis Cluster 如何分配這六個節點?
一個集群至少要有三個主節點,選項 --cluster-replicas 1 表示希望為集群中的每個主節點創建一個從節點。分配原則盡量保證每個主數據庫運行在不同的IP地址,每個從庫和主庫不在一個IP地址上。
(2)什么是slots?

① 一個 Redis 集群包含 16384 個插槽(hash slot),數據庫中的每個鍵都屬于這 16384 個插槽的其中一個。集群使用公式 CRC16(key) % 16384 來計算鍵 key 屬于哪個槽, 其中CRC16(key) 語句用于計算鍵 key 的 CRC16 校驗和 。
② 集群中的每個節點負責處理一部分插槽。 例如,上面搭建的集群有3個主節點, 那么節點 6379 負責處理 0 號至 5460 號插槽、節點 6380 負責處理 5461 號至 10922 號插槽、節點 6381 負責處理 10923 號至 16383 號插槽。

(3)在集群中寫入值
① 一次寫入一個key

② 一次寫入多個key
由于不在一個slot下的鍵值,是不能直接使用mget、mset等多鍵操作。不過可以通過{ }來定義組的概念,從而使key中{ }內相同內容的鍵值對放到一個slot中去。

(4)查詢集群中的值

# 返回count個slot槽中的鍵 cluster getkeysinslot <slot> <count>

14.4.故障恢復

(1)當三臺主機中的某一臺(例如主機A)掛掉后,其對應的從機(例如從機a)會頂替該主機的位置,成為一個新的主機,而當原來掛掉的主機A重啟后,它會變成a的從機。
(2)如果某一段插槽的主從都掛掉,而配置文件redis.conf中的參數cluster-require-full-coverage為yes ,那么 整個集群都掛掉;如果某一段插槽的主從都掛掉,而cluster-require-full-coverage 為no ,那么該插槽數據全都不能使用,也無法存儲。

14.5.集群的Jedis開發

即使連接的不是主機,集群會自動切換主機存儲。主機寫,從機讀。無中心化主從集群,即無論從哪臺主機寫的數據,其他主機上都能讀到數據。

public class JedisClusterTest {public static void main(String[] args) { Set<HostAndPort>set =new HashSet<HostAndPort>();set.add(new HostAndPort("192.168.88.100",6379));JedisCluster jedisCluster=new JedisCluster(set);jedisCluster.set("k1", "v1");System.out.println(jedisCluster.get("k1"));} }

15.Redis應用問題解決

15.1.緩存穿透

15.1.1.問題描述

緩存穿透是指用戶不斷發起請求的數據在緩存和數據庫中都沒有,如發起為id為“-1”的數據或id為特別大不存在的數據。這時的用戶很可能是攻擊者,該攻擊會導致數據庫壓力過大。

15.1.2.解決方案

(1)對空值緩存:
如果一個查詢返回的數據為空(不管是數據是否不存在),我們仍然把這個空結果(null)進行緩存,設置空結果的過期時間會很短,一般最長不超過五分鐘。
(2)設置可訪問的名單(白名單):
使用bitmaps類型定義一個可以訪問的名單,名單id作為bitmaps的偏移量,每次訪問和bitmap里面的id進行比較,如果訪問id不在bitmaps里面,進行攔截,不允許訪問。
(3)采用布隆過濾器:
布隆過濾器(Bloom Filter)是1970年由布隆提出的,它實際上是一個很長的二進制向量(位圖)和一系列隨機映射函數(哈希函數)。布隆過濾器可以用于檢索一個元素是否在一個集合中。它的優點是空間效率和查詢時間都遠遠超過一般的算法,缺點是有一定的誤識別率和刪除困難。將所有可能存在的數據哈希到一個足夠大的bitmaps中,一個一定不存在的數據會被這個bitmaps攔截掉,從而避免了對底層存儲系統的查詢壓力。
(4)進行實時監控:
當發現Redis的命中率開始急速降低,需要排查訪問對象和訪問的數據,和運維人員配合,可以設置黑名單限制服務。

15.2.緩存擊穿

15.2.1.問題描述

緩存擊穿是指緩存中沒有但數據庫中有的數據(一般是數據的緩存時間到期),這時由于并發請求特別多,同時讀緩存沒讀到數據,又同時去數據庫去取數據,造成數據庫壓力瞬間增大。

15.2.2.解決方案

(1)預先設置熱門數據:
在redis高峰訪問之前,把一些熱門數據提前存入到redis里面,加大這些熱門數據key的時長。
(2)實時調整:
現場監控數據熱門,實時調整key的過期時長。
(3)使用鎖:
① 在緩存失效的時候(判斷拿出來的值為空),不是立即去load db,而是先使用緩存工具的某些帶成功操作返回值的操作(比如Redis的SETNX)去set一個mutex key。
③ 當操作返回成功時,再進行load db的操作,并回設緩存,最后刪除mutex key;
④ 當操作返回失敗,證明有線程在load db,當前線程睡眠一段時間再重試整個get緩存的方法。

15.3.緩存雪崩

15.3.1.問題描述

緩存雪崩是指緩存中數據大批量地過期,而查詢數據量巨大,引起數據庫壓力過大。緩存雪崩與緩存擊穿的區別在于前者針對很多key緩存,后者則是某一個key。

15.3.2.解決方案

(1)構建多級緩存架構:
nginx緩存 + redis緩存 +其他緩存(ehcache等)
(2)使用鎖或隊列:
用加鎖或者隊列的方式保證來保證不會有大量的線程對數據庫一次性進行讀寫,從而避免失效時大量的并發請求落到底層存儲系統上(不適用高并發情況)。
(3)設置過期標志更新緩存:
記錄緩存數據是否過期(設置提前量),如果過期會觸發通知另外的線程在后臺去更新實際key的緩存。
(4)將緩存失效時間分散開:
比如可以在原有的失效時間基礎上增加一個隨機值,比如1-5分鐘隨機,這樣每一個緩存的過期時間的重復率就會降低,就很難引發集體失效的事件。

15.4.分布式鎖

15.4.1.問題描述

(1)隨著業務發展的需要,原單體單機部署的系統被演化成分布式集群系統后,由于分布式系統多線程、多進程并且分布在不同機器上,這將使原單機部署情況下的并發控制鎖策略失效,單純的Java API并不能提供分布式鎖的能力。為了解決這個問題就需要一種跨JVM的互斥機制來控制共享資源的訪問,這就是分布式鎖要解決的問題。
(2)目前分布式鎖主流的實現方案如下:
① 基于數據庫實現分布式鎖
② 基于緩存(例如Redis等)
③ 基于Zookeeper
其中,Redisd的性能最高,Zookeeper的可靠性最高。

15.4.2.使用Redis命令實現分布式鎖

set <key> <value> NX|XX EX|PX second|millisecond NX只在鍵不存在時,才對鍵進行設置操作,SET key value NX 效果等同于 SETNX key value
EX second設置鍵的過期時間為 second 秒,SET key value EX second 效果等同于 SETEX key second value
PX millisecond設置鍵的過期時間為 millisecond 毫秒,SET key value PX millisecond 效果等同于 PSETEX key millisecond value
XX只在鍵已經存在時,才對鍵進行設置操作

15.4.3.使用Java代碼實現分布式鎖

(1)具體代碼如下:

@RestController @RequestMapping("/redisTest") public class RedisController {@Autowiredprivate RedisTemplate redisTemplate;@GetMapping("testLock")public void testLock(){//1獲取鎖,setneBoolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111");//2獲取鎖成功、查詢num的值if(lock){Object value = redisTemplate.opsForValue().get("num");//2.1判斷num為空returnif(StringUtils.isEmpty(value)){return;}//2.2有值就轉成成intint num = Integer.parseInt(value+"");//2.3把redis的num加1redisTemplate.opsForValue().set("num", ++num);//2.4釋放鎖,delredisTemplate.delete("lock");}else{//3獲取鎖失敗、每隔0.1秒再獲取try {Thread.sleep(100);testLock();} catch (InterruptedException e) {e.printStackTrace();}}} }

初始化num的值,使其為0。

使用ab工具進行壓力測試,命令如下:

ab -n 1000 -c 100 http://192.168.88.1:8080/redisTest/testLock


再次查看num的值,結果為100。

(2)代碼優化——使用UUID防止誤刪鎖

@GetMapping("testLock") public void testLock(){String uuid = UUID.randomUUID().toString();//1獲取鎖,setneBoolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid,3, TimeUnit.SECONDS);//2獲取鎖成功、查詢num的值if(lock){Object value = redisTemplate.opsForValue().get("num");//2.1判斷num為空returnif(StringUtils.isEmpty(value)){return;}//2.2有值就轉成成intint num = Integer.parseInt(value+"");//2.3把redis的num加1redisTemplate.opsForValue().set("num", ++num);//2.4釋放鎖,delredisTemplate.delete("lock");//判斷比較uuid值是否一樣String lockUuid = (String)redisTemplate.opsForValue().get("lock");if(lockUuid.equals(uuid)){redisTemplate.delete("lock");}}else{//3獲取鎖失敗、每隔0.1秒再獲取try {Thread.sleep(100);testLock();} catch (InterruptedException e) {e.printStackTrace();}} }

(3)代碼優化——LUA腳本保證刪除的原子性

@RestController @RequestMapping("/redisTest") public class RedisTestController {@Autowiredprivate RedisTemplate redisTemplate;@GetMapping("testLockLua")public void testLockLua() {//1 聲明一個uuid ,將做為一個value 放入我們的key所對應的值中String uuid = UUID.randomUUID().toString();//2 定義一個鎖:lua 腳本可以使用同一把鎖,來實現刪除!String skuId = "25"; // 訪問skuId 為25號的商品 100008348542String locKey = "lock:" + skuId; // 鎖住的是每個商品的數據// 3 獲取鎖Boolean lock = redisTemplate.opsForValue().setIfAbsent(locKey, uuid, 3, TimeUnit.SECONDS);// 第一種: lock 與過期時間中間不寫任何的代碼。// redisTemplate.expire("lock",10, TimeUnit.SECONDS);//設置過期時間// 如果trueif (lock) {// 執行的業務邏輯開始// 獲取緩存中的num 數據Object value = redisTemplate.opsForValue().get("num");// 如果是空直接返回if (StringUtils.isEmpty(value)) {return;}// 不是空 如果說在這出現了異常! 那么delete 就刪除失敗! 也就是說鎖永遠存在!int num = Integer.parseInt(value + "");// 使num 每次+1 放入緩存redisTemplate.opsForValue().set("num", String.valueOf(++num));/*使用lua腳本來鎖*/// 定義lua 腳本String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";// 使用redis執行lua執行DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();redisScript.setScriptText(script);// 設置一下返回值類型 為Long// 因為刪除判斷的時候,返回的0,給其封裝為數據類型。如果不封裝那么默認返回String 類型,// 那么返回字符串與0 會有發生錯誤。redisScript.setResultType(Long.class);// 第一個要是script 腳本 ,第二個需要判斷的key,第三個就是key所對應的值。redisTemplate.execute(redisScript, Arrays.asList(locKey), uuid);} else {// 其他線程等待try {// 睡眠Thread.sleep(1000);// 睡醒了之后,調用方法。testLockLua();} catch (InterruptedException e) {e.printStackTrace();}}}@GetMapping("testLock")public void testLock(){String uuid = UUID.randomUUID().toString();//1獲取鎖,setneBoolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid,3, TimeUnit.SECONDS);//2獲取鎖成功、查詢num的值if(lock){Object value = redisTemplate.opsForValue().get("num");//2.1判斷num為空returnif(StringUtils.isEmpty(value)){return;}//2.2有值就轉成成intint num = Integer.parseInt(value+"");//2.3把redis的num加1redisTemplate.opsForValue().set("num", ++num);//2.4釋放鎖,del//判斷比較uuid值是否一樣String lockUuid = (String)redisTemplate.opsForValue().get("lock");if(lockUuid.equals(uuid)) {redisTemplate.delete("lock");}}else{//3獲取鎖失敗、每隔0.1秒再獲取try {Thread.sleep(100);testLock();} catch (InterruptedException e) {e.printStackTrace();}}} }

15.4.4.總結

為了確保分布式鎖可用,我們至少要確保鎖的實現同時滿足以下四個條件:
(1)互斥性。在任意時刻,只有一個客戶端能持有鎖。
(2)不會發生死鎖。即使有一個客戶端在持有鎖的期間崩潰而沒有主動解鎖,也能保證后續其他客戶端能加鎖。
(3)解鈴還須系鈴人。加鎖和解鎖必須是同一個客戶端,客戶端自己不能把別人加的鎖給解了。
(4)加鎖和解鎖必須具有原子性。

16.Redis6新功能

16.1.ACL

16.1.1.介紹

Redis ACL是Access Control List(訪問控制列表)的縮寫,該功能允許根據可以執行的命令和可以訪問的鍵來限制某些連接。在Redis 5版本之前,Redis 安全規則只有密碼控制,還有通過rename 來調整高危命令比如 flushdb、KEYS*、 shutdown等。Redis6 則提供ACL的功能對用戶進行更細粒度的權限控制 :
(1)接入權限:用戶名和密碼
(2)可以執行的命令
(3)可以操作的 KEY

16.1.2.相關命令

(1)acl list:展現用戶權限列表

(2)acl cat
① 查看添加權限指令類別

② 加參數類型名可以查看類型下具體命令,例如:acl cat string

(3)acl whoami:查看當前用戶

(4)acl setuser

① 創建新用戶(默認權限)

acl setuser <username>


上面的示例沒有指定任何規則,如果用戶不存在,這將使用just created的默認屬性來創建用戶,如果用戶已經存在,則上面的命令將不執行任何操作。
② 設置有用戶名、密碼、ACL權限、并啟用的用戶

acl setuser mary on >password ~cached:* +get


切換用戶,驗證權限

# 切換用戶 auth mary password

16.2.I/O多線程

16.2.1.介紹

Redis6支撐多線程,但不是說Redis告別單線程。I/O多線程指的是客戶端交互部分的網絡I/O交互處理模塊多線程,而非執行命令多線程,Redis6執行命令依然是單線程。

16.2.2.原理架構

(1)Redis6 加入多線程,但跟 Memcached 這種從 I/O處理到數據訪問多線程的實現模式有些差異。Redis 的多線程部分只是用來處理網絡數據的讀寫和協議解析,執行命令仍然是單線程。之所以這么設計是不想因為多線程而變得復雜,需要去控制 key、Lua、事務,LPUSH/LPOP 等等的并發問題。其整體的設計大體如下:

(2)此外,多線程I/O默認是不開啟的,需要在redis.conf配置文件中進行手動配置

# 開啟I/O多線程 io-threads-do-reads yes # 多線程數量 io-threads 4

16.3.工具支持 Cluster

之前版本的Redis如果想要搭建集群,就需要單獨安裝ruby環境,Redis 5 將 redis-trib.rb 的功能集成到 redis-cli 。另外官方 redis-benchmark 工具開始支持 cluster 模式了,通過多線程的方式對多個分片進行壓測。

16.4.Redis6其它新功能

(1)RESP3新的 Redis 通信協議:
優化服務端與客戶端之間通信
(2)Client side caching客戶端緩存:
基于 RESP3 協議實現的客戶端緩存功能。為了進一步提升緩存的性能,將客戶端經常訪問的數據cache到客戶端。減少TCP網絡交互。
(3)Proxy集群代理模式:
Proxy 功能,讓 Cluster 擁有像單實例一樣的接入方式,降低開發者使用cluster的門檻。不過需要注意的是代理不改變 Cluster 的功能限制,不支持的命令還是不會支持,比如跨 slot 的多Key操作。
(4)Modules API
Redis 6中模塊API開發進展非常大,因為Redis Labs為了開發復雜的功能,從一開始就用上Redis模塊。Redis可以變成一個框架,利用Modules來構建不同系統,而不需要從頭開始寫然后還要BSD許可。Redis一開始就是一個向編寫各種系統開放的平臺。

總結

以上是生活随笔為你收集整理的Redis6——入门介绍的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。