【spring boot】【redis】spring boot基於redis的LUA腳本 實現分佈式鎖

spring boot基於redis的LUA腳本 實現分佈式鎖【都是基於redis單點下】java

 

一.spring boot 1.5.X 基於redis 的 lua腳本實現分佈式鎖

1.pom.xml

<!-- Redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

 

2.RedisLock 工具類 (注入spring)

import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.data.redis.core.script.RedisScript; import org.springframework.data.redis.serializer.StringRedisSerializer; import org.springframework.stereotype.Component; import java.util.Collections; /** * spring boot 1.5.X * 使用redis 的 lua腳本 基於單點實現分佈式鎖 * * lua腳本做爲原子性操做,保證加鎖和設置超時時間 爲原子性操做 * @author sxd * @date 2019/5/27 10:52 */ @Component public class RedisLock { @Autowired RedisTemplate redisTemplate; private static final Long SUCCESS = 1L; /** * 獲取鎖 * * @param lockKey redis的key * @param value redis的value要求是隨機串,防止釋放其餘請求的鎖 * @param expireTime redis的key 的過時時間 防止死鎖,致使其餘請求沒法正常執行業務 * @return
     */
    public  boolean lock(String lockKey, String value, int expireTime) { String script = "if redis.call('setNx',KEYS[1],ARGV[1])  then " +
                "   if redis.call('get',KEYS[1])==ARGV[1] then " +
                "      return redis.call('expire',KEYS[1],ARGV[2]) " +
                "   else " +
                "      return 0 " +
                "   end " +
                "end"; RedisScript<String> redisScript = new DefaultRedisScript<>(script, String.class); //對非string類型的序列化
        redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setValueSerializer(new StringRedisSerializer()); Object result = redisTemplate.execute(redisScript, Collections.singletonList(lockKey), value, String.valueOf(expireTime)); return SUCCESS.equals(result); } /** * 釋放鎖 * * @param lockKey redis的key * @param value redis的value 只有value比對一致,才能肯定是本請求 加的鎖 才能正常釋放 * @return
     */
    public  boolean unlock(String lockKey, String value) { String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; RedisScript<String> redisScript = new DefaultRedisScript<>(script, String.class); try { Object result = redisTemplate.execute(redisScript, Collections.singletonList(lockKey), value); if (SUCCESS.equals(result)) { return true; } } catch (Exception e) { e.printStackTrace(); } return false; } }
View Code

 

3.controller使用

/** * 使用分佈式鎖 邏輯 * 1.準備好 key value expireTime * value要求是隨機字符串 * expireTime 是根據業務 衡量決定的 鎖過時時間 * * 2.獲取鎖 * 成功獲取,則執行業務,執行完成,釋放鎖 * 失敗獲取,則重試獲取,注意獲取鎖的時間間隔,直到獲取成功,執行業務,最後釋放鎖 * * 注意: * 對於redis加鎖的業務,儘可能用在耗時短的業務上。 * */ @RequestMapping("/test") public void test(){ boolean flag = false; //標識 是否正常獲取鎖
        String uuid = UUID.randomUUID().toString(); //redis的value 是一串隨機數
        flag = lock.lock("mykey1",uuid,5); if (flag){ business(uuid); }else { //若是未正常獲取鎖 能夠經過重試 直到獲取鎖成功
            while (!flag){ try { //重試 時間間隔 減小與redis交互次數
                    Thread.sleep(3000); System.out.println("重試"); flag = lock.lock("mykey1",uuid,5); if (flag){ business(uuid); }else { continue; } } catch (InterruptedException e) { e.printStackTrace(); } } } } public void business(String uuid){ try { System.out.println("加鎖成功,執行業務"); Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); }finally { //業務執行完成 正常釋放鎖
            lock.unlock("mykey1",uuid); } }
View Code

 

 

 

二.spring boot 2.x 基於redis 的LUA腳本 實現分佈式鎖

1.pom.xml

<!-- redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!--spring2.0集成redis所需common-pool2-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
            <version>2.4.2</version>
        </dependency>
        <!-- 使用redis的LUA腳本 須要序列化操做的jar-->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-annotations</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>

 

 

2.替代SpringBoot自動配置的RedisTemplate的RedisConfig類

 

package com.sxd.swapping.config; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; import java.io.Serializable; /** * @author sxd * @date 2019/5/27 16:13 */
/** * @Description Redis配置類,替代SpringBoot自動配置的RedisTemplate,參加RedisAutoConfiguration */ @Configuration @AutoConfigureAfter(RedisAutoConfiguration.class) public class RedisConfig { @Bean public RedisTemplate<String, Serializable> redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate<String, Serializable> template = new RedisTemplate<>(); template.setConnectionFactory(redisConnectionFactory); //Jackson序列化器
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class); ObjectMapper om = new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jackson2JsonRedisSerializer.setObjectMapper(om); //字符串序列化器
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); //普通Key設置爲字符串序列化器
 template.setKeySerializer(stringRedisSerializer); //Hash結構的key設置爲字符串序列化器
 template.setHashKeySerializer(stringRedisSerializer); //普通值和hash的值都設置爲jackson序列化器
 template.setValueSerializer(jackson2JsonRedisSerializer); template.setHashValueSerializer(jackson2JsonRedisSerializer); template.afterPropertiesSet(); return template; } }
View Code

 

3.RedisLock工具類,自動注入Spring

