程序員必懂的Redis技術(shù)實(shí)戰(zhàn)
Redis是現(xiàn)在很受歡迎的NoSQL數(shù)據(jù)庫(kù)之一,目前廣泛用于緩存系統(tǒng)、分布式鎖、計(jì)數(shù)器、消息隊(duì)列系統(tǒng)、排行榜、社交網(wǎng)絡(luò)等場(chǎng)景中,本篇文章成哥為大家?guī)?lái)redis日常使用實(shí)踐,及通過(guò)代碼實(shí)現(xiàn)redis的分布式鎖。
01 Redis簡(jiǎn)介
Redis是一個(gè)開(kāi)源使用ANSI C語(yǔ)言編寫(xiě)、遵守BSD協(xié)議、支持網(wǎng)絡(luò)、可基于內(nèi)存亦可以持久化的日志類(lèi)型、key-value數(shù)據(jù)庫(kù),并提供多種語(yǔ)言的API。Redis的出現(xiàn),很大程度上彌補(bǔ)了Memcache這類(lèi)key/value存儲(chǔ)的不足,在部分場(chǎng)合可以對(duì)關(guān)系型數(shù)據(jù)庫(kù)(MySql、DB2等,關(guān)系型數(shù)據(jù)庫(kù)通過(guò)外鍵關(guān)聯(lián)來(lái)建立表與表之間的關(guān)系。非關(guān)系型數(shù)據(jù)庫(kù)通常指數(shù)據(jù)以對(duì)象的形式存儲(chǔ)在數(shù)據(jù)庫(kù)中,而對(duì)象之間的關(guān)系通過(guò)每個(gè)對(duì)象自身的屬性來(lái)決定)起到很好的補(bǔ)充作用。(如可降低數(shù)據(jù)庫(kù)訪問(wèn)壓力,弊端冷數(shù)據(jù)的處理)。
02 Redis實(shí)現(xiàn)特點(diǎn)
(1)單線程
Redis是通過(guò)單線程實(shí)現(xiàn)的,單線程避免了多線程的切換性能損耗問(wèn)題,同時(shí)它所有的數(shù)據(jù)都在內(nèi)存中,所有的運(yùn)算都是內(nèi)存級(jí)別的運(yùn)算,所以即使是單線程redis還能這么快。但也正是因?yàn)槭褂玫氖菃尉€程,所以要小心使用 Redis 指令,對(duì)于那些耗時(shí)的指令(比如keys),一定要謹(jǐn)慎使用,一不小心就可能會(huì)導(dǎo)致 Redis 卡頓。
(2)IO多路復(fù)用
Redis通過(guò)IO多路復(fù)用解決單線程下并發(fā)客戶(hù)端的訪問(wèn),redis利用epoll來(lái)實(shí)現(xiàn)IO多路復(fù)用,將連接信息和事件放到隊(duì)列中,依次放到文件事件分派器,事件分派器將事件分發(fā)給事件處理器。具體架構(gòu)如下:

03 Redis集群方案比較
(1)哨兵模式

在redis3.0以前的版本要實(shí)現(xiàn)集群一般是借助哨兵sentinel工具來(lái)監(jiān)控master節(jié)點(diǎn)的狀態(tài),如果master節(jié)點(diǎn)異常,則會(huì)做主從切換,將某一臺(tái)slave作為master,哨兵的配置略微復(fù)雜,并且性能和高可用性等各方面表現(xiàn)一般,特別是在主從切換的瞬間存在訪問(wèn)瞬斷的情況,而且哨兵模式只有一個(gè)主節(jié)點(diǎn)對(duì)外提供服務(wù),沒(méi)法支持很高的并發(fā),且單個(gè)主節(jié)點(diǎn)內(nèi)存也不宜設(shè)置得過(guò)大,否則會(huì)導(dǎo)致持久化文件過(guò)大,影響數(shù)據(jù)恢復(fù)或主從同步的效率 。
(2)高可用集群模式

