1、手写 Redis 分布式锁,包括上锁、解锁、自动续期。
此功能实现采用 Lua脚本实现,Lua脚本可以保证原子性。
setnx可以实现分布式锁,但是无法实现可重入锁,所以用hset来代替setnx实现可重入的分布式锁。
-- lock
if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then
redis.call('hincrby',KEYS[1],ARGV[1],1)
redis.call('expire',KEYS[1],ARGV[2])
return 1
else
return 0
end
-- unlock
if redis.call('hexists',KEYS[1],ARGV[1]) == 0 then
return nil
elseif redis.call('hincrby',KEYS[1],ARGV[1],-1) == 0 then
return redis.call('del',KEYS[1])
else
return 0
end
-- expire
if redis.call('hexists',KEYS[1],ARGV[1]) == 0 then
return redis.call('expire',KEYS[1],ARGV[2])
else
return 0
end
2、工具类如下:
/**
* @author xxx
* @descpription: 自定义redis分布式锁
* @date 2024/7/24
*/
public class MyRedissonLua implements Lock {
/**
* 加锁脚本
*/
private static final String lockScript =
"if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then " +
"redis.call('hincrby',KEYS[1],ARGV[1],1) " +
"redis.call('expire',KEYS[1],ARGV[2]) " +
"return 1 " +
"else " +
"return 0 " +
"end";
/**
* 解锁脚本
*/
private static final String unLockScript =
"if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 0 then " +
"return nil " +
"elseif redis.call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then " +
"return redis.call('del',KEYS[1]) " +
"else " +
"return 0 " +
"end";
/**
* 续期脚本
*/
private static final String expireScript =
"if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 0 then " +
" return redis.call('EXPIRE',KEYS[1],ARGV[2]) " +
"else " +
" return 0 " +
"end";
private StringRedisTemplate stringRedisTemplate;
/**
* KEYS[1]
*/
private String lockName;
/**
* ARGV[1]
*/
private String uuidValue;
/**
* ARGV[2]
*/
private Long expireTime;
public MyRedissonLua(StringRedisTemplate stringRedisTemplate, String lockName,String uuid) {
this.stringRedisTemplate = stringRedisTemplate;
this.lockName = lockName;
this.uuidValue = uuid + ":" + Thread.currentThread().getId();
this.expireTime = 30L;
}
@Override
public void lock() {
tryLock();
}
@Override
public void unlock() {
Long flag = stringRedisTemplate.execute(new DefaultRedisScript<>(unLockScript, Long.class), Arrays.asList(lockName), uuidValue);
System.out.println("unlock lockName:" + lockName + "\tuuidValue:" + uuidValue + "\t expireTime:" + expireTime);
if(flag == null) {
throw new IllegalMonitorStateException("释放锁异常");
}else {
System.out.println("释放锁成功");
}
}
@Override
public boolean tryLock() {
boolean result;
try {
result = tryLock(-1L, TimeUnit.SECONDS);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return result;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
if (time == -1L){
System.out.println("lockName:" + lockName + "\tuuidValue:" + uuidValue + "\t expireTime:" + expireTime);
//可重入
while (!stringRedisTemplate.execute(new DefaultRedisScript<>(lockScript, Boolean.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime))){
TimeUnit.MILLISECONDS.sleep(60);
}
//后台扫描程序,检测key的ttl,来实现续期
reExpire();
return true;
}
return false;
}
private void reExpire() {
//每 10s 续期一次
new Timer().schedule(new TimerTask(){
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 续期");
if (stringRedisTemplate.execute(new DefaultRedisScript<>(lockScript, Boolean.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime))){
reExpire();
}
}
},(this.expireTime * 1000) / 3);
}
@Override
public void lockInterruptibly() {
}
@Override
public Condition newCondition() {
return null;
}
}
3、由于实现分布式锁的方式有很多,故采用工厂模式
/**
* @author xxx
* @descpription: 工厂模式生产分布式锁
* @date 2024/7/24
*/
@Component
public class DistributedLockFactory {
private String lockName;
private String uuid;
@Resource
private StringRedisTemplate stringRedisTemplate;
public DistributedLockFactory() {
this.uuid = IdUtil.simpleUUID();
}
public Lock getDistributedLock(String lockType){
if (lockType == null) {
return null;
}
if (lockType.equals("REDIS")){
lockName = "zzyyRedisLock";
return new MyRedissonLua(stringRedisTemplate, lockName,uuid);
} else if (lockType.equals("ZOOKEEPER")) {
lockName = "zzyyZookeeperLock";
//...Zookeeper版本的分布式锁
return null;
}
return null;
}
}
4、业务代码
import cn.hutool.core.util.IdUtil;
import com.coco.service.ICardService;
import com.coco.utils.lua.DistributedLockFactory;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Arrays;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
/**
* @author xxx
* @descpription:
* @date 2024/7/18
*/
@Service
public class ICardServiceImpl implements ICardService {
@Resource
private StringRedisTemplate stringRedisTemplate;
private static final String KEY = "sale001";
@Value("${server.port}")
private String port;
@Resource
private RedissonClient redissonClient;
/**
* 自定义的redis分布式锁
*/
// private Lock lock = new MyRedissonLua(stringRedisTemplate, "zzyyRedisLock");
/**
* 通过工厂获取自定义的redis分布式锁
*/
@Resource
private DistributedLockFactory distributedLockFactory;
@Override
public String sale() {
version7();
return "success";
}
private void version7() {
Lock lock = distributedLockFactory.getDistributedLock("REDIS");
lock.lock();
try{
String countStr = stringRedisTemplate.opsForValue().get(KEY);
Integer count = countStr == null ? 0 : Integer.parseInt(countStr);
if (count > 0) {
stringRedisTemplate.opsForValue().set(KEY,String.valueOf(--count));
System.out.println("剩余库存为:" + stringRedisTemplate.opsForValue().get(KEY) + "服务端口:" + port);
//演示自动续期
try {TimeUnit.SECONDS.sleep(120);} catch (InterruptedException e) {throw new RuntimeException(e);}
}else {
System.out.println("没有库存了");
}
}finally {
lock.unlock();
}
}
/**
* 可重入锁
*/
private void version6() {
Lock lock = distributedLockFactory.getDistributedLock("REDIS");
lock.lock();
try{
String countStr = stringRedisTemplate.opsForValue().get(KEY);
Integer count = countStr == null ? 0 : Integer.parseInt(countStr);
if (count > 0) {
stringRedisTemplate.opsForValue().set(KEY,String.valueOf(--count));
System.out.println("剩余库存为:" + stringRedisTemplate.opsForValue().get(KEY) + "服务端口:" + port);
//可重入
testReEntry();
}else {
System.out.println("没有库存了");
}
}finally {
lock.unlock();
}
}
/**
* 可重入锁
*/
private void testReEntry() {
Lock lock = distributedLockFactory.getDistributedLock("REDIS");
lock.lock();
try {
System.out.println("===========再次获取锁=============");
} finally {
lock.unlock();
}
}
/**
* 通过Lua脚本实现分布式锁解锁
*/
private void version5() {
String key = "zzyyRedisLock";
String uuidValue = IdUtil.simpleUUID() +":" + Thread.currentThread().getId();
//分布式锁(自旋)
while (!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue,10,TimeUnit.SECONDS)) {
try {TimeUnit.MILLISECONDS.sleep(20);} catch (InterruptedException e) {throw new RuntimeException(e);}
}
try {
String countStr = stringRedisTemplate.opsForValue().get(KEY);
Integer count = countStr == null ? 0 : Integer.parseInt(countStr);
if (count > 0) {
stringRedisTemplate.opsForValue().set(KEY,String.valueOf(--count));
System.out.println("剩余库存为:" + stringRedisTemplate.opsForValue().get(KEY) + "服务端口:" + port);
}else {
System.out.println("没有库存了");
}
} finally {
String script =
"if redis.call(\"get\",KEYS[1]) == ARGV[1] then\n" +
" return redis.call(\"del\",KEYS[1])\n" +
"else\n" +
" return 0\n" +
"end";
stringRedisTemplate.execute(
new DefaultRedisScript<>(script, Boolean.class),
Arrays.asList(key), uuidValue);
}
}
/**
* 添加判断防止解锁解的不是同一把锁
*/
private void version4() {
String key = "zzyyRedisLock";
String uuidValue = IdUtil.simpleUUID()+":" + Thread.currentThread().getId();
//分布式锁(自旋)
while (!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue,10,TimeUnit.SECONDS)) {
try {TimeUnit.MILLISECONDS.sleep(20);} catch (InterruptedException e) {throw new RuntimeException(e);}
}
try {
String countStr = stringRedisTemplate.opsForValue().get(KEY);
Integer count = countStr == null ? 0 : Integer.parseInt(countStr);
if (count > 0) {
stringRedisTemplate.opsForValue().set(KEY,String.valueOf(--count));
System.out.println("剩余库存为:" + stringRedisTemplate.opsForValue().get(KEY) + "服务端口:" + port);
}else {
System.out.println("没有库存了");
}
} finally {
if (Objects.equals(uuidValue, stringRedisTemplate.opsForValue().get(key))){
stringRedisTemplate.delete(key);
}
}
}
/**
* 通过setnx实现redis分布式锁
*/
private void version3() {
String key = "zzyyRedisLock";
String uuidValue = IdUtil.simpleUUID()+":" + Thread.currentThread().getId();
//分布式锁
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue);
if (flag) {
try {
String countStr = stringRedisTemplate.opsForValue().get(KEY);
Integer count = countStr == null ? 0 : Integer.parseInt(countStr);
if (count > 0) {
stringRedisTemplate.opsForValue().set(KEY,String.valueOf(--count));
System.out.println("剩余库存为:" + stringRedisTemplate.opsForValue().get(KEY) + "服务端口:" + port);
}else {
System.out.println("没有库存了");
}
} finally {
if (Objects.equals(uuidValue, stringRedisTemplate.opsForValue().get(key))){
stringRedisTemplate.delete(key);
}
}
}else {
try {TimeUnit.MILLISECONDS.sleep(20);} catch (InterruptedException e) {throw new RuntimeException(e);}
sale();
}
}
/**
* juc的lock锁
*/
private void version2() {
// lock.lock();
// try {
// Integer count = (Integer) redisTemplate.opsForValue().get(KEY);
// if (count > 0) {
// redisTemplate.opsForValue().decrement(KEY);
// System.out.println("剩余库存为:" + redisTemplate.opsForValue().get(KEY) + "服务端口:" + port);
// }else {
// System.out.println("没有库存了");
// }
// } finally {
// lock.unlock();
// }
}
}