锁代码:
package com.wjj.application.util;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.DefaultScriptExecutor;
import org.springframework.data.redis.core.script.ScriptExecutor;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.Collections;
import java.util.UUID;
/**
* redis锁(单实例)
* redLock
* 安全和可靠性保证
* 在描述我们的设计之前,我们想先提出三个属性,这三个属性在我们看来,是实现高效分布式锁的基础。
* 一致性:互斥,不管任何时候,只有一个客户端能持有同一个锁。
* 分区可容忍性:不会死锁,最终一定会得到锁,就算一个持有锁的客户端宕掉或者发生网络分区。
* 可用性:只要大多数Redis节点正常工作,客户端应该都能获取和释放锁。
* @author hank
* @since 2021/3/23 0023 下午 15:15
*/
@Component
public class HankRedLock {
/**
* 锁key前缀
*/
private final String LOCK_REDIS_KEY_PREFIX = "HANK_RED_LOCK:";
/**
* 加锁脚本
*/
private final DefaultRedisScript<String> lockScript;
/**
* 解锁脚本
*/
private final DefaultRedisScript<Long> unLockScript;
/**
* lua返回的真值判断
*/
private final static String LOCK_TRUE = "OK";
private final static Long UN_LOCK_TRUE = 1L;
/**
* 执行器
*/
private final ScriptExecutor<String> scriptExecutor;
@Autowired
public HankRedLock(StringRedisTemplate stringRedisTemplate) {
/*
初始化属性
*/
String lockLuaScript = "return redis.call('SET', KEYS[1], KEYS[2], 'NX', 'PX', KEYS[3])";
lockScript = new DefaultRedisScript<>(lockLuaScript, String.class);
String unLockLuaScript = "if redis.call('EXISTS',KEYS[1]) == 0 then return 1 else if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end end";
unLockScript = new DefaultRedisScript<>(unLockLuaScript, Long.class);
scriptExecutor = new DefaultScriptExecutor<>(stringRedisTemplate);
}
/**
* 获得锁
* @param key 锁key,在redis里会加入 LOCK_REDIS_KEY_PREFIX 前缀
* @param timeoutMillisecond 超时时间单位(毫秒)
* @return 锁获得成功返回随机value(用于解锁), 获取失败返回null
*/
public String lock(String key, Long timeoutMillisecond){
String myRandomValue = UUID.randomUUID().toString();
String realKey = getRealKey(key);
if(LOCK_TRUE.equals(scriptExecutor.execute(lockScript, Arrays.asList(realKey, myRandomValue, timeoutMillisecond.toString())))){
return myRandomValue;
}
return null;
}
/**
* 解锁
* !这里如果锁自动过期不存在了也返回 解锁成功true!
* @param key 锁key,在redis里会加入 LOCK_REDIS_KEY_PREFIX 前缀
* @param myRandomValue 解锁随机值(lock的返回值)
* @return bool
*/
public boolean unLock(String key, String myRandomValue){
String realKey = getRealKey(key);
return UN_LOCK_TRUE.equals(scriptExecutor.execute(unLockScript, Collections.singletonList(realKey), myRandomValue));
}
/**
* @param key 锁key,在redis里会加入 LOCK_REDIS_KEY_PREFIX 前缀
* @return
*/
private String getRealKey(String key){
return LOCK_REDIS_KEY_PREFIX+key;
}
}
这里需要明白为什么使用lua脚本,因为redis的lua脚本是原子性(事务)的,加锁解锁整个过程必须保证原子性(事务),具体的原理可以参考:《Redis官方文档》用Redis构建分布式锁
简单的JUnit4测试:
package com.wjj.application.util;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.transaction.annotation.Transactional;
import static org.junit.Assert.*;
import static org.junit.Assert.assertNotNull;
/**
* HankRedLock Tester.
*
* @author hank
* @since <pre>03/23/2021</pre>
* @version 1.0
*/
@RunWith(SpringRunner.class)
@SpringBootTest
//@ContextConfiguration("/bootstrap.properties")
@ActiveProfiles(value="local")
@Transactional
@Rollback(false)
public class HankRedLockTest{
@Autowired
private HankRedLock hankRedLock;
@Before
public void before() throws Exception {
}
@After
public void after() throws Exception {
}
/**
*
* Method: lock(String key, Long timeoutMillisecond)
*
*/
@Test
public void testLock() throws Exception {
//Test goes here...
String lockKey1 = "test3";
// 锁定10秒
String lock1 = hankRedLock.lock(lockKey1, 10000L);
assertNotNull(lock1);
// 再次获取锁 返回null
assertNull(hankRedLock.lock(lockKey1, 100000L));
// 等待10秒
Thread.sleep(10000);
// 再次获取锁成功
lock1 = hankRedLock.lock(lockKey1, 100000L);
assertNotNull(lock1);
// 解锁成功
assertTrue(hankRedLock.unLock(lockKey1, lock1));
// 再次加锁 100秒成功
lock1 = hankRedLock.lock(lockKey1, 100000L);
assertNotNull(lock1);
// 解锁成功 返回true
assertTrue(hankRedLock.unLock(lockKey1, lock1));
// 再次解锁(解锁也是成功) 返回false
assertFalse(hankRedLock.unLock(lockKey1, lock1));
}
}