redis高可用集群是一個(gè)由多個(gè)主從節(jié)點(diǎn)群組成的分布式服務(wù)器群,它具有復(fù)制、高可用和分片特性。Redis集群不需要sentinel哨兵也能完成節(jié)點(diǎn)移除和故障轉(zhuǎn)移的功能。需要將每個(gè)節(jié)點(diǎn)設(shè)置成集群模式,這種集群模式?jīng)]有中心節(jié)點(diǎn),可水平擴(kuò)展,據(jù)官方文檔稱(chēng)可以線性擴(kuò)展到上萬(wàn)個(gè)節(jié)點(diǎn)(官方推薦不超過(guò)1000個(gè)節(jié)點(diǎn))。redis集群的性能和高可用性均優(yōu)于之前版本的哨兵模式,且集群配置非常簡(jiǎn)單。
04 Redis集群部署常見(jiàn)問(wèn)題
在了解Redis集群部署常見(jiàn)問(wèn)題之前我們先來(lái)了解一下Redis集群的實(shí)現(xiàn)原理。Redis Cluster 將所有數(shù)據(jù)劃分為 16384 的 slots(槽位),每個(gè)節(jié)點(diǎn)負(fù)責(zé)其中一部分槽位。槽位的信息存儲(chǔ)于每個(gè)節(jié)點(diǎn)中,當(dāng) Redis Cluster 的客戶(hù)端來(lái)連接集群時(shí),它也會(huì)得到一份集群的槽位配置信息并將其緩存在客戶(hù)端本地。這樣當(dāng)客戶(hù)端要查找某個(gè) key 時(shí),可以直接定位到目標(biāo)節(jié)點(diǎn)。同時(shí)因?yàn)椴畚坏男畔⒖赡軙?huì)存在客戶(hù)端與服務(wù)器不一致的情況,還需要糾正機(jī)制來(lái)實(shí)現(xiàn)槽位信息的校驗(yàn)調(diào)整。
(1)跳轉(zhuǎn)重定位問(wèn)題
當(dāng)客戶(hù)端向一個(gè)錯(cuò)誤的節(jié)點(diǎn)發(fā)出了指令,該節(jié)點(diǎn)會(huì)發(fā)現(xiàn)指令的 key 所在的槽位并不歸自己管理,這時(shí)它會(huì)向客戶(hù)端發(fā)送一個(gè)特殊的跳轉(zhuǎn)指令攜帶目標(biāo)操作的節(jié)點(diǎn)地址,告訴客戶(hù)端去連這個(gè)節(jié)點(diǎn)去獲取數(shù)據(jù)??蛻?hù)端收到指令后除了跳轉(zhuǎn)到正確的節(jié)點(diǎn)上去操作,還會(huì)同步更新糾正本地的槽位映射表緩存,后續(xù)所有 key 將使用新的槽位映射表。
(2)網(wǎng)絡(luò)抖動(dòng)問(wèn)題
在生產(chǎn)環(huán)境中網(wǎng)絡(luò)抖動(dòng)問(wèn)題不可避免,為解決這種問(wèn)題,Redis Cluster 提供了一種選項(xiàng)clusternodetimeout,表示當(dāng)某個(gè)節(jié)點(diǎn)持續(xù) timeout 的時(shí)間失聯(lián)時(shí),才可以認(rèn)定該節(jié)點(diǎn)出現(xiàn)故障,需要進(jìn)行主從切換。如果沒(méi)有這個(gè)選項(xiàng),網(wǎng)絡(luò)抖動(dòng)會(huì)導(dǎo)致主從頻繁切換 (數(shù)據(jù)的重新復(fù)制)。
05 代碼實(shí)現(xiàn)基于Redis的分布式鎖
在多個(gè)進(jìn)程/線程對(duì)同一個(gè)共享資源讀寫(xiě)場(chǎng)景下,會(huì)因?yàn)橘Y源的爭(zhēng)奪而出現(xiàn)混亂,導(dǎo)致數(shù)據(jù)不一致。為了避免該問(wèn)題我們可以在進(jìn)程/線程在操作共享資源前獲取一個(gè)令牌(也就鎖),只有獲取了該令牌的進(jìn)程/線程才可以操作資源,在操作完資源后釋放該令牌。這就實(shí)現(xiàn)了分布式鎖。
Redis的分布式鎖是基于Redis SETNX命令來(lái)實(shí)現(xiàn)的,在Redis中通過(guò)SETNX命令設(shè)置Key Value時(shí)有如下兩種結(jié)果:
1)返回1,表示為指定的key設(shè)置值成功,也即表示當(dāng)前進(jìn)程已經(jīng)獲取了鎖資源
2)返回0,表示為指定的key設(shè)置值失敗,因?yàn)楫?dāng)前已存在該key,也即表示其它進(jìn)程獲取了鎖資源
下面我們就來(lái)看看怎么通過(guò)python實(shí)現(xiàn)分布式鎖吧
(1)首先我們創(chuàng)建一個(gè)不使用分布式鎖的示列,通過(guò)多線程對(duì)全局變量進(jìn)行加1操作看看結(jié)果如何,具體代碼如下:

