Redis 單執行緒和多執行緒

到 6.0 以後,整個 redis 來說,是多執行緒的,但 redis 的 工作執行緒 (十大資料型態的操作)是單執行緒,如 RDB,AOF,非同步刪除,叢集資料同步 都是額外的執行緒執行的

Redis 最初為何選擇單執行緒

assets/Redis高级篇/image-20240224112815163.png assets/Redis高级篇/image-20240224112823238.png assets/Redis高级篇/image-20240224113953213.png

最初單執行緒仍然很快的原因

多執行緒是為了更好的利用 CPU,而對於 redis 來說主要瓶頸是記憶體或者網路頻寬而非 CPU,而且即使單執行緒也能透過多路複用和非阻塞 IO 來並行處理客戶端的請求,最初單執行緒的維護和開發更加簡單

assets/Redis高级篇/image-20240224114035636.png assets/Redis高级篇/image-20240224114047435.png

為什麼後來又加入了多執行緒

例如時包含了成千上萬個元素的 hash 集合時,那麼 del 指令就會造成 Redis 主執行緒卡頓

等待很久這個執行緒才會釋放,類似加了一個 synchronized 鎖,你可以想像高並行下,程式會超級堵

assets/Redis高级篇/image-20240225225924253.png

網路 IO 是多執行緒,真正幹活的執行緒還是單執行緒

多個伺服器連接一個 redis 伺服器進行操作,多個網路 IO,但是只有 redis 主執行緒負責寫入

IO 多路複用入門,真正的主執行緒登場

assets/Redis高级篇/image-20240225220655469.png

一般來說 CPU、記憶體都不是問題,網路 IO 是主要的問題

雖然有些命令操作可以用背景執行緒或子行程執行(比如資料刪除、快照生成、AOF 重寫)。但是,從網路 IO 處理到實際的讀寫命令處理,都是由單個執行緒完成的。Redis 的效能瓶頸有時會出現在網路 IO 的處理上,也就是說,單個主執行緒處理網路請求的速度跟不上底層網路硬體的速度,為了應對這個問題:

採用多個IO執行緒來處理網路請求,提高網路請求處理的並行度,Redis6/7就是採用的這種方法

assets/Redis高级篇/image-20240225221100871.png 也就是網路 IO 是多執行緒,真正幹活的執行緒還是單執行緒

  1. 連接,建立了Socket連接 assets/Redis高级篇/image-20240225221336857.png

assets/Redis高级篇/image-20240225222023779.png assets/Redis高级篇/image-20240225221841448.png assets/Redis高级篇/image-20240225222015206.png

IO 執行緒讀取和解析,比如網路是否通,IP 是否允許,防火牆是否允許

redis 很快的原因是 IO 多路複用和 epoll 函式的使用

Unix 網路程式設計中五種 IO 模型

IO 多路複用入門 : 只用一個服務行程就可以同時處理多個套接字描述符連接 assets/Redis高级篇/image-20240225224837407.png

IO 多路複用就是一種同步的 IO 模型,實現一個執行緒監視多個檔案句柄,一旦某個檔案句柄就緒就可以通知到對應程式進行相應的讀寫操作,沒有問及那句柄就緒就會阻塞應用程式,從而釋放電腦資源 IO 多路複用 : 使用單執行緒就能夠處理多個客戶端的連接,無需維護過多的行程

IO 多路複用入門

檔案描述符 FD 句柄

assets/Redis高级篇/image-20240225223213381.png

IO 多路複用是什麼

assets/Redis高级篇/image-20240225223415524.png

"文件句柄就绪"通常指的是作業系統中的檔案描述符(file descriptor)或檔案句柄(file handle)處於一種可以進行I/O操作的狀態。

assets/Redis高级篇/image-20240225223956014.png

assets/Redis高级篇/image-20240225224426317.png

assets/Redis高级篇/image-20240225224444894.png

assets/Redis高级篇/image-20240225224837407.png

redis 與多路複用

redis 不會出現執行緒安全問題因為主命令執行依舊是由主執行緒執行完成的

redis 與多路複用

assets/Redis高级篇/image-20240225225231125.png assets/Redis高级篇/image-20240225225309844.png

assets/Redis高级篇/image-20240225225615879.png

由於多執行緒預設關閉,實際應用中,如果發現 redis 實例的 CPU 開銷不大但吞吐量卻沒有提升,可以考慮開啟,80% 的公司單執行緒就夠了

開啟多執行緒

assets/Redis高级篇/image-20240225230058937.png assets/Redis高级篇/image-20240225230115940.png

1.設定io-thread-do-reads配置項為yes,表示啟動多執行緒。

2。設定執行緒個數。關於執行緒數的設定,官方的建議是如果為 4 核的 CPU,建議執行緒數設定為 2 或 3,如果為 8 核 CPU 建議執行緒數設定為 6,執行緒數一定要小於機器核數,執行緒數並不是越大越好。

執行緒不是越大越好 assets/Redis高级篇/image-20240225230310891.png

Redis 的多路複用指的是在單執行緒中透過 epoll 或 kqueue 等機制同時監聽多個通訊端的可讀寫狀態,從而實現對多個連線的管理和處理。這種機制使得 Redis 單執行緒能夠處理大量的並行連線而不會阻塞。

即使 Redis 不使用多執行緒,仍然可以利用多路複用機制來管理連線。這是因為多路複用並不依賴於多執行緒,而是利用了作業系統提供的非同步 I/O 機制,透過單個執行緒來監聽和處理多個連線的狀態變化。

BigKey

停用redis中keys */flushDB/flushALL

直接在設定檔中設定成空字串 "" assets/Redis高级篇/image-20240226155417140.png

scan

Redis 的 SCAN 命令是用於迭代集合中的元素的命令。它可以用於走訪集合中的所有元素,而不會阻塞伺服器。

相較於使用 KEYS 命令取得所有元素,SCAN 命令更加高效,因為它可以在不同的迭代器之間平均地分布工作負載,而不會對伺服器產生太大的負載

SCAN cursor [MATCH pattern] [COUNT count] [TYPE type]

SCAN 回傳一個包含兩個元素的陣列

第一個元素是用於進行下一次迭代的新游標

第二個元素則是一個 陣列,這個陣列中包含了所有被迭代的元素。如果新游標回傳零表示迭代已結束。

一次回傳的數量不可控, 只能是大概率符合 count 參數

使用方式

  1. 建立好 scanParams
  2. scan 中填入scanParams
定義一個scan
ScanParams scanParams = new ScanParams().match(pattern).count(count); ScanResult<String> scanResult = jedis.scan(cursor, scanParams); 

//第一個元素
cursor = scanResult.getCursor();
//第二個元素
List<String> keys = scanResult.getResult(); 
基本使用

assets/Redis高级篇/image-20240226160234341.png assets/Redis高级篇/image-20240226160637907.png

assets/Redis高级篇/image-20240506182731690.png

大 key

多大算大key

(阿里雲規範) assets/Redis高级篇/image-20240226184857377.png assets/Redis高级篇/image-20240226184916178.png assets/Redis高级篇/image-20240226191328590.png

如何發現大key
  1. redis-cli xxxx 結尾加上 --bigkeys assets/Redis高级篇/image-20240226193545644.png 雖然--bigkeys命令可以幫助你找到一些大鍵,但是當鍵的大小超過一定限制時,它可能無法提供高效的效能,甚至可能會對Redis實例的穩定性和整體效能產生負面影響。 assets/Redis高级篇/image-20240226193904401.png

  2. memory usage key

assets/Redis高级篇/image-20240226194600235.png

刪除

漸進式刪除 , 慢慢刪除部分資料,最後再進行 del

hash

assets/Redis高级篇/image-20240226195122291.png assets/Redis高级篇/image-20240226195129942.png assets/Redis高级篇/image-20240226195144210.png

