通过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();
//        }
    }
}
相关推荐
drebander几秒前
MySQL 查询优化案例分享
数据库·mysql
初晴~16 分钟前
【Redis分布式锁】高并发场景下秒杀业务的实现思路(集群模式)
java·数据库·redis·分布式·后端·spring·
盖世英雄酱5813621 分钟前
InnoDB 的页分裂和页合并
数据库·后端
有一个好名字25 分钟前
zookeeper分布式锁模拟12306买票
分布式·zookeeper·云原生
YashanDB2 小时前
【YashanDB知识库】XMLAGG方法的兼容
数据库·yashandb·崖山数据库
独行soc2 小时前
#渗透测试#漏洞挖掘#红蓝攻防#护网#sql注入介绍11基于XML的SQL注入(XML-Based SQL Injection)
数据库·安全·web安全·漏洞挖掘·sql注入·hw·xml注入
风间琉璃""3 小时前
bugkctf 渗透测试1超详细版
数据库·web安全·网络安全·渗透测试·内网·安全工具
drebander3 小时前
SQL 实战-巧用 CASE WHEN 实现条件分组与统计
大数据·数据库·sql
IvorySQL3 小时前
IvorySQL 4.0 发布:全面支持 PostgreSQL 17
数据库·postgresql·开源数据库·国产数据库·ivorysql
18号房客3 小时前
高级sql技巧进阶教程
大数据·数据库·数据仓库·sql·mysql·时序数据库·数据库架构