分佈式鎖通常有三種實現方式:redis
本篇博客將介紹第二種方式,基於Redis實現分佈式鎖。spring
雖然網上已經有各類介紹Redis分佈式鎖實現的博客,然而他們的實現卻有着各類各樣的問題,爲了不誤人子弟,本篇博客將詳細介紹如何正確地實現Redis分佈式鎖。數據庫
首先,爲了確保分佈式鎖可用,咱們至少要確保鎖的實現同時知足如下四個條件:服務器
互斥性:在任意時刻,只有一個客戶端能持有鎖。
不會發生死鎖:即便有一個客戶端在持有鎖的期間崩潰而沒有主動解鎖,也能保證後續其餘客戶端能加鎖。
具備容錯性:只要大部分的Redis節點正常運行,客戶端就能夠加鎖和解鎖。
解鈴還須繫鈴人:加鎖和解鎖必須是同一個客戶端,客戶端本身不能把別人加的鎖給解了。app
<dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>2.9.0</version> </dependency> 備註:根據版本不一樣,jedis 的set 方法也有所不一樣
# redis.properties 配置文件: # region Redis jedis # redis配置開始 # Redis數據庫索引(默認爲0) spring.redis.database=0 # Redis服務器地址 spring.redis.host=localhost # Redis服務器鏈接端口 spring.redis.port=6379 # Redis服務器鏈接密碼(默認爲空) spring.redis.password=redis123456 # 鏈接池最大鏈接數(使用負值表示沒有限制) spring.redis.jedis.pool.max-active=1024 # 鏈接池最大阻塞等待時間(使用負值表示沒有限制) spring.redis.jedis.pool.max-wait=10000 # 鏈接池中的最大空閒鏈接 spring.redis.jedis.pool.max-idle=100 # 鏈接池中的最小空閒鏈接 spring.redis.jedis.pool.min-idle=5 # 鏈接超時時間(毫秒) spring.redis.timeout=10000 # 鏈接耗盡時是否阻塞, false報異常,ture阻塞直到超時 spring.redis.block-when-exhausted=true # endregion
@Configuration @PropertySource("classpath:application.properties") @Slf4j public class RedisConfig { @Value("${spring.redis.host}") private String host; @Value("${spring.redis.port}") private int port; @Value("${spring.redis.timeout}") private int timeout; @Value("${spring.redis.jedis.pool.max-idle}") private int maxIdle; @Value("${spring.redis.jedis.pool.max-wait}") private long maxWaitMillis; @Value("${spring.redis.password}") private String password; @Value("${spring.redis.block-when-exhausted}") private boolean blockWhenExhausted; @Bean public JedisPool redisPoolFactory() throws Exception { log.info("JedisPool注入開始!!"); log.info("redis地址:" + host + ":" + port); JedisPoolConfig jedisPoolConfig = new JedisPoolConfig(); jedisPoolConfig.setMaxIdle(maxIdle); jedisPoolConfig.setMaxWaitMillis(maxWaitMillis); // 鏈接耗盡時是否阻塞, false報異常,ture阻塞直到超時, 默認true jedisPoolConfig.setBlockWhenExhausted(blockWhenExhausted); // 是否啓用pool的jmx管理功能, 默認true jedisPoolConfig.setJmxEnabled(true); JedisPool jedisPool = new JedisPool(jedisPoolConfig, host, port, timeout, password); log.info("JedisPool注入成功!!"); return jedisPool; } }
@Service @Slf4j public class RedisService { //Redis 成功返回結果標識 private static final String LOCK_SUCCESS = "OK"; //Reis 操做返回成功 private static final Long RELEASE_SUCCESS = 1L; //Redis鎖不存在時才設置成功 private static final String SET_IF_NOT_EXIST = "NX"; //Redis鎖超時時間單位 private static final String SET_WITH_EXPIRE_TIME = "PX"; @Autowired private JedisPool jedisPool; /** * 嘗試獲取分佈式鎖 * * @param lockKey 鎖 * @param requestId 請求惟一標識(能夠經過uuid等方式獲取惟一ID) * @param expireTime 超期時間(毫秒) * @return 是否獲取成功 */ public boolean tryGetDistributedLock(String lockKey, String requestId, int expireTime) { Jedis jedis = jedisPool.getResource(); //Jedis 3.0.0 之前的版本 //String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime); //Jedis 3.0.0 及之後的版本 SetParams setParams = SetParams.setParams().nx().px(expireTime); String result = jedis.set(lockKey, requestId, setParams); return LOCK_SUCCESS.equals(result); } /** * 釋放分佈式鎖 * * @param lockKey 鎖 * @param requestId 請求惟一標識(加鎖時用的惟一標識) * @return 是否釋放成功 */ public boolean releaseDistributedLock(String lockKey, String requestId) { Jedis jedis = jedisPool.getResource(); String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId)); return RELEASE_SUCCESS.equals(result); } }
1. 利用redis 的set 命令的 5個參數保證操做的原子性 2. 利用Lua 腳本保證在釋放鎖時的原子性 3. 利用requestId 惟一標識保證不會釋放別人的鎖