代碼運(yùn)行結(jié)果如下,發(fā)現(xiàn)不是我們預(yù)期的值(預(yù)期值應(yīng)為3+3=6)

(2)接著我們創(chuàng)建帶分布式鎖的示列,我們先來(lái)看看分布式鎖創(chuàng)建的方法,具體如下
- 1. import time
- 2. import uuid
- 3. from redis import StrictRedis, ConnectionPool
- 4. import threading
- 5.
- 6. class CollectRedis:
- 7. # 創(chuàng)建redis操作類(lèi)
- 8. def __init__(self):
- 9. self.host = "1.1.1.1"
- 10. self.port = 6379
- 11. self.db = 5
- 12.
- 13. @property
- 14. def redis_session(self):
- 15. _session = getattr(self, "__redis_session", None)
- 16. if _session:
- 17. return _session
- 18. redis_pool = ConnectionPool(host=self.host, port=self.port, db=self.db)
- 19. _session = StrictRedis(connection_pool=redis_pool)
- 20. setattr(self, "__redis_session", _session)
- 21. return _session
- 22.
- 23. # 獲取鎖
- 24. def get_lock(self, lock_key):
- 25. return self.redis_session.get(lock_key)
- 26.
- 27. # 設(shè)置鎖
- 28. def set_lock(self, lock_key, value, timeout=300):
- 29. session = self.redis_session
- 30. tag = session.setnx(lock_key, value)
- 31. # 如果key能創(chuàng)建成功則為該key設(shè)置一個(gè)超時(shí)時(shí)間,這個(gè)相當(dāng)于鎖的有效時(shí)間
- 32. # 如果沒(méi)有超時(shí)時(shí)間則會(huì)導(dǎo)致程序死鎖
- 33. if tag:
- 34. session.expire(lock_key, timeout)
- 35. return tag
- 36.
- 37. # 刪除鎖也就是釋放鎖
- 38. def delete_lock(self, lock_key):
- 39. return self.redis_session.delete(lock_key)
- 40.
- 41. # 獲取鎖資源方法
- 42. def acquire_lock(lock_name, time_out=300):
- 43. identifier = str(uuid.uuid4())
- 44. end = time.time() + time_out + 30
- 45. redis_connect = CollectRedis()
- 46. # 如果不能獲取鎖資源則線程一直掛起直到獲取鎖資源或者超時(shí)
- 47. while time.time() < end:
- 48. if redis_connect.set_lock(lock_name, identifier, timeout=time_out):
- 49. return identifier
- 50. time.sleep(0.01)
- 51. return False
- 52.
- 53. # 釋放鎖資源
- 54. def release_lock(lock_name, identifier):
- 55. redis_connect = CollectRedis()
- 56. value = redis_connect.get_lock(lock_name)
- 57. if not value:
- 58. return True
- 59. if value == identifier:
- 60. redis_connect.delete_lock(lock_name)
- 61. return True
- 62. return False
- 63.
- 64.
- 65. def resource_lock(lock_name, timeout=10):
- 66. """
- 67. 并發(fā)鎖裝飾器函數(shù)
- 68. :param lock_name:
- 69. :param timeout:
- 70. :return:
- 71. """
- 72. def _outfunc(func):
- 73. def inner_func(*args, **kwargs):
- 74. identifier = acquire_lock(lock_name, time_out=timeout)
- 75. if not identifier:
- 76. raise Exception("獲取({})鎖資源失敗".format(lock_name))
- 77. try:
- 78. result = func(*args, **kwargs)
- 79. release_lock(lock_name, identifier)
- 80. except Exception as e:
- 81. # 程序出現(xiàn)異常時(shí)主動(dòng)釋放鎖資源
- 82. release_lock(lock_name, identifier)
- 83. raise Exception(e.args)
- 84. return result
- 85. return inner_func
- 86. return _outfunc
(3)最后我們?cè)谟?jì)算函數(shù)中增加分布式鎖裝飾器,然后查看程序運(yùn)行結(jié)果是否符合預(yù)期,具體如下


06 總結(jié)
本篇文章主要帶大家了解了Redis的一些特點(diǎn)、部署方案、集群中容器遇到的問(wèn)題及如何基于redis實(shí)現(xiàn)分布式鎖等內(nèi)容,如果喜歡本篇文章不要忘了點(diǎn)贊、關(guān)注與轉(zhuǎn)發(fā)哦!