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

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

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

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

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

(2)接著我們創(chuàng)建帶分布式鎖的示列,我們先來看看分布式鎖創(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操作類
- 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. # 設置鎖
- 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設置一個超時時間,這個相當于鎖的有效時間
- 32. # 如果沒有超時時間則會導致程序死鎖
- 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. # 如果不能獲取鎖資源則線程一直掛起直到獲取鎖資源或者超時
- 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)異常時主動釋放鎖資源
- 82. release_lock(lock_name, identifier)
- 83. raise Exception(e.args)
- 84. return result
- 85. return inner_func
- 86. return _outfunc
(3)最后我們在計算函數(shù)中增加分布式鎖裝飾器,然后查看程序運行結果是否符合預期,具體如下


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