list

assets/Redis高级篇/image-20240226195334732.png assets/Redis高级篇/image-20240226195343981.png assets/Redis高级篇/image-20240226195406077.png

set

assets/Redis高级篇/image-20240226195427067.png assets/Redis高级篇/image-20240226195446417.png assets/Redis高级篇/image-20240226195457819.png

zset

assets/Redis高级篇/image-20240226195527418.png assets/Redis高级篇/image-20240226195540329.png assets/Redis高级篇/image-20240226195551971.png

設定調優

redis 有兩種刪除模式,阻塞和非阻塞

LAZY FREEING

assets/Redis高级篇/image-20240226195752876.png

assets/Redis高级篇/image-20240226202308740.png

快取雙寫一致性

redis 充當快取,分為兩種

  1. 唯讀快取 (資料只在 redis 中存在,不存在mysql)
  2. 讀寫快取 (有寫有讀)
    1. 同步直寫策略 assets/Redis高級篇/image-20240226220618130.png 即時生效業務要求,採取先更新資料庫,後刪除快取
    2. 非同步緩寫策略 assets/Redis高級篇/image-20240226220544159.png 物流等,不著急從mysql更新到 redis

取得快取策略 : 雙檢加鎖策略

採用雙重檢查加鎖機制,只保證一個執行緒從 mysql 讀取最新資料,然後寫入 mysql,完成了資料的一致性,其他執行緒在拿到鎖再查時就已經有快取

首先嘗試從 Redis 中取得,沒有之後嘗試取得鎖,

取得機制 : 雙重檢查加鎖策略

從 Redis 取得,Redis 沒有則從 MySQL 取得

assets/Redis高级篇/image-20240226214858699.png assets/Redis高级篇/image-20240226220902417.png assets/Redis高级篇/image-20240226220925126.png


@Service
@Slf4j
public class UserService {
    public static final String CACHE_KEY_USER = "user:";
    @Resource
    private UserMapper userMapper;
    @Resource
    private RedisTemplate redisTemplate;

    /**
     * Business logic is not written wrong, for small and medium factories(QPS《=1000)it can be used, but for big factories it is not acceptable
     * @param id
     * @return
     */
    public User findUserById(Integer id)
    {
        User user = null;
        String key = CACHE_KEY_USER+id;

        //1 First from redis query inside, if there is a result return it directly, if not then query mysql
        user = (User) redisTemplate.opsForValue().get(key);

        if(user == null)
        {
            //2 redis nothing inside, continue querying mysql
            user = userMapper.selectByPrimaryKey(id);
            if(user == null)
            {
                //3.1 redis+mysql no data in both
                //you refine it specifically, to prevent repeated penetration, our business rule is, record the one that causes penetration key write back redis
                return user;
            }else{
                //3.2 mysql there is, need to write the data back to redis,ensure the cache hit rate next time
                redisTemplate.opsForValue().set(key,user);
            }
        }
        return user;
    }


    /**
     * strengthen the supplement, avoid sudden key expiration, overwhelming mysql,do some prevention, try not to have breakdown situations.
     * @param id
     * @return
     */
    public User findUserById2(Integer id)
    {
        User user = null;
        String key = CACHE_KEY_USER+id;

        //1 First from redis query inside, if there is a result return it directly, if not then query mysql,
        // Number 1 query time redis,before locking
        user = (User) redisTemplate.opsForValue().get(key);
        if(user == null) {
            //2 used by big factories, for high QPS optimization, lock first when entering, ensure one request operation, let the outside redis wait a bit, avoid breakdown mysql
            synchronized (UserService.class){
                //Number 2 query time redis,after locking
                user = (User) redisTemplate.opsForValue().get(key);
                //3 second check redis still null,can query the mysql it is done(mysql default has data)
                if (user == null) {
                    //4 query mysql get data(mysql default has data)
                    user = userMapper.selectByPrimaryKey(id);
                    if (user == null) {
                        return null;
                    }else{
                        //5 mysql there is data inside, need to write back redis,complete the synchronization work of data consistency
                        redisTemplate.opsForValue().setIfAbsent(key,user,7L,TimeUnit.DAYS);
                    }
                }
            }
        }
        return user;
    }
}

更新策略

給快取設定過期時間,定期清理快取並回寫,是保證最終一致性的解決方案

assets/Redis高级篇/image-20240226222119529.png

先更新快取,再更新資料庫

不推薦,業務上把 MySQL 作為底單資料,保證最後解釋

同樣多執行緒 assets/Redis高级篇/image-20240226223903566.png

先刪除快取,再更新資料庫(延遲雙刪)

先刪除了reids資料,讓然後mysql進行了更新,mysql沒更新完的時候就存取了資料,存取取得了舊值,還會把舊值寫回了redis,之前就白刪了,就導致新更新的資料沒有同步到redis,redis中資料一直是髒的,採用延時雙刪來解決,也就是在mysql更新後再刪除一次 Redis,延遲刪除時間需要估算

assets/Redis高级篇/image-20240226224102966.png assets/Redis高级篇/image-20240226224244107.png assets/Redis高级篇/image-20240226224307905.png assets/Redis高级篇/image-20240226225114399.png assets/Redis高级篇/image-20240226225139329.png

解決方案 : 延時雙刪策略

更新 MySQL 前後兩次進行刪除,負責刪除的 A 執行緒 sleep 時間要大於 B 執行緒寫入 Redis 快取的時間,防止別的執行緒讀取到舊資料寫入 Redis 後沒有被刪除(第二次刪除:將刪除變成非同步的避免吞吐量降低)

assets/Redis高级篇/image-20240226225927741.png

  • 刪除該休眠多久

assets/Redis高级篇/image-20240226230156341.png

  • 同步淘汰策略,吞吐量降低怎麼辦

將刪除變成非同步的避免吞吐量降低

前面的方案都不完美,就算解決還要引入新的比如延時雙刪

先更新資料庫,再更新快取
  1. 異常情況:多執行緒下,順序錯亂 assets/Redis高级篇/image-20240226222030956.png 1.異常情況:Redis 更新失敗,Redis 讀到髒資料 assets/Redis高级篇/image-20240226221900997.png assets/Redis高级篇/image-20240227113511287.png
先更新資料庫,再刪除快取(推薦)

問題還是讀到舊值,但 MySQL 更新完後舊值就會被替換,下次的請求就會得到最新的值,不會引發其他新的問題

assets/Redis高级篇/image-20240226230625978.png

但如果刪除失敗就有問題了

面試

面試

assets/Redis高级篇/image-20240226203415318.png assets/Redis高级篇/image-20240227113857061.png assets/Redis高级篇/image-20240227114009151.png

事實一致性很難做到,一般都是最終一致性

總結

assets/Redis高级篇/image-20240227114057438.png assets/Redis高级篇/image-20240227114108573.png

Redis 與 MySQL 資料雙寫一致性工程落地案例

cannel

alibaba/canal:阿里巴巴 MySQL binlog 增量訂閱與消費元件 (github.com)

如何知道mysql發生了變動?並且能夠通知給redis? assets/Redis高级篇/image-20240227121343144.png assets/Redis高级篇/image-20240227122420881.png

傳統 MySQL 主從複製的工作原理

assets/Redis高级篇/image-20240227122640454.png

canal 工作原理

assets/Redis高级篇/image-20240227123015632.png

使用 canal 前 MySQL 修改
  • MySQL 設定修改 assets/Redis高级篇/image-20240227123715932.png

開啟 MySQL 的 binlog 寫入功能

assets/Redis高级篇/image-20240227123753697.png 之後會變成 ON assets/Redis高级篇/image-20240227123930027.png

新建 canal 帳戶並授權 select * from mysql.user

