Redis链表结构深入
鏈表結(jié)構(gòu)是 Redis 中一個(gè)常用的結(jié)構(gòu),它可以存儲(chǔ)多個(gè)字符串,而且它是有序的,能夠存儲(chǔ) 2 的 32 次方減 1 個(gè)節(jié)點(diǎn)(超過(guò) 40 億個(gè)節(jié)點(diǎn))。
Redis 鏈表是雙向的,因此即可以從左到右,也可以從右到左遍歷它存儲(chǔ)的節(jié)點(diǎn),鏈表結(jié)構(gòu)如下圖所示。
由于是雙向鏈表,所以只能夠從左到右,或者從右到左地訪(fǎng)問(wèn)和操作鏈表里面的數(shù)據(jù)節(jié)點(diǎn)。但是使用鏈表結(jié)構(gòu)就意味著讀性能的喪失,所以要在大量數(shù)據(jù)中找到一個(gè)節(jié)點(diǎn)的操作性能是不佳的,因?yàn)殒湵碇荒軓囊粋€(gè)方向中去遍歷所要節(jié)點(diǎn)。
比如從查找節(jié)點(diǎn) 10 000 開(kāi)始查詢(xún),它需要按照節(jié)點(diǎn) 1、節(jié)點(diǎn) 2、節(jié)點(diǎn) 3……直至節(jié)點(diǎn) 10 000,這樣的順序查找,然后把一個(gè)個(gè)節(jié)點(diǎn)和你給出的值比對(duì),才能確定節(jié)點(diǎn)所在。如果這個(gè)鏈表很大,如有上百萬(wàn)個(gè)節(jié)點(diǎn),可能需要遍歷幾十萬(wàn)次才能找到所需要的節(jié)點(diǎn),顯然查找性能是不佳的。
而鏈表結(jié)構(gòu)的優(yōu)勢(shì)在于插入和刪除的便利,因?yàn)殒湵淼臄?shù)據(jù)節(jié)點(diǎn)是分配在不同的內(nèi)存區(qū)域的,并不連續(xù),只是根據(jù)上一個(gè)節(jié)點(diǎn)保存下一個(gè)節(jié)點(diǎn)的順序來(lái)索引而已,無(wú)需移動(dòng)元素。其新增和刪除的操作如下圖所示。
上述的的阿拉伯?dāng)?shù)字代表新增的步驟,而漢字?jǐn)?shù)字代表刪除步驟。
1 新增節(jié)點(diǎn)
對(duì)插入圖中的節(jié)點(diǎn) 4 而言,先看從左到右的指向,先讓節(jié)點(diǎn) 4 指向節(jié)點(diǎn) 1 原來(lái)的下一個(gè)節(jié)點(diǎn),也就是節(jié)點(diǎn) 2,然后讓節(jié)點(diǎn) 1 指向節(jié)點(diǎn) 4,這樣就完成了從右到左的指向修改。再看從右到左,先讓節(jié)點(diǎn) 4 指向節(jié)點(diǎn) 1,然后節(jié)點(diǎn) 2 指向節(jié)點(diǎn) 4,這個(gè)時(shí)候就完成了從右到左的指向,那么節(jié)點(diǎn) 1 和節(jié)點(diǎn) 2 之間的原有關(guān)聯(lián)關(guān)系都已經(jīng)失效,這樣就完成了在鏈表中新增節(jié)點(diǎn)4的功能。
2 刪除節(jié)點(diǎn)
對(duì)刪除圖中的節(jié)點(diǎn) 3 而言,首先讓節(jié)點(diǎn) 2 從左到右指向后續(xù)節(jié)點(diǎn),然后讓后續(xù)節(jié)點(diǎn)指向節(jié)點(diǎn) 2,這樣節(jié)點(diǎn) 3 就脫離了鏈表,也就是斷絕了與節(jié)點(diǎn) 2 和后繼節(jié)點(diǎn)的關(guān)聯(lián)關(guān)系,然后對(duì)節(jié)點(diǎn) 3 進(jìn)行內(nèi)存回收,無(wú)須移動(dòng)任何節(jié)點(diǎn),就完成了刪除。
由此可見(jiàn),鏈表結(jié)構(gòu)的使用是需要注意場(chǎng)景的,對(duì)于那些經(jīng)常需要對(duì)數(shù)據(jù)進(jìn)行插入和刪除的列表數(shù)據(jù)使用它是十分方便的,因?yàn)樗梢栽诓灰苿?dòng)其他節(jié)點(diǎn)的情況下完成插入和刪除。而對(duì)于需要經(jīng)常查找的,使用它性能并不佳,它只能從左到右或者從右到左的查找和比對(duì)。
因?yàn)槭请p向鏈表結(jié)構(gòu),所以 Redis 鏈表命令分為左操作和右操作兩種命令,左操作就意味著是從左到右,右操作就意味著是從右到左。
Redis關(guān)于鏈表的命令
上表所示的鏈表命令,其中以“l(fā)”開(kāi)頭的代表左操作,以“r”開(kāi)頭的代表右操作。對(duì)于很多個(gè)節(jié)點(diǎn)同時(shí)操作的,需要考慮其花費(fèi)的時(shí)間,鏈表數(shù)據(jù)結(jié)構(gòu)對(duì)于查找而言并不適合于大數(shù)據(jù),而 Redis 也給了比較靈活的命令對(duì)其進(jìn)行操作。
Redis關(guān)于鏈表的操作命令
這里展示了關(guān)于 Redis 鏈表的常用命令,只是對(duì)于大量數(shù)據(jù)操作的時(shí)候,我們需要考慮插入和刪除內(nèi)容的大小,因?yàn)檫@將是十分消耗性能的命令,會(huì)導(dǎo)致 Redis 服務(wù)器的卡頓。對(duì)于不允許卡頓的一些服務(wù)器,可以進(jìn)行分批次操作,以避免出現(xiàn)卡頓。
需要指出的是,之前這些操作鏈表的命令都是進(jìn)程不安全的,因?yàn)楫?dāng)我們操作這些命令的時(shí)候,其他 Redis 的客戶(hù)端也可能操作同一個(gè)鏈表,這樣就會(huì)造成并發(fā)數(shù)據(jù)安全和一致性的問(wèn)題,尤其是當(dāng)你操作一個(gè)數(shù)據(jù)量不小的鏈表結(jié)構(gòu)時(shí),常常會(huì)遇到這樣的問(wèn)題。
為了克服這些問(wèn)題,Redis 提供了鏈表的阻塞命令,它們?cè)谶\(yùn)行的時(shí)候,會(huì)給鏈表加鎖,以保證操作鏈表的命令安全性,如下表所示。
鏈表的阻塞命令
當(dāng)使用這些命令時(shí),Redis 就會(huì)對(duì)對(duì)應(yīng)的鏈表加鎖,加鎖的結(jié)果就是其他的進(jìn)程不能再讀取或者寫(xiě)入該鏈表,只能等待命令結(jié)束。加鎖的好處可以保證在多線(xiàn)程并發(fā)環(huán)境中數(shù)據(jù)的一致性,保證一些重要數(shù)據(jù)的一致性,比如賬戶(hù)的金額、商品的數(shù)量。
不過(guò)在保證這些的同時(shí)也要付出其他線(xiàn)程等待、線(xiàn)程環(huán)境切換等代價(jià),這將使得系統(tǒng)的并發(fā)能力下降。
Redis 鏈表阻塞操作命令
在項(xiàng)目中,雖然阻塞可以有效保證了數(shù)據(jù)的一致性,但是阻塞就意味著其他進(jìn)程的等待,CPU 需要給其他線(xiàn)程掛起、恢復(fù)等操作,更多的時(shí)候我們希望的并不是阻塞的處理請(qǐng)求,所以這些命令在實(shí)際中使用得并不多。
使用 Spring 去操作 Redis 鏈表的命令,這里繼續(xù)保持代碼關(guān)于 RedisTemplate 的配置,在此基礎(chǔ)上獲取 RedisTemplate 對(duì)象,然后輸入以下代碼。
public static void testList() {ApplicationContext applicationcontext = new ClassPathXmlApplicationContext("applicationContext.xml");RedisTemplate redisTemplate = applicationcontext.getBean(RedisTemplate.class);try {//刪除鏈表,以便我們可以反復(fù)測(cè)試redisTemplate.delete("list");//把node3插入鏈表listredisTemplate. opsForList ().leftPush ("list", "node3");List<String> nodeList = new ArrayList<String>();for (int i = 2; i >= 1; i--){nodeList.add("nnode" + i);}//相當(dāng)于lpush把多個(gè)價(jià)值從左插入鏈表redisTemplate.opsForList().leftPushAll("list", nodeList);//從右邊插入一個(gè)節(jié)點(diǎn)redisTemplate.opsForList().rightPush("list", "node4");//獲取下標(biāo)為0的節(jié)點(diǎn)String nodel = (String) redisTemplate.opsForList() .index("list", 0);//獲取鏈表長(zhǎng)度long size = redisTemplate.opsForList ().size ("listn");//從左邊彈出一個(gè)節(jié)點(diǎn)String lpop = (String) redisTemplate.opsForList().leftPop("list");//從右邊彈出一個(gè)節(jié)點(diǎn)String rpop = (String) redisTemplate.opsForList().rightPop("list");//注意,需要使用更為底層的命令才能操作linsert命令//使用linsert命令在node2前插入一個(gè)節(jié)點(diǎn)redisTemplate.getConnectionFactory().getConnection().lInsert("list".getBytes("utf-8"),RedisListCommands.Position.BEFORE,"node2".getBytes("utf-8"),"before_node".getBytes("utf-8"));//使用linsert命令在node2后插入一個(gè)節(jié)點(diǎn)redisTemplate.getConnectionFactory().getConnection().linsert("list".getBytes("utf-8"),RedisListCommands.Position.AFTER,"node2".getBytes("utf-8"), "after_node".getBytes("utf-8"));//判斷l(xiāng)ist是否存在,如果存在則從左邊插入head節(jié)點(diǎn)redisTemplate.opsForList().leftPushlfPresent("list", "head");//判斷l(xiāng)ist是否存在,如果存在則從右邊插入end節(jié)點(diǎn)redisTemplate.opsForList().rightPushlfPresent("list", "end");//從左到右,或者下標(biāo)從0到10的節(jié)點(diǎn)元素List valueList = redisTemplate.opsForList().range("list", 0, 10);nodeList.clear();for (int i = 1; i <= 3; i++) {nodeList.add("node");}//在鏈表左邊插入三個(gè)值為node的節(jié)點(diǎn)redisTemplate.opsForList().leftPushAll.("list", nodeList);//從左到右刪除至多三個(gè)node節(jié)點(diǎn)redisTemplate.opsForList().remove("list", 3,"node");//給鏈表下標(biāo)為0的節(jié)點(diǎn)設(shè)置新值redisTemplate.opsForList().set("list",0, "new_head_value");} catch (UnsupportedEncodingException ex) {ex.printStackTrace();}//打印鏈表數(shù)據(jù)printList(redisTemplate, "list"); } public static void printList(RedisTemplate redisTemplate, String key) { //鏈表長(zhǎng)度Long size = redisTemplate.opsForList().size(key); }這里所展示的是 RedisTemplate 對(duì)于 Redis 鏈表的操作,其中 left 代表左操作,right 代表右操作。有些命令 Spring 所提供的 RedisTemplate 并不能支持,比如 linsert 命令,這個(gè)時(shí)候可以使用更為底層的方法去操作,正如代碼中的這段:
// 使用linsert命令在node2前插入一個(gè)節(jié)點(diǎn) redisTemplate.getConnectionFactory().getConnection().lInsert("list".getBytes("utf-8"), RedisListCommands.Position.BEFORE,"node2".getBytes("utf-8"), "before_node".getBytes("utf-8"));在多值操作的時(shí)候,往往會(huì)使用 list 進(jìn)行封裝,比如 leftPushAll 方法,對(duì)于很大的 list 的操作需要注意性能,比如 remove 這樣的操作,在大的鏈表中會(huì)消耗 Redis 系統(tǒng)很多的性能。
Redis 還有對(duì)鏈表進(jìn)行阻塞操作的命令,這里 Spring 也給出了支持,代碼如下所示。
public static void testBList() {ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");RedisTemplate redisTemplate = applicationContext.getBean(RedisTemplate.class);// 清空數(shù)據(jù),可以重復(fù)測(cè)試redisTemplate.delete ("list1");redisTemplate.delete ("list2");//初始化鏈表 list1List<String> nodeList = new ArrayList<String>();for (int i=1; i<=5; i++) {nodeList.add("node" + i);}redisTemplate.opsForList().leftPushAll("list1", nodeList);// Spring 使用參數(shù)超時(shí)時(shí)間作為阻塞命令區(qū)分,等價(jià)于 blpop 命令,并且可以設(shè)置時(shí)間參數(shù) redisTemplate.opsForList().leftPop ("list1", 1, TimeUnit.SECONDS);// Spring 使用參數(shù)超時(shí)時(shí)間作為阻塞命令區(qū)分,等價(jià)于 brpop 命令,并且可以設(shè)置時(shí)間參數(shù)redisTemplate.opsForList().rightPop("list1", 1, TimeUnit.SECONDS);nodeList.clear();// 初始化鏈表 list2for (int i=1; i<=3; i++) {nodeList.add("dato" + i);}redisTemplate.opsForList().leftPushAll("list2", nodeList);// 相當(dāng)于 rpoplpush 命令,彈出 list1 最右邊的節(jié)點(diǎn),插入到 list2 最左邊redisTemplate.opsForList().rightPopAndLeftPush("list1","list2");// 相當(dāng)于brpoplpush命令,注意在 Spring 中使用超時(shí)參數(shù)區(qū)分 redisTemplate.opsForList().rightPopAndLeftPush("list1", "list2",1,TimeUnit.SECONDS);// 打印鏈表數(shù)據(jù)printList(redisTemplate, "list1");printList(redisTemplate, "list2"); }這里展示了 Redis 關(guān)于鏈表的阻塞命令,在 Spring 中它和非阻塞命令的方法是一致的,只是它會(huì)通過(guò)超時(shí)參數(shù)進(jìn)行區(qū)分,而且我們還可以通過(guò)方法設(shè)置時(shí)間的單位,使用還是相當(dāng)簡(jiǎn)單的。注意,它是阻塞的命令,在多線(xiàn)程的環(huán)境中,它能在一定程度上保證數(shù)據(jù)的一致而性能卻不佳。
總結(jié)
以上是生活随笔為你收集整理的Redis链表结构深入的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: MySQL 删除数据
- 下一篇: C++ 重载左移和右移运算符