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

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();
//        }
    }
}

分布式系统中,实现一个高效的分布式锁机制是保障资源互斥访问的关键。Redis 作为一种高性能的内存数据库,非常适合用于实现分布式锁。以下是基于 Redis 实现高效分布式锁的详细方案。 ### 加操作 加的核心在于确保多个节点之间对共享资源的互斥访问。可以通过 Redis 的 `SET` 命令结合特定参数来实现原子性的加操作。例如: ```bash SET lock_key unique_value NX PX 30000 ``` - `lock_key` 是的唯一标识。 - `unique_value` 是客户端生成的唯一值,用于标识当前持有的客户端。 - `NX` 表示只有当键不存在时才设置成功。 - `PX 30000` 设置键的过期时间为 30,000 毫秒(即 30 秒),防止死[^4]。 这种命令方式可以保证设置和设置超时时间的操作是原子性的,从而避免了因网络中断或其他问题导致的死。 ### 解操作 解需要确保只有持有的客户端才能释放它。这可以通过 Lua 脚本来实现,以保证解操作的原子性。以下是一个简单的 Lua 脚本示例: ```lua if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end ``` - `KEYS[1]` 是的键名。 - `ARGV[1]` 是客户端持有的唯一值。 通过这种方式,只有当客户端提供的唯一值与 Redis 中存储的值匹配时,才会执行删除操作,从而安全地释放。 ### 阻塞与非阻塞机制 根据业务需求,可以选择阻塞式或非阻塞式的获取策略。对于阻塞式,如果尝试获取失败,则会持等待直到获得为止;而非阻塞式则立即返回结果而不进行等待。在实际应用中,通常采用轮询的方式,在一定时间内不断尝试获取,一旦超过设定的时间仍未成功,则放弃此次请求。 ### 异常处理 为了应对可能出现的各种异常情况,比如持有的服务实例突然宕机等,应为每个设置合理的超时时间。这样即使某个客户端未能正常释放Redis 也会自动将其删除,从而防止整个系统陷入停滞状态[^4]。 ### 使用 Jedis 客户端实现 Java 开发者可以使用 Jedis 客户端库来与 Redis 进行交互。Jedis 提供了丰富的 API 支持,使得开发人员能够轻松地实现上述功能。例如,使用 Jedis 实现加逻辑如下所示: ```java public String acquireLock(Jedis jedis, String lockKey, String requestId, int expireTime) { String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime); if ("OK".equals(result)) { return requestId; } return null; } ``` - `jedis` 是 Jedis 实例。 - `lockKey` 是的名称。 - `requestId` 是客户端标识。 - `expireTime` 是的有效期。 以上代码展示了如何利用 Jedis 设置带有过期时间和条件判断的。 ###
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值