package com.sxd.swapping.utils; import org.apache.log4j.Logger; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.stereotype.Component; import java.util.Arrays; import java.util.List; /** * * spring boot 2.x版本 * @author sxd * @date 2019/5/27 16:11 */ @Component public class RedisLock2 { Logger logger = Logger.getRootLogger(); static final Long LOCK_SUCCESS = 1L; static final Long LOCK_EXPIRED = -1L; @Autowired RedisTemplate redisTemplate; //定義獲取鎖的lua腳本
    private final static DefaultRedisScript<Long> LOCK_LUA_SCRIPT = new DefaultRedisScript<>( "if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then return redis.call('pexpire', KEYS[1], ARGV[2]) else return 0 end" , Long.class ); //定義釋放鎖的lua腳本
    private final static DefaultRedisScript<Long> UNLOCK_LUA_SCRIPT = new DefaultRedisScript<>( "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return -1 end" , Long.class ); /** * 加鎖 * @param key redis鍵值對 的 key * @param value redis鍵值對 的 value 隨機串做爲值 * @param timeout redis鍵值對 的 過時時間 pexpire 以毫秒爲單位 * @param retryTimes 重試次數 即加鎖失敗以後的重試次數,根據業務設置大小 * @return
     */
    public boolean lock(String key,String value ,long timeout, int retryTimes) { try { logger.debug("加鎖信息:lock :::: redisKey = " + key + " requestid = " + value); //組裝lua腳本參數
            List<String> keys = Arrays.asList(key); //執行腳本
            Object result = redisTemplate.execute(LOCK_LUA_SCRIPT, keys,value,timeout); //存儲本地變量
            if(LOCK_SUCCESS.equals(result)) { logger.info("成功加鎖:success to acquire lock:" + Thread.currentThread().getName() + ", Status code reply:" + result); return true; } else if (retryTimes == 0) { //重試次數爲0直接返回失敗
                return false; } else { //重試獲取鎖
                logger.info("重試加鎖:retry to acquire lock:" + Thread.currentThread().getName() + ", Status code reply:" + result); int count = 0; while(true) { try { //休眠必定時間後再獲取鎖,這裏時間能夠經過外部設置
                        Thread.sleep(100); result = redisTemplate.execute(LOCK_LUA_SCRIPT, keys); if(LOCK_SUCCESS.equals(result)) { logger.info("成功加鎖:success to acquire lock:" + Thread.currentThread().getName() + ", Status code reply:" + result); return true; } else { count++; if (retryTimes == count) { logger.info("加鎖失敗:fail to acquire lock for " + Thread.currentThread().getName() + ", Status code reply:" + result); return false; } else { logger.warn(count + " times try to acquire lock for " + Thread.currentThread().getName() + ", Status code reply:" + result); continue; } } } catch (Exception e) { logger.error("加鎖異常:acquire redis occured an exception:" + Thread.currentThread().getName(), e); break; } } } } catch (Exception e1) { logger.error("加鎖異常:acquire redis occured an exception:" + Thread.currentThread().getName(), e1); } return false; } /** * 釋放KEY * @param key 釋放本請求對應的鎖的key * @param value 釋放本請求對應的鎖的value 是不重複隨即串 用於比較,以避免釋放別的線程的鎖 * @return
     */
    public boolean unlock(String key,String value) { try { //組裝lua腳本參數
            List<String> keys = Arrays.asList(key); logger.debug("解鎖信息:unlock :::: redisKey = " + key + " requestid = " + value); // 使用lua腳本刪除redis中匹配value的key,能夠避免因爲方法執行時間過長而redis鎖自動過時失效的時候誤刪其餘線程的鎖
 Object result = redisTemplate.execute(UNLOCK_LUA_SCRIPT, keys, value); //若是這裏拋異常,後續鎖沒法釋放
            if (LOCK_SUCCESS.equals(result)) { logger.info("解鎖成功:release lock success:" + Thread.currentThread().getName() + ", Status code reply=" + result); return true; } else if (LOCK_EXPIRED.equals(result)) { //返回-1說明獲取到的KEY值與requestId不一致或者KEY不存在,可能已通過期或被其餘線程加鎖 // 通常發生在key的過時時間短於業務處理時間,屬於正常可接受狀況
                logger.warn("解鎖異常:release lock exception:" + Thread.currentThread().getName() + ", key has expired or released. Status code reply=" + result); } else { //其餘狀況,通常是刪除KEY失敗,返回0
                logger.error("解鎖失敗:release lock failed:" + Thread.currentThread().getName() + ", del key failed. Status code reply=" + result); } } catch (Exception e) { logger.error("解鎖異常:release lock occured an exception", e); } return false; } }
View Code

 

 

4.使用

 @Autowired RedisLock2 lock2; @Autowired RedisTemplate redisTemplate; @RequestMapping("/test3") public void test3(){ ValueOperations vops = redisTemplate.opsForValue(); String uuid = UUID.randomUUID().toString(); //加鎖
        if (lock2.lock("mykey1",uuid,5000,3)){ try { // 執行業務
                System.out.println("加鎖成功,作業務"); vops.increment(REDIS_COUNT_KEY,1); Thread.sleep(3000); System.out.println("業務執行結束"); } catch (InterruptedException e) { e.printStackTrace(); }finally { //解鎖
                lock2.unlock("mykey1",uuid); } } }
View Code
相關文章
相關標籤/搜索