DROP USER IF EXISTS 'canal'@'%'; CREATE USER 'canal'@'%' IDENTIFIED BY 'canal';   GRANT ALL PRIVILEGES ON . TO 'canal'@'%' IDENTIFIED BY 'canal';   FLUSH PRIVILEGES; SELECT * FROM mysql.user;

安裝設定 canal
  1. 下載 deployer 版本然後解壓縮到/mycanal路徑下 assets/Redis高级篇/image-20240227124349998.png
  2. 設定assets/Redis高级篇/image-20240227124713097.png assets/Redis高级篇/image-20240227124727411.png 將 master 填寫自己的 MySQL 位址 assets/Redis高级篇/image-20240227124737618.png 將帳戶換成之前自己新建的 assets/Redis高级篇/image-20240227124750228.png

啟動

/opt/mycanal/bin路徑下執行 ./startup.sh

日誌

mycanal/logs/canal 中cat canal.log

canal 在 Java 業務中的編寫

pom
<dependency>
    <groupId>com.alibaba.otter</groupId>
    <artifactId>canal.client</artifactId>
    <version>1.1.0</version>
</dependency>


<dependency>
		<groupId>redis.clients</groupId>
		<artifactId>jedis</artifactId>
		<version>3.8.0</version>
</dependency>

RedisUtils


public class RedisUtils
{
    public static final String  REDIS_IP_ADDR = "192.168.111.185";
    public static final String  REDIS_pwd = "111111";
    public static JedisPool jedisPool;

    static {
        JedisPoolConfig jedisPoolConfig=new JedisPoolConfig();
        jedisPoolConfig.setMaxTotal(20);
        jedisPoolConfig.setMaxIdle(10);
        jedisPool=new JedisPool(jedisPoolConfig,REDIS_IP_ADDR,6379,10000,REDIS_pwd);
    }

    public static Jedis getJedis() throws Exception {
        if(null!=jedisPool){
            return jedisPool.getResource();
        }
        throw new Exception("Jedispool is not ok");
    }

}


canal 設定 RedisCanalClientExample


public class RedisCanalClientExample
{
    public static final Integer _60SECONDS = 60;
    public static final String  REDIS_IP_ADDR = "192.168.111.185";

    private static void redisInsert(List<Column> columns)
    {
        JSONObject jsonObject = new JSONObject();
        for (Column column : columns)
        {
            System.out.println(column.getName() + " : " + column.getValue() + "    update=" + column.getUpdated());
            jsonObject.put(column.getName(),column.getValue());
        }
        if(columns.size() > 0)
        {
            try(Jedis jedis = RedisUtils.getJedis())
            {
                jedis.set(columns.get(0).getValue(),jsonObject.toJSONString());
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }


    private static void redisDelete(List<Column> columns)
    {
        JSONObject jsonObject = new JSONObject();
        for (Column column : columns)
        {
            jsonObject.put(column.getName(),column.getValue());
        }
        if(columns.size() > 0)
        {
            try(Jedis jedis = RedisUtils.getJedis())
            {
                jedis.del(columns.get(0).getValue());
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }

    private static void redisUpdate(List<Column> columns)
    {
        JSONObject jsonObject = new JSONObject();
        for (Column column : columns)
        {
            System.out.println(column.getName() + " : " + column.getValue() + "    update=" + column.getUpdated());
            jsonObject.put(column.getName(),column.getValue());
        }
        if(columns.size() > 0)
        {
            try(Jedis jedis = RedisUtils.getJedis())
            {
                jedis.set(columns.get(0).getValue(),jsonObject.toJSONString());
                System.out.println("---------update after: "+jedis.get(columns.get(0).getValue()));
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }

    public static void printEntry(List<Entry> entrys) {
        for (Entry entry : entrys) {
            if (entry.getEntryType() == EntryType.TRANSACTIONBEGIN || entry.getEntryType() == EntryType.TRANSACTIONEND) {
                continue;
            }

            RowChange rowChage = null;
            try {
                //get changed row data
                rowChage = RowChange.parseFrom(entry.getStoreValue());
            } catch (Exception e) {
                throw new RuntimeException("ERROR ## parser of eromanga-event has an error,data:" + entry.toString(),e);
            }
            //get change type
            EventType eventType = rowChage.getEventType();
            System.out.println(String.format("================&gt; binlog[%s:%s] , name[%s,%s] , eventType : %s",
                    entry.getHeader().getLogfileName(), entry.getHeader().getLogfileOffset(),
                    entry.getHeader().getSchemaName(), entry.getHeader().getTableName(), eventType));

            for (RowData rowData : rowChage.getRowDatasList()) {
                if (eventType == EventType.INSERT) {
                    redisInsert(rowData.getAfterColumnsList());
                } else if (eventType == EventType.DELETE) {
                    redisDelete(rowData.getBeforeColumnsList());
                } else {//EventType.UPDATE
                    redisUpdate(rowData.getAfterColumnsList());
                }
            }
        }
    }


    public static void main(String[] args)
    {
        System.out.println("---------O(∩_∩)O哈哈~ initCanal() main方法-----------");

        //=================================
        // create connection canal server side
        CanalConnector connector = CanalConnectors.newSingleConnector(new InetSocketAddress(REDIS_IP_ADDR,
                11111), "example", "", "");
        int batchSize = 1000;
        //idle spin counter
        int emptyCount = 0;
        System.out.println("---------------------canal init OK,开始监听mysql变化------");
        try {
            connector.connect();
            //connector.subscribe(".*\\..*");
            //more monitoring,slower write back,only guarantee double write consistency for important data
            connector.subscribe("bigdata.t_user");
            connector.rollback();
            int totalEmptyCount = 10 * _60SECONDS;
            while (emptyCount < totalEmptyCount) {
                System.out.println("我是canal,每秒一次正在监听:"+ UUID.randomUUID().toString());
                Message message = connector.getWithoutAck(batchSize); // get specified amount of data
                long batchId = message.getId();
                int size = message.getEntries().size();
                if (batchId == -1 || size == 0) {
                    emptyCount++;
                    try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
                } else {
                    //reset counter to zero
                    emptyCount = 0;
                    printEntry(message.getEntries());
                }
                connector.ack(batchId); // submit confirmation
                // connector.rollback(batchId); // processing failed, roll back data
            }
            System.out.println("已经监听了"+totalEmptyCount+"秒,无任何消息,请重启重试......");
        } finally {
            connector.disconnect();
        }
    }
}
設定細則

Java 程式下 connector.subscribe 設定的過濾正規表示式

assets/Redis高级篇/image-20240227133108859.png

try-with-resources無需再手動關閉 assets/Redis高级篇/image-20240227133159757.png assets/Redis高级篇/image-20240227144136857.png

jedis會自動關閉,所以不需要手動finally

案例落地實戰 bitmap/hyperloglog/GEO

hyperloglog

UV/PV/DAU/MAU :獨立訪客/頁面瀏覽量/日活躍使用者/月活躍使用者

基數統計:統計集合中不重複的元素個數,就是對集合去重複後剩餘元素的計算,透過機率演算法,並不儲存資料本身,預估基數值,同時保證誤差在一定範圍內

百分之 0.81 誤差

JAVA微服務生態/Redis7/基礎/Redis入門 > redis 基數統計( HyperLogLog )

assets/Redis高级篇/image-20240227200949823.png

hyperloglog如何演化出來的

由於bitmap在資料量過大時佔用了過多記憶體,因此演化出了hyperloglog,記憶體減少的効果顯著

assets/Redis高级篇/image-20240227201745289.png assets/Redis高级篇/image-20240227201956254.png assets/Redis高级篇/image-20240227201949379.png assets/Redis高级篇/image-20240227202115350.png

http://antirez.com/news/75

1.04/16384/16384 = 0.0081
assets/Redis高级篇/image-20240227202151936.png

GEO

JAVA微服務生態/Redis7/基礎/Redis入門 > 地理空間 GEO

assets/Redis高级篇/image-20240228115525757.png

JAVA微服務生態/Redis7/基礎/Redis入門 > 地理空間 GEO

assets/Redis高级篇/image-20240228120757914.png

RedisTemplate對GEO的相關操作

public static final String CITY ="city";  

@Autowired  
private RedisTemplate redisTemplate;
public String geoAdd()  
{  
    Map<String, Point> map = new HashMap<>();  
    map.put("天安门",new Point(116.403963,39.915119));  
    map.put("故宫",new Point(116.403414,39.924091));  
    map.put("长城",new Point(116.024067,40.362639));  
  
    redisTemplate.opsForGeo().add(CITY,map);  
  
    return map.toString();  
}  
  
public Point position(String member) {  
    //get longitude and latitude coordinates  
    List<Point> list = redisTemplate.opsForGeo().position(CITY, member);  
    return list.get(0);  
}  
  
public String hash(String member) {  
    //geohash generated by algorithm base32 encoded value  
    List<String> list = redisTemplate.opsForGeo().hash(CITY, member);  
    return list.get(0);  
}  
  
public Distance distance(String member1, String member2) {  
    //get distance between two given locations  
    Distance distance = redisTemplate.opsForGeo().distance(CITY, member1, member2,  
            RedisGeoCommands.DistanceUnit.KILOMETERS);  
    return distance;  
}  
public GeoResults radiusByxy() {  
    //find nearby places by longitude and latitude,Beijing Wangfujing location 116.418017,39.914402  
    //Metrics.KILOMETERS.getMultiplier()radius of circle in kilometers
    Circle circle = new Circle(116.418017, 39.914402, Metrics.KILOMETERS.getMultiplier());  
    // return 50 items  
    RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeDistance().includeCoordinates().sortDescending().limit(50);  
  
    GeoResults<RedisGeoCommands.GeoLocation<String>> geoResults = redisTemplate.opsForGeo().radius(CITY, circle, args);  
  
    return geoResults;  
}  
public GeoResults radiusByMember() {  
    //find nearby places by location  
    return null;  
}

assets/Redis入门/image-20231122210524494.png

assets/Redis高级篇/image-20240228130413769.png

assets/Redis高级篇/image-20240228125751045.png

bitmap

assets/Redis高级篇/image-20240228142123227.png

JAVA微服務生態/Redis7/基礎/Redis入門 > redis 位圖( bitmap )

布隆過濾器 BloomFilter

assets/Redis高级篇/image-20240228142157788.png

布隆過濾器是一段很長的 binary array+hash algorithm 製作而成的,並不儲存資料而是判斷資料是否存在

布隆過濾器就是利用自身存在則可能存在 (不同資料 hash 值可能一樣),不存在則一定不存在的特性,快速判斷一定一定 data not present in the sample / data present in the sample(has error) ,根據需求進行過濾

高效的插入和查詢,佔用空間少

返回的結果是不確定加不夠完美

由一個初始值為 0 的 bit 陣列和多個雜湊函式構成,用來快速判斷 collection 中是否存在某個元素,判斷一個區間是否有資料

  • 根據存在是否 (存在的判斷有誤差)

白名單校驗:存在才允許存取,不存在不允許存取(解決 redis 快取穿透) 黑名單校驗 (垃圾郵件):存在不允許存取,不存在允許存取

assets/Redis高级篇/image-20240430001456570.png assets/Redis高级篇/image-20240228151943075.png

assets/Redis高级篇/image-20240228143534196.png assets/Redis高级篇/image-20240228193741352.png

原理

我們可以將 redis 中的一個 bitmap 資料製作成布隆過濾器

原理和操作

assets/Redis高级篇/image-20240228153535104.png

可以用多個位置聯合表示一個資料是否存在,多個位置同時存在才存在 assets/Redis高级篇/image-20240228153804694.png assets/Redis高级篇/image-20240228153814155.png

所以布隆過濾器新增完成後盡量減少刪除

assets/Redis高级篇/image-20240228154706723.png assets/Redis高级篇/image-20240228154713011.png 只有一個hash函式時,容易發生雜湊碰撞,多個hash函式能減少雜湊碰撞

使用步驟
  • 初始化bitmap assets/Redis高级篇/image-20240228155539538.png

  • 新增佔位 assets/Redis高级篇/image-20240228155558184.png

  • 判斷是否存在 assets/Redis高级篇/image-20240228155609616.png assets/Redis高级篇/image-20240228155617894.png assets/Redis高级篇/image-20240228160420295.png assets/Redis高级篇/image-20240228160619282.png

使用場景

解決快取穿透問題

assets/Redis高级篇/image-20240228160902622.png assets/Redis高级篇/image-20240228161254384.png

手寫布隆過濾器

一般不用手寫,用谷歌的 guava 即可

Math.abs 計算絕對值,避免負數

手寫布隆過濾器

assets/Redis高级篇/image-20240228162951167.png

計算雜湊後透過hashValue和2的32次方取餘後得到對應的下標坑位

  1. 透過BloomFlterInit將已經存在資料作為白名單加入到布隆過濾器

  2. 透過CheckUtils檢查新的資料是否在布隆過濾器

  3. 得到雜湊值 int hashvalues = Math.abs(key.hashCode());

  4. 得到槽數 long v = (long) (hashvalues % Math.pow(2, 32));

  5. 設定bit redisTemplate.opsForValue().setBit("whitelistCustomer",v, true);

布隆過濾器, 新增 @component, @PostConstruct

自訂一個 鍵值 如 whitelistCustomer, 資料類型為 bitmap, 這個資料類型的 bitmap 就作為布隆過濾器

將其他鍵的值進行計算, 得出對應的坑位,並設定為true

BloomFilterInit

* 布隆過濾器白名單初始化工具類,一開始就設定一部分資料為白名單所有,  
* 白名單業務預設規定:布隆過濾器有,redis是極大可能有。


@Component
public class BloomFilterInit  
{  
    @Resource  
    private RedisTemplate redisTemplate;  
  
    @PostConstruct//初始化白名單資料,暫時註解省的後台列印  
    public void init()  
    {  
        //1 白名單客戶載入到布隆過濾器  
        String key = "customer:12";  
        //2 計算hashValue,由於存在計算出來負數的可能,我們取絕對值  
        int hashValue = Math.abs(key.hashCode());  
        //3 透過hashValue和2的32次方後取餘,獲得對應的下標坑位  
        long index = (long)(hashValue % Math.pow(2,32));  
        log.info(key+" 对应的坑位index:{}",index);  
        //4 設定redis裡面的bitmap對應類型白名單:whitelistCustomer的坑位,將該值設定為1  
        redisTemplate.opsForValue().setBit("whitelistCustomer",index,true);  
  
    }  
}

CheckUtils 同樣計算出坑位的值 , 檢查是否在布隆過濾器中

checkItem 就代表布隆過濾器

CheckUtils
public class CheckUtils  
{  
    @Resource  
    private RedisTemplate redisTemplate;  
  
    public boolean checkWithBloomFilter(String checkItem,String key)  
    {  
        int hashValue = Math.abs(key.hashCode());  
        long index = (long)(hashValue % Math.pow(2,32)); 
        ////checkItem就是key值 
        boolean existOK = redisTemplate.opsForValue().getBit(checkItem,index);  
        log.info("--->key:"+key+" 对应坑位下标index: "+index+" 是否存在:"+existOK);  
  
        return existOK;  
    }  
}
java
public Customer findCustomerByIdWithBloomFilter (Integer customerId)  
{  
    Customer customer = null;  
    //快取key的名稱  
    String key = CACHA_KEY_CUSTOMER + customerId;  
  
    //布隆過濾器check,無是絕對無,有是可能有  
    //===============================================  
    if(!checkUtils.checkWithBloomFilter("whitelistCustomer",key))  
    {  
        log.info("白名单无此顾客,不可以访问: "+key);  
        return null;    }  
    //===============================================  
  
    //1 查詢redis  
    customer = (Customer) redisTemplate.opsForValue().get(key);  
    //redis無,進一步查詢mysql  
    if (customer == null) {  
        //2 從mysql查出來customer  
        customer = customerMapper.selectByPrimaryKey(customerId);  
        // mysql有,redis無  
        if (customer != null) {  
            //3 把mysql撈到的資料寫入redis,方便下次查詢能redis命中。  
            redisTemplate.opsForValue().set(key, customer);  
        }  
    }  
    return customer;  
}

除了布隆還有布穀鳥過濾器

mapper

abel533/Mapper: Mybatis Common Mapper - Easy to use (github.com)

https://www.bilibili.com/video/BV13R4y1v7sP?p=121&vd_source=17535a06cd4587318e9b67ac64afeab7

快取預熱加快取雪崩加快取擊穿加快取穿透

assets/Redis高级篇/image-20240228194654216.png

快取預熱

主動將資料先放到 redis 中

  1. @PostConstruct 初始化白名單資料 (也用於初始化布隆過濾器)
  2. 從設定檔 application.yml 注入
@Value("${whitelist.ips}") // 注入 IP 白名單
private String ips;
@Value("${whitelist.users}") // 注入 使用者白名單
private String users;

快取雪崩

快取雪崩

redis 掛了,或者 redis 中同時有大量 key 大面積失效

解決:

  1. 均勻過期
  2. redis 中的 熱點key 設定為 永不過期、過期時間錯開
  3. redis 快取叢集實現高可用:主從加哨兵、叢集,AOF/RDB
  4. 熔斷降級 服務熔斷/服務降級

快取穿透

每次查詢 redis 中都沒有,從頭到尾都沒有,直接達到了 mysql 導致資料庫壓力暴增

assets/Redis高级篇/image-20240229114927835.png

assets/Redis高级篇/image-20240430000312389.png assets/Redis高级篇/image-20240430000419379.png

解決方案

  1. 空物件快取 (使用預設值) 穿透的都進一步快取到 redis 中,駭客會用不同 key 進行更新, 會寫入很多無關緊要的 key,越寫越大

  2. Google 布隆過濾器 Guava(權威且建議) Guava 解耦性好 誤判機率很小可以接受 將全部合法的 key 放入 Guava 版本布隆過濾器 +redis 裡面,不然資料返回 null


<!--guava Google 開源的 Guava 中自帶的布隆過濾器-->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>23.0</version>
        </dependency>

Guava 白名單實例

誤判率越小:生成的槽位越多,使用的雜湊函式越多

但是,增加雜湊函式的數量也會增加計算開銷。通常情況下,合理的雜湊函式數量可以在保持低誤判率的同時降低計算成本

assets/Redis高级篇/image-20240228230943050.png assets/Redis高级篇/image-20240228231126403.png

這10萬資料都不在布隆過濾器中,應該都不存在,但由於過濾器誤判可能判為存在

@Service
@Slf4j
public class GuavaBloomFilterService
{
    //1 定義一個常數
    public static final int _1W = 10000;
    //2 定義我們guava布隆過濾器,初始容量
    public static final int SIZE = 100 * _1W;
    //3 誤判率,它越小誤判的個數也就越少(Thinking, can it be infinitely small? Would it not be better without false positives?)
    public static double fpp = 0.01;//0.01 0.000000000000001
    //4 Create guava Bloom filter
    private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), SIZE,fpp);


    public void guavaBloomFilter()
    {
        //1 First let bloomFilter add 100W whitelist data
        for (int i = 1; i <= SIZE  ; i++) {
            bloomFilter.put(i);
        }
        //2 Deliberately take 10W pieces of data outside the valid range,to demonstrate the false positive rate
        ArrayList<Integer> list = new ArrayList<>(10 * _1W);

        //3 Verify
        //The filter only added SIZE data,Deliberately start from SIZE+1 data outside to begin judging,false positives occur
        for (int i = SIZE+1; i <= SIZE+(10 * _1W) ; i++)
        {
            if(bloomFilter.mightContain(i))
            {
                log.info("被误判了:{}",i);
                list.add(i);
            }
        }
        log.info("误判总数量:{}",list.size());
    }
}

Guava blacklist example

assets/Redis高级篇/image-20240228231604404.png

Cache breakdown

A certain key when heavily requested, this key happens to expire, causing a large number of requests to hit the database, resulting in cache breakdown assets/Redis高级篇/image-20240229115521197.png

Some hackers will, at the instant an event ends, at this instant there will be a batch of redis being deleted, new ones imported redis, hackers will exploit this instant to perform cache breakdown

Solution: double check locking, mutual exclusion update, random backoff, staggered expiration times

Mutual exclusion update: similar to double check locking, but more focused on ensuring only one thread updates the cache, while other threads wait for the update to complete and then directly read the cache.

Random backoff: when the cache expires, different requests apply a random delay before accessing the database, which avoids a large number of requests accessing the database at the same time. It affects user experience.

Cause

For example, a Double Eleven business only runs for a few days, after that time it is cleared but then accessed again assets/Redis高级篇/image-20240229123022599.png assets/Redis高级篇/image-20240229131954946.png Right after deletion, other threads may access it again

  • Solution
  1. Use the double check locking on read described earlier
  2. Staggered expiration times, or for frequently accessed hot keys, simply do not set an expiration time
  3. Use two copies of data A expired B as backup, A at deletion time the update is not yet complete, at this time there is B holding up

Summary

assets/Redis高级篇/image-20240229190353764.png

Hand writing a Redis distributed lock

(lock name|UUID|counter)``redisLock:uuid:valueNumber

Lock name: all the same, ensuring uniqueness, there is only one lock UUID = factory UUID plus thread UUID: determines whether the lock was created by itself, so as to judge whether it can be deleted (ensuring only the creator of the lock has permission to operate on the lock), and whether reentrant operations can be performed, but the UUID in the lock is bound to the thread

Use Hset to build a distributed lock

assets/Redis高级篇/image-20240229191750770.png

Because of multiple distributed virtual machines, while Java the lock in can only manage one virtual machine, the single thread thread lock mechanism no longer works, at this time a lock that all threads can access and acquire is needed (can be through redis or zookeeper to build), to go across processes and services

assets/Redis高级篇/image-20240229195626005.png

What a qualified redis lock requires

assets/Redis高级篇/image-20240229211008632.png

Using the string data type in redis (which can be improved to hash when the lock is reusable), create a key with a fixed value and only one, as a distributed lock, other threads must compete for this unique redis lock before they can operate, the stand alone version lock (synchronized/lock) can only manage its own current thread

Prevent multi thread spurious wakeup

  • Problem 1: prevent multi thread spurious wakeup,不要使用 if 判斷,而是使用 while,while在wait喚醒後,又進行一次while判斷,而if不會,同樣遞迴在高併發容易出錯,而此時採用自旋來代替方法重試呼叫

搶不到要重試,如果使用 if,則需要 遞迴 重試,太多請求可能爆 StackOverflowError assets/Redis高级篇/image-20240301131741828.png 為了防止多執行緒的 虛假喚醒,不要使用if判斷,而是使用while

while 可以在滿足條件時一直執行,而 if 想要一直執行則需要遞迴本方法

assets/Redis高级篇/image-20240725113135093.png

  • 改善,避免了 StackOverflowError assets/Redis高级篇/image-20240301132338693.png 只有拿到鎖時才會結束

保證過期和建立具有原子性

  • 問題 2 : 宕機導致的問題,需要保證過期和建立具有原子性 assets/Redis高级篇/image-20240301164214540.png 當拿到鎖後,其中一個 Java 服務 A 突然宕機了,而建立的分散式鎖沒有設定時間依然存在,而沒有設定過期時間就會一直存在,需要加上到期時間 assets/Redis高级篇/image-20240301164351655.png 這串程式碼不具有 原子性,可能出現搶到鎖了,但是卻掛了沒有設定上過期時間(程式崩潰、伺服器宕機等),確保搶到鎖的同時設定上了過期時間則具有原子性

如何防止到期誤刪 key

  • 問題 3 : 如何防止到期誤刪 key,過期時間到了鎖已經失效了但任務還沒有完成,別的執行緒拿到了鎖,之後自己完成後進行刪除,卻刪除了別人執行緒的鎖

鎖也就是 key 透過設定相同的,value 設定為 uuid,則會透過判斷 value 的值來判斷該 key 是否是自己建立的

因為 key 都是相同的,key 存在則不能進行建立,這樣到期後別的執行緒建立了新的鎖,自己的鎖已經到期,也不會刪除別人建立的鎖

assets/Redis高级篇/image-20240301193550454.png

以前 assets/Redis高级篇/image-20240301193518166.png

現在: 判斷是否是自己的,

assets/Redis高级篇/image-20240301193529989.png 但該刪除操作依舊不具有原子性

lua 腳本: 將刪除具有原子性

Distributed Locks with Redis | Redis

判斷和刪除兩個操作如何具有原子性?

  • 問題四 : 刪除不具有原子性,需要 lua 腳本,如果剛判斷完掛掉了就會導致沒有刪除

assets/Redis高级篇/image-20240302122044509.png

作用

redis 呼叫 lua 腳本透過 eval 命令保證程式執行的原子性,直接 return 返回腳本執行後的結果值

assets/Redis高级篇/image-20240301210203079.png

lua 腳本的作用就是 將多條命令進行多合一執行,如上面三條變成一次性執行 assets/Redis高级篇/image-20240301212033905.png

語法格式

assets/Redis高级篇/image-20240301214338095.png

nil 代表返回空,等價於 java 中的 false

nil=false 1=true 0=false

  • lua語法格式

assets/Redis高级篇/image-20240302115152920.png

assets/Redis高级篇/image-20240302115734714.png

end" 後的參數數量先依次分給 KEYS, 再分給 ARGV EVAL 命令中,KEYS 參數的數量之後的所有參數都會被視為 ARGV 參數

java 中 lua 腳本的使用

assets/Redis高级篇/image-20240302122256572.png

執行 lua 腳本推薦用 DefaultRedisScript()

stringRedisTemplate.execute(new DefaultRedisScript(luaScript,Boolean.class), Arrays.asList(key),uuidValue);

Arrays.asList(key)==KEYS[]
uuidValue==ARGV[]

assets/Redis高级篇/image-20240302122440182.png

nil = null 1 = true 0 = false 返回值有三套值

所以用 Boolean.class 接受,也可以用 Long.class 接受 但 Boolean.class 類型無法直接接受 Redis 返回的 nil 值

Redis Lua 腳本返回值到 Java Boolean 的轉換規則:嘗試將 Redis Lua 腳本的返回值轉換為 Boolean 時,它會遵循一定的規則。通常,Redis 的整數返回值會被這樣轉換: 0 會被轉換為 false,任何非零的整數 (通常是 1 或其他正數或負數) 會被轉換為 true

Lua 語言本身的真值和假值規則: 在 Lua 的邏輯判斷中(例如 if 條件),lua 認為 0 和 空字串 都是 true, 除了 false 和 nil 為假, 其他值都為真。

注意

如果 lua 腳本外面有雙引號了,裡面要改成單引號

assets/Redis高级篇/image-20240302115013449.png

Boolean 代表了返回值

可重用鎖

assets/Redis高级篇/image-20240302134425020.png

同一個執行緒中的方法拿到鎖後,本身呼叫的方法又需要拿鎖,該如何設計?

redis 中一個業務拿到鎖後,下次還是自己搶到,該如何分配?

assets/Redis高级篇/image-20240302134210350.png

一個執行緒中的一個方法拿到鎖後,方法內的方法如果再需要鎖,可以用之前拿到的鎖進行重用

每個鎖物件擁有一個鎖計數器和一個指向持有該鎖的執行緒的指標。

隱式鎖 如synchronized 都是可重用鎖

assets/Redis高级篇/image-20240302135802993.png assets/Redis高级篇/image-20240302135813958.png

原理

assets/Redis高级篇/image-20240302135829392.png

同步區塊

assets/Redis高级篇/image-20240302135235376.png

同步方法

assets/Redis高级篇/image-20240302135252976.png

顯式鎖

assets/Redis高级篇/image-20240302140213199.png

只需要將自己寫的 redis 鎖具有 lock,unlock 功能,以及鎖的計數器 就可以

只有 redis 中 hash 滿足 kkv 結構

(鎖名|UUID|計數器)``redisLock:uuid:valueNumber

鎖名都是相同的,UUID 只是刪除時進行判斷,和可重用鎖時進行判斷

因此應該用 hash 版 redis 鎖代替之前的 string 版本

LOCK

為了 lock 希望將下面一段抽取出來

希望將這一段抽進一個方法變成lock

assets/Redis高级篇/image-20240302172820416.png

  • Lock :加鎖並計數的 lua 腳本

判斷鎖是否存在,存在就建立,計數預設為 1,設定過期時間 如果判斷存在,且是自己的,表示重用,設定計數器加一,設定過期時間

回傳 0 表示沒有,回傳 1 表示有

assets/Redis高级篇/image-20240302174642457.png 優化為 assets/Redis高级篇/image-20240302175013029.png 然後改寫為 lua 腳本

hincrby如果沒有會自動建立 assets/Redis高级篇/image-20240302181352573.png

lua 腳本中回傳給 java 的資料,也就是 ruturn 中:0 會被轉換為 false,任何非零的整數 (通常是 1 或其他正數或負數) 會被轉換為 true

lua 腳本中 : call 方法回傳 0 表示不存在,或者 1 表示存在

Lua 語言本身的真值和假值規則:在 Lua 的邏輯判斷中 (例如 if 條件),lua 認為 0 和 空字串 都是 true, 除了 false 和 nil 為假,其他值都為真。

unLOCK

unlock :將這一段抽進一個方法變成 unlock

判斷鎖是否存在且是自己的,然後計數器減一 判斷計數器如果為零就直接刪除 如果不存在就直接回傳

call 方法 return 0 時,它通常表示執行成功並回傳一個整數值,就相當於java中return

assets/Redis高级篇/image-20240302173502065.png

計數器減到 0 表示已經全部被解鎖,可以刪除

assets/Redis高级篇/image-20240302182656392.png

目前 UUID 是在鎖中產生的,重用鎖會導致 UUID 不一樣,後期使用一個工廠造鎖來解決

lua 整合進 java 程式

實作 Lock 鎖,按照 Lock 鎖的規範進行設計

引入工廠模式:就是提前幫你 new 好各種類型,自己選擇使用

assets/Redis高级篇/image-20240303122911227.png

  • 問題

uuid 在鎖中製造,而透過工廠造鎖,重使用時工廠會重新造鎖,導致 uuid 都不一樣,同一個執行緒出現了兩個 uuid 為了確保同一個執行緒共用一個鎖,需要設定為同一個 UUID

因此鎖工廠引入固定的 uuid,這樣不同執行緒產生相同的 uuid,而在 redis 鎖中 uuid 是執行緒工廠的 uuid 加上對應執行緒

解決:在工廠中製造好 UUID,不在鎖中製作

  • 問題

有時業務執行時間長,如何自動續時長

透過 java 的定時器 timer

寫一個自動續時的 lua 腳本,例如過了 10s 還沒完成,自動加鐘

加鐘腳本 assets/Redis高级篇/image-20240303183102388.png

DistributedLockFactory 工廠
@Component  
public class DistributedLockFactory  
{  
    @Autowired  
    private StringRedisTemplate stringRedisTemplate;  
    private String lockName;  
    private String uuid;  
  
    public DistributedLockFactory()  
    {
        this.uuid = IdUtil.simpleUUID();
    }  
  
    public Lock getDistributedLock(String lockType)  
    {
        if(lockType == null) return null;  
  
        if(lockType.equalsIgnoreCase("REDIS")){  
            this.lockName = "zzyyRedisLock";  
            return new RedisDistributedLock(stringRedisTemplate,lockName,uuid);  
        }else if(lockType.equalsIgnoreCase("ZOOKEEPER")){  
            this.lockName = "zzyyZookeeperLockNode";  
            //TODO zookeeper版本的分散式鎖  
            return null;  
        }else if(lockType.equalsIgnoreCase("MYSQL")){  
            //TODO MYSQL版本的分散式鎖  
            return null;  
        }  
  
        return null;  
    }  
}

timer 是 java 的定時器,用 timer 來實現續期

RedisDistributedLock

lock()會呼叫 tryLock() tryLock()呼叫tryLock(long time, TimeUnit unit)


import cn.hutool.core.util.IdUtil;  
import org.springframework.data.redis.core.StringRedisTemplate;  
import org.springframework.data.redis.core.script.DefaultRedisScript;  
import org.springframework.stereotype.Component;  
  
import java.util.Arrays;  
import java.util.Timer;  
import java.util.TimerTask;  
import java.util.concurrent.TimeUnit;  
import java.util.concurrent.locks.Condition;  
import java.util.concurrent.locks.Lock;  


/**  
 * @auther zzyy  
 * @create 2023-01-09 16:41  
 * 我們自行研發的redis分散式鎖,實作了Lock介面  
 */  
//@Component 引入DistributedLockFactory工廠模式,從工廠取得即可  
public class RedisDistributedLock implements Lock  
{  
    private StringRedisTemplate stringRedisTemplate;  
  
    private String lockName;//KEYS[1]  
    private String uuidValue;//ARGV[1]  
    private long   expireTime;//ARGV[2]  
  
  
    public RedisDistributedLock(StringRedisTemplate stringRedisTemplate, String lockName, String uuid)  
    {  
        this.stringRedisTemplate = stringRedisTemplate;  
        this.lockName = lockName;  
        this.uuidValue = uuid+":"+Thread.currentThread().getId();  
        this.expireTime = 30L;  
    }  
  
  
    @Override  
    public void lock()  
    {  
        tryLock();  
    }  
    @Override  
    public boolean tryLock()  
    {  
        try {tryLock(-1L,TimeUnit.SECONDS);} catch (InterruptedException e) {e.printStackTrace();}  
        return false;  
    }  
  
    @Override  
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException  
    {  
        if(time == -1L)  
        {  
            String script =  
                    "if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then    " +  
                            "redis.call('hincrby',KEYS[1],ARGV[1],1)    " +  
                            "redis.call('expire',KEYS[1],ARGV[2])    " +  
                            "return 1  " +  
                    "else   " +  
                            "return 0 " +  
                    "end";  
            System.out.println("lockName:"+lockName+"\t"+"uuidValue:"+uuidValue);  
            //執行腳本為true成功,但while中為false會跳出迴圈,也就是需要取反  
            while(!stringRedisTemplate.execute(new DefaultRedisScript<>(script,Boolean.class), Arrays.asList(lockName), uuidValue,String.valueOf(expireTime)))  
            {  
                //暫停60毫秒  
                try { TimeUnit.MILLISECONDS.sleep(60); } catch (InterruptedException e) { e.printStackTrace(); }  
            }  
            //新建一個後台掃描程式,來持續key目前的ttl,是否到我們規定的1/2 1/3來實作續期  
            renewExpire();  
            return true;        }  
        return false;  
    }  
  
  
    //執行解鎖腳本  
    @Override  
    public void unlock()  
    {  
        System.out.println("unlock(): lockName:"+lockName+"\t"+"uuidValue:"+uuidValue);  
        String script =  
                "if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 0 then    " +  
                        "return nil  " +  
                "elseif redis.call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then    " +  
                        "return redis.call('del',KEYS[1])  " +  
                "else    " +  
                        "return 0 " +  
                "end";  
        // nil = null 1 = true 0 = false 傳回值有三套值  
        Long flag = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime));  
  
        //如果沒鎖會傳回null,說明逾時,自己的鎖會自動消失,不會影響其他執行緒建立自己的鎖 (key:value,number)  
        if(null == flag)  
        {  
            throw new RuntimeException("this lock doesn't exists,o(╥﹏╥)o");  
        }  
    }  
	  //自動續期
    private void renewExpire()  
  
    {  
        String script =  
                "if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 1 then     " +  
                        "return redis.call('expire',KEYS[1],ARGV[2]) " +  
                "else     " +  
                        "return 0 " +  
                "end";  
        //timer是java的計時器  
        new Timer().schedule(new TimerTask()  
        {  
            @Override  
            public void run()  
            {  
                if (stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime)))  
                {  
                    renewExpire();  
                }  
            }  
        },(this.expireTime * 1000)/3);  
    }  
  
  
    //====下面兩個暫時用不到,不再重寫  
    //不需要考慮中斷,用完就刪就行了  
    @Override  
    public void lockInterruptibly() throws InterruptedException  
    {  
  
    }  
    @Override  
    public Condition newCondition()  
    {  
        return null;  
    }  
}
//V8.0,實作自動續期功能的完善,後台自訂掃描程式,如果規定時間內沒有完成業務邏輯,會呼叫加鐘自動續期的腳本
//V8.0,實作自動續期功能的完善,後台自訂掃描程式,如果規定時間內沒有完成業務邏輯,會呼叫加鐘自動續期的腳本  
public String sale()  
{  
    String retMessage = "";  
  
    Lock redisLock = distributedLockFactory.getDistributedLock("redis");  
    redisLock.lock();  
    try    {  
        //1 查詢庫存資訊  
        String result = stringRedisTemplate.opsForValue().get("inventory001");  
        //2 判斷庫存是否足夠  
        Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);  
        //3 扣減庫存,每次減少一個  
        if(inventoryNumber > 0)  
        {  
            stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));  
            retMessage = "成功卖出一个商品,库存剩余:"+inventoryNumber;  
            System.out.println(retMessage+"\t"+"服务端口号"+port);  
            //暫停120秒鐘執行緒,故意的,示範自動續期的功能。。。。。。  
            try { TimeUnit.SECONDS.sleep(120); } catch (InterruptedException e) { e.printStackTrace(); }  
  
        }else{  
            retMessage = "商品卖完了,o(╥﹏╥)o";  
        }  
    }finally {  
        redisLock.unlock();  
    }  
    return retMessage+"\t"+"服务端口号"+port;  
}

