聊聊使用RedisTemplat實現簡單的分佈式鎖的問題

不使用redisson框架實現Redis分佈式鎖

準備工作:

導入依賴

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

編寫RedisConfig類

@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<String , Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        //String類型 key序列器
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        //String類型 value序列器
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        //Hash類型 key序列器
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        //Hash類型 value序列器
        redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        //將連接工廠註入
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        return redisTemplate;
    }
 }

1.在SpringBootTest中編寫測試模塊

1.1:使用占位符加鎖:

占位符加鎖問題:出現異常時無法釋放鎖,導致後繼進入的線程成為死鎖

@SpringBootTest
class ApplicationTests {
    @Autowired
    private RedisTemplate redisTemplate;
	@Test
	public void lodsTest01(){
		ValueOperations valueOperations = redisTemplate.opsForValue();
	        //創建一個占位符,如果key不存在才可以設置成功
	        Boolean isLock = valueOperations.setIfAbsent("k1", "v1");
	        //如果占位成功,進行正常操作
	        if (isLock){
	        	//設置一個name存到redis
	            valueOperations.set("name","xxxx");
	            //從redis取出name
	            String name = (String) valueOperations.get("name");
	            System.out.println("name = " + name);
	            //手動制造異常
	            Integer.parseInt("xxxx");
	            //操作結束刪除鎖
	            redisTemplate.delete("k1");
	        }else{
	            System.out.println("有線程在用,請稍後在試");
	        }
	}
}

測試
第一個線程出現異常無法釋放鎖:

之後所有線程都無法訪問:

解決方案為鎖加一個有效時間。

1.2:使用占位符設置有效時間解決死鎖問題:

占位符設置有效時間問題即使某線程出現異常,但占位符過瞭有效時間,鎖就會釋放。但是在大量線程同時訪問時,如果線程1被外界因素影響(網絡波動,服務器出問題等等),線程1的業務還沒完成,但鎖的有效時間到瞭的話,下一個線程就會進來,就會出現線程不安全的情況,出現線程互相刪鎖的情況。

	@Test
    public void testLock02()  {
        ValueOperations valueOperations = redisTemplate.opsForValue();
        //如果key不存在才可以設置成功,設置一個有效時間防止線程異常出現死鎖
        Boolean isLock = valueOperations.setIfAbsent("k1", "v1",5, TimeUnit.SECONDS);
        //如果占位成功,進行正常操作
        if (isLock){
        	//設置一個name存到redis
            valueOperations.set("name","xxxx");
            //從redis取出name
            String str = (String) valueOperations.get("name");
            System.out.println("name = " + str);
            //制造異常
            Integer.parseInt("xxxx");
            //操作結束刪除鎖
            redisTemplate.delete("k1");
        }else{
            System.out.println("有線程在用,請稍後在試");
        }
    }

解決方案: 使用lua腳本,給每個鎖的key對應的value設置一個隨機數

1.3:使用lua腳本解決線程不安全問題:

lua腳本可以寫在Redis服務器上:
優點: 在服務器上運行速度快

缺點: 修改代碼時比較麻煩

lua腳本可以通過java發送
優點: 修改代碼方便

缺點: 每次發送請求時都需要占用網絡資源

1.3.1:編寫lua腳本

if redis.call("get",KEYS[1])==ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

1.3.2:修改ReidsConfig類

	@Bean
    public DefaultRedisScript<Boolean> defaultRedisScript(){
        DefaultRedisScript<Boolean> redisScript = new DefaultRedisScript<>();
        //lock.lua腳本位置和application.yml同級目錄
        redisScript.setLocation(new ClassPathResource("lock.lua"));
        //設置類型為boolean
        redisScript.setResultType(Boolean.class);
        return redisScript;
    }

1.3.3:編寫測試模塊

@Test
    public void testLock03(){
        ValueOperations valueOperations = redisTemplate.opsForValue();
        String value = UUID.randomUUID().toString();
        //如果key不存在才可以設置成功,設置一個value為隨機數的值,防止出現線程太多 導致線程不安全
        Boolean isLock = valueOperations.setIfAbsent("k1", value, 5, TimeUnit.SECONDS);
        //如果占位成功,進行正常操作 
        if (isLock){
        	//設置一個name存到redis
            valueOperations.set("name","xxxx");
            //從redis取出name
            String name = (String) valueOperations.get("name");
            System.out.println("name = " + name);
            //為redis發送lua腳本刪除鎖對應的value
            Boolean aBoolean = (Boolean) redisTemplate.execute(redisScript, Collections.singletonList("k1"), value);
            System.out.println(aBoolean);
        }else{
            System.out.println("有線程在用,請稍後在試");
        }
    }

測試結果:
順利把name值存到redis中並把鎖刪除並返回true

鎖會被正常刪除隻留下name:

到此這篇關於使用RedisTemplat實現簡單的分佈式鎖的文章就介紹到這瞭,更多相關RedisTemplat分佈式鎖內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: