通过Lua脚本手写redis分布式锁

1、手写 Redis 分布式锁,包括上锁、解锁、自动续期。

此功能实现采用 Lua脚本实现,Lua脚本可以保证原子性。

setnx可以实现分布式锁,但是无法实现可重入锁,所以用hset来代替setnx实现可重入的分布式锁。

Lua 复制代码
-- 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
Lua 复制代码
-- 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
Lua 复制代码
-- expire
if redis.call('hexists',KEYS[1],ARGV[1]) == 0 then
    return redis.call('expire',KEYS[1],ARGV[2])
else
    return 0
end

2、工具类如下:

java 复制代码
/**
 * @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、由于实现分布式锁的方式有很多,故采用工厂模式

java 复制代码
/**
 * @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、业务代码

java 复制代码
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();
//        }
    }
}
相关推荐
数据智能老司机2 小时前
CockroachDB权威指南——SQL调优
数据库·分布式·架构
数据智能老司机2 小时前
CockroachDB权威指南——应用设计与实现
数据库·分布式·架构
数据智能老司机2 小时前
CockroachDB权威指南——CockroachDB 模式设计
数据库·分布式·架构
数据智能老司机21 小时前
CockroachDB权威指南——CockroachDB SQL
数据库·分布式·架构
数据智能老司机21 小时前
CockroachDB权威指南——开始使用
数据库·分布式·架构
松果猿1 天前
空间数据库学习(二)—— PostgreSQL数据库的备份转储和导入恢复
数据库
Kagol1 天前
macOS 和 Windows 操作系统下如何安装和启动 MySQL / Redis 数据库
redis·后端·mysql
无名之逆1 天前
Rust 开发提效神器:lombok-macros 宏库
服务器·开发语言·前端·数据库·后端·python·rust
s9123601011 天前
rust 同时处理多个异步任务
java·数据库·rust
数据智能老司机1 天前
CockroachDB权威指南——CockroachDB 架构
数据库·分布式·架构