CAP

assets/Redis高级篇/image-20240303180011939.png

面試

如何撰寫?

首先按照 Java 中 juc 中的 LOCK 規範實作方法,

上鎖邏輯 : 使用 Hset , KKV 結構來進行加鎖,透過 while 以一定間隔, 重試,不斷 自旋 來判斷是否能夠達到鎖,透過 lua 腳本來保證原子性,透過續期來保證時間長的業務能夠自動續期順利完成業務 assets/Redis高级篇/image-20240303202214402.png

解鎖邏輯 : 確保保證是自己的鎖,只能自己刪除自己的鎖,透過鎖的計數器,為 0 才可以刪除

assets/Redis高级篇/image-20240303202226298.png

使用Redis實作分散式鎖 | Redis --- Distributed Locks with Redis | Redis

Redlock 演算法和底層原始碼分析

使用Redis实现分布式锁 _ Redis --- Distributed Locks with Redis _ Redis (03_03_2024 21_53_05) ↗

redisson/redisson

單點故障

單點故障: 由於之前 redis 中複製是異步的,突然宕機會出現鎖消失的問題,執行緒一拿到鎖後,master 還未將鎖寫入 slave 後就掛了,然後 slave 被選舉為了 master,執行緒二看到新的 master 沒有鎖後就建立了自己的鎖,此時出現兩個以上執行緒拿到了鎖,這種情況是嚴禁的,解決方法就是捨棄 slave 節點,而是建立鎖節點,只能從鎖節點取得鎖

