通过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();
//        }
    }
}
相关推荐
gavin_gxh14 分钟前
ORACLE 删除archivelog日志
数据库·oracle
一叶飘零_sweeeet17 分钟前
MongoDB 基础与应用
数据库·mongodb
猿小喵33 分钟前
DBA之路,始于足下
数据库·dba
明达技术39 分钟前
物联优化汽车齿轮锻造
分布式·物联网
tyler_download42 分钟前
golang 实现比特币内核:实现基于椭圆曲线的数字签名和验证
开发语言·数据库·golang
编程、小哥哥1 小时前
设计模式之抽象工厂模式(替换Redis双集群升级,代理类抽象场景)
redis·设计模式·抽象工厂模式
weixin_449310841 小时前
高效集成:聚水潭采购数据同步到MySQL
android·数据库·mysql
Cachel wood2 小时前
Github配置ssh key原理及操作步骤
运维·开发语言·数据库·windows·postgresql·ssh·github
standxy2 小时前
如何将钉钉新收款单数据高效集成到MySQL
数据库·mysql·钉钉
Narutolxy3 小时前
MySQL 权限困境:从权限丢失到权限重生的完整解决方案20241108
数据库·mysql