通过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();
//        }
    }
}
相关推荐
jiayou641 天前
KingbaseES 实战:深度解析数据库对象访问权限管理
数据库
李广坤2 天前
MySQL 大表字段变更实践(改名 + 改类型 + 改长度)
数据库
初次攀爬者3 天前
ZooKeeper 实现分布式锁的两种方式
分布式·后端·zookeeper
爱可生开源社区3 天前
2026 年,优秀的 DBA 需要具备哪些素质?
数据库·人工智能·dba
随逸1773 天前
《从零搭建NestJS项目》
数据库·typescript
加号34 天前
windows系统下mysql多源数据库同步部署
数据库·windows·mysql
シ風箏4 天前
MySQL【部署 04】Docker部署 MySQL8.0.32 版本(网盘镜像及启动命令分享)
数据库·mysql·docker
李慕婉学姐4 天前
Springboot智慧社区系统设计与开发6n99s526(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
数据库·spring boot·后端
百锦再4 天前
Django实现接口token检测的实现方案
数据库·python·django·sqlite·flask·fastapi·pip
tryCbest4 天前
数据库SQL学习
数据库·sql