assets/Redis高级篇/image-20240303214357337.png

assets/Redis高级篇/image-20240303214717045.png assets/Redis高级篇/image-20240303180011939.png

Redlock

8. 分散式鎖與同步器 · redisson/redisson Wiki (github.com)

redisson/redisson: Redisson 使用多個 reids,每個都是 master,沒有使用複製或任何其他隱式協調系統

Redlock

第二步就是依次嘗試在每個 Redis 執行個體上取得鎖,如果超過逾時時間就直接進行下一次 Redis 取得,最後嘗試完所有的 Redis 所消耗的時間,減去第一步消耗的時間就是取得鎖的耗時

assets/Redis高级篇/image-20240303220712637.png assets/Redis高级篇/image-20240303220724870.png

容錯公式

2x+1

x 為掛掉的個數

assets/Redis高级篇/image-20240303220943011.png

設定

1. 修改pom

        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.19.1</version>
        </dependency>


2. 在configuration中redisConfig中的設定,或者設定yaml)

@Bean  
public Redisson redisson()  
{  
    Config config = new Config();  
    //新增多台,這裡只是示範
    config.useSingleServer().setAddress("redis://192.168.111.185:6379").setDatabase(0).setPassword("111111");  
    
  // or read config from file
//config = Config.fromYAML(new File("config-file.yaml")); 

    return (Redisson) Redisson.create(config);  
}

  • 問題

