使用redis實現(xiàn)分布式鎖
redis特性介紹
1、支持豐富的數(shù)據(jù)類型,如String、List、Map、Set、ZSet等。
2、支持數(shù)據(jù)持久化,RDB和AOF兩種方式
3、支持集群工作模式,分區(qū)容錯性強
4、單線程,順序處理命令
5、支持事務(wù)
6、支持發(fā)布與訂閱
Redis實現(xiàn)分布式鎖使用了SETNX命令:
SETNX key value
將key的值設(shè)為value?,當且僅當key不存在。
若給定的key已經(jīng)存在,則SETNX不做任何動作。
SETNX?是『SET if Not eXists』(如果不存在,則 SET)的簡寫。
可用版本:>= 1.0.0時間復雜度:O(1)返回值:
設(shè)置成功,返回?1?。
設(shè)置失敗,返回?0?。
redis>?EXISTS?job????????????????#?job?不存在 (integer)?0 redis>?SETNX?job?"programmer"????#?job?設(shè)置成功 (integer)?1 redis>?SETNX?job?"code-farmer"???#?嘗試覆蓋?job?,失敗 (integer)?0 redis>?GET?job???????????????????#?沒有被覆蓋 "programmer"
首先,我們需要封裝一個公共的Redis訪問工具類。該類需要注入RedisTemplate實例和ValueOperations實例,使用ValueOperations實例是因為Redis實現(xiàn)的分布式鎖使用了最簡單的String類型。另外,我們需要封裝3個方法,分別是setIfObsent (String key, String value)、 expire (String key, long timeout, TimeUnit unit) 、delete (String key) ,分別對應Redis的SETNX、expire、del命令。以下是Redis訪問工具類的具體實現(xiàn):
@Component public?class?RedisDao?{ @Autowired private?RedisTemplate?redisTemplate; @Resource(name="redisTemplate") private?ValueOperations<object>?valOpsObj; /** ?*?如果key不存在,就存儲一個key-value,相當于SETNX命令 ?*?@param?key??????鍵 ?*?@param?value????值,可以為空 ?*?@return ?*/ public?boolean?setIfObsent?(String?key,?String?value)?{ return?valOpsObj.setIfAbsent(key,?value); } /** ?*?為key設(shè)置失效時間 ?*?@param?key???????鍵 ?*?@param?timeout???時間大小 ?*?@param?unit??????時間單位 ?*/ public?boolean?expire?(String?key,?long?timeout,?TimeUnit?unit)?{ return?redisTemplate.expire(key,?timeout,?unit); } /** ?*?刪除key ?*?@param?key?鍵 ?*/ public?void?delete?(String?key)?{ redisTemplate.delete(key); } }</object>
完成了Redis訪問工具類的實現(xiàn),現(xiàn)在需要考慮的是如何去模擬競爭分布式鎖。因為Redis本身就是支持分布式集群的,所以只需要模擬出多線程處理業(yè)務(wù)場景。這里采用線程池來模擬,以下是測試類的具體實現(xiàn):
@RestController @RequestMapping("test") public?class?TestController?{ private?static?final?Logger?LOG?=?LoggerFactory.getLogger(TestController.class);??//日志對象 @Autowired private?RedisDao?redisDao; //定義的分布式鎖key private?static?final?String?LOCK_KEY?=?"MyTestLock"; @RequestMapping(value={"testRedisLock"},?method=RequestMethod.GET) public?void?testRedisLock?()?{ ExecutorService?executorService?=?Executors.newFixedThreadPool(5); for?(int?i?=?0;?i?<p>通過上面這段代碼,可能會產(chǎn)生以下幾個疑問:</p><p>線程如果獲取分布式鎖失敗,為什么不嘗試重新獲取鎖?</p><p>線程獲取分布式鎖成功后,設(shè)置了鎖的失效時間,這個失效時間長短如何確定?</p><p>線程業(yè)務(wù)處理結(jié)束后,為什么要做刪除鎖的操作?</p><p>針對這幾個疑問,我們可以來討論下。</p><p>第一,Redis的SETNX命令,如果key已經(jīng)存在,則不會做任何操作,所以SETNX實現(xiàn)的分布式鎖并不是可重入鎖。當然,也可以自己通過代碼實現(xiàn)重試n次或者直至獲取到分布式鎖為止。但是,這不能保證競爭的公平性,某個線程會因為一直等待鎖而阻塞。因此,Redis實現(xiàn)的分布式鎖更適用于對共享資源一寫多讀的場景。</p><p>第二,分布式鎖必須設(shè)置失效時間,而且失效時間必須大于業(yè)務(wù)處理所需的時間(保證數(shù)據(jù)一致性)。所以,在測試階段盡可能準確的預測出業(yè)務(wù)正常處理所需的時間,設(shè)置失效時間是防止因為業(yè)務(wù)處理過程的某些原因?qū)е滤梨i的情況。</p><p>第三,業(yè)務(wù)處理結(jié)束,必須要做刪除鎖的操作。</p><p>上面設(shè)置分布式鎖和為鎖設(shè)置失效時間是通過兩個操作步驟完成的,更合理的方式應該是把設(shè)置分布式鎖和為鎖設(shè)置失效時間通過一個操作完成。要么都成功,要么都失敗。實現(xiàn)代碼如下:</p><pre class="brush:sql;toolbar:false">/** *?Redis訪問工具類 */ @Component public?class?RedisDao?{ private?static?Logger?logger?=?LoggerFactory.getLogger(RedisDao.class); @Autowired private?StringRedisTemplate?stringRedisTemplate; /** ?*?設(shè)置分布式鎖???? ?*?@param?key?????鍵 ?*?@param?value???值 ?*?@param?timeout?失效時間 ?*?@return ?*/ public?boolean?setDistributeLock?(String?key,?String?value,?long?timeout)?{ RedisConnection?connection?=?null; boolean?flag?=?false; try?{ //獲取一個連接 connection?=?stringRedisTemplate.getConnectionFactory().getConnection(); //設(shè)置分布式鎖的同時為鎖設(shè)置失效時間 connection.set(key.getBytes(),?value.getBytes(),?Expiration.seconds(timeout),?RedisStringCommands.SetOption.SET_IF_ABSENT); flag?=?true; }?catch?(Exception?e)?{ logger.error("set?automic?lock?error:",?e); }?finally?{ //使用后關(guān)閉連接 connection.close(); } return?flag; } /** ?*?查詢key的失效時間 ?*?@param?key???????鍵 ?*?@param?timeUnit??時間單位 ?*?@return ?*/ public?long?ttl?(String?key,?TimeUnit?timeUnit)?{ return?stringRedisTemplate.getExpire(key,?timeUnit); } } /** *?單元測試類 */ @RunWith(SpringRunner.class) @SpringBootTest public?class?Demo1ApplicationTests?{ private?static?final?Logger?LOG?=?LoggerFactory.getLogger(Demo1ApplicationTests.class); @Autowired private?RedisDao?redisDao; @Test public?void?testDistributeLock?()?{ String?key?=?"MyDistributeLock"; //設(shè)置分布式鎖,失效時間20s boolean?result?=?redisDao.setDistributeLock(key,?"1",?20); if?(result)?{ LOG.info("設(shè)置分布式鎖成功"); long?ttl?=?redisDao.ttl(key,?TimeUnit.SECONDS); LOG.info("{}距離失效還有{}s",?key,?ttl); } } }
運行單元測試類,結(jié)果如下:
2019-05-15?13:07:10.827?-?設(shè)置分布式鎖成功 2019-05-15?13:07:10.838?-?MyDistributeLock距離失效還有19s
更多Redis相關(guān)知識,請訪問Redis使用教程欄目!