redlock 也要判斷是否是自己的鎖

//V9.1,引入 Redisson 對應的官網推薦 RedLock 演算法實作類別

@Autowired  
private StringRedisTemplate stringRedisTemplate;

@Value("${server.port}")  
private String port;

@Autowired
private DistributedLockFactory distributedLockFactory;  

//V9.1,引入Redisson對應的官網推薦RedLock演算法實作類別
@Autowired  
private Redisson redisson;  
public String saleByRedisson()  
{  
    String retMessage = "";  
  
    RLock redissonLock = redisson.getLock("zzyyRedisLock");  
    redissonLock.lock();  
  
    try{
        //1 查詢庫存資訊
        String result = stringRedisTemplate.opsForValue().get("inventory001");  
        //2 判斷庫存是否足夠
        Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);  
        //3 扣減庫存,每次減少一個
        if(inventoryNumber > 0)  
        {  
            stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));  
            retMessage = "成功卖出一个商品,库存剩余:"+inventoryNumber;  
            System.out.println(retMessage+"\t"+"服务端口号"+port);  
        }else{  
            retMessage = "商品卖完了,o(╥﹏╥)o";  
        }  
    }finally {  
        //改進點,只能刪除屬於自己的key,不能刪除別人的  
        if(redissonLock.isLocked() && redissonLock.isHeldByCurrentThread())  
        {  
            redissonLock.unlock();  
        }  
    }  
    return retMessage+"\t"+"服务端口号"+port;  
}

原始碼解析

依舊是加鎖,可重入,續命,解鎖

redissonLock 預設過期時間 30 秒,

java

新 new 出的預設 30 秒 assets/Redis高级篇/image-20240305160545983.png assets/Redis高级篇/image-20240305160552204.png

加鎖過程

scheduleExpriationRenewal給快取續期

assets/Redis高级篇/image-20240305160911218.png assets/Redis高级篇/image-20240305161528860.png

續期

assets/Redis高级篇/image-20240305161545972.png assets/Redis高级篇/image-20240324192852271.png

assets/Redis高级篇/image-20240324192832406.png

解鎖

assets/Redis高级篇/image-20240324192923778.png

多機案例

基於容錯移轉實作的 redis 主從無法實現真正的 redlock

java

assets/Redis高级篇/image-20240324204344371.png assets/Redis高级篇/image-20240324204356828.png


        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.19.1</version>
        </dependency>


server.port=9090
spring.application.name=redlock


spring.swagger2.enabled=true


spring.redis.database=0
spring.redis.password=
spring.redis.timeout=3000
spring.redis.mode=single
//池化技術
spring.redis.pool.conn-timeout=3000
spring.redis.pool.so-timeout=3000
spring.redis.pool.size=10

spring.redis.single.address1=192.168.111.185:6381
spring.redis.single.address2=192.168.111.185:6382
spring.redis.single.address3=192.168.111.185:6383

Redis 快取過期淘汰策略

assets/Redis高级篇/image-20240324221518475.png

記憶體設定

assets/Redis高级篇/image-20240324223009410.png

如果不設定或者設定為 0, 在 64 位元作業系統下不限制記憶體大小,在 32 位元作業系統最多使用 3GB ,一般推薦設定 Redis 記憶體為最大實體記憶體的四分之三

maxmemory maxmemory 128mb 超出記憶體會爆 OOM

修改 : 設定檔/命令 config set maxmemory xxx

查看 : info memory / config get maxmemory

過期鍵的刪除策略

如果一個鍵是過期的,那它到了過期時間之後是不是馬上就從記憶體中被刪除呢?

立即刪除

對 CPU 不好,用處理器效能換取儲存空間 (時間換空間)

assets/Redis高级篇/image-20240324223716667.png

惰性刪除

開啟 : lazyfree-lazy-eviction=yes

對記憶體不友善,用儲存空間換取處理器效能 (空間換時間)

只掃描被存取的 key,沒有被存取的 key 即使過期也不知道,不會被刪除 assets/Redis高级篇/image-20240324223731390.png

定期刪除

定期刪除是前兩種檢查策略的折衷

每隔一段時間抽取 一部分key 檢查是否有過期的 key,會產生漏網之魚

assets/Redis高级篇/image-20240324230500854.png

快取淘汰策略

lru 和 lfu 演算法的區別是什麼

LRU:最近 最少使用 頁面置換演算法,使用時間上來講 LFU:最近 最不常用 頁面置換演算法,頻率上來講

assets/Redis高级篇/image-20240325191116616.png

一共八種 兩個維度 : 過期鍵中篩選/所有鍵中篩選 四個方面 : LRU/LFU/random/ttl

assets/Redis高级篇/image-20240325192158807.png assets/Redis高级篇/image-20240325192218356.png

配置

assets/Redis高级篇/image-20240325192428595.png

快取配置建議

assets/Redis高级篇/image-20240325192547187.png

Redis 經典五大類型原始碼及底層實作

這一章完全是為了面試,沒有太多學習的意義

面試

assets/Redis高级篇/image-20240325195618815.png assets/Redis高级篇/image-20240325195628669.png assets/Redis高级篇/image-20240325195638618.png

assets/Redis高级篇/image-20240325195646954.png

高效能設計之 epoll 和 IO 多路複用深度解析

總結

資料

  1. 起步思維: 官網
  2. 排除故障思維 : 日誌

Redis脑图(基础篇+高级篇) ↗

^u0ikb3

2020 尚硅谷 Java 大廠面試題

尚硅谷 50 道 Java 面試題