手写一个基于redis的分布式锁

本文分享了基于Redis实现的分布式锁设计与代码实现。详细介绍了锁的核心注意点,包括原子操作与过期时间设置,并提供了简洁易用的代码示例,通过WatchDog线程自动续约确保锁的有效性。

基于redis的分布式锁

一、为什么要做

无疑,关于分布式锁,我们都已比较熟悉,网上有较多的开源解决方案,如redis的redisson,以及zookeeper的curator等,关于这两种分布式锁的使用及原理,后期会写文章介绍。本文主要针对小白,分享一下我学习分布式锁的一些心得,如果是大神请留下您的宝贵意见。

二、俯视代码

关于代码,我写的比较精简,力争在保证功能的情况下让使用上变得更加简单。

 

1.设计思路

redis锁的核心注意点主要有:

  • 设置锁和过期时间是否是原子操作
  • 过期时间设置是否合理?太长如果当前实例crash掉影响其他实例获取锁的效率(例如设置一分钟,则其他线程需要等待一分钟之后才能重新获取锁,在这期间无法处理业务),太短的话可能会导致业务还没处理完key就过期,导致锁失效的雪崩效应。

 

在设计上主要参考了JUC锁的使用模式,实现其Lock接口,在使用上当作ReentranLock使用即可。

 

2.代码分析

首先看一下锁的主体,首先在过期时间上采取一个比较折中的策略:默认30s,目前直接在代码写死,后期优化成可配置的形式,这样程序宕掉也不至于长时间的不可用;其次,关于业务线程可能阻塞导致的执行时间过长的问题,这边可以看到在lock的时候会启动一个WatchDog线程,此线程的作用是用于监视key的剩余过期时间,发现过小时完成自动续约,以此来保证锁不会被提前释放。

 

@Component
@Slf4j
public class DefaultLock implements Lock {
    public static final String LOCK = "lock";
    //默认锁过期时间30秒,后期优化做成可配置
    private static long DEFAULT_EXPIRE_TIME = 30L;
    @Autowired
    private RedisTemplate redisTemplate;

    private WatchDog watchDog;
    @Override
    public void lock() {
        //1.这里应对和本地jvm锁一样竞争的场景,如果竞争失败,则自旋
        while(!tryLock()){
            log.info("线程{}获取分布式锁失败",Thread.currentThread().getName());
        }
        //2.这里应对有些需要没有获取到锁直接返回失败的场景,后期会做一个策略优化
//        Boolean re = redisTemplate.opsForValue().setIfAbsent(LOCK, UUID.randomUUID(), DEFAULT_EXPIRE_TIME, TimeUnit.SECONDS);
//        if(Boolean.FALSE.equals(re)){
//            throw new LockCompititionFailException("获取分布式锁失败,当前线程ID:"+Thread.currentThread().getId());
//        }
        //走到这里说明获取分布式锁成功,开启线程监视key过期时间,防止业务流程还没结束就释放锁的情况
        startWatchDog();
    }

    private void startWatchDog(){
        watchDog = new WatchDog(true,redisTemplate);
        watchDog.start();
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {

    }

    @Override
    public boolean tryLock() {
        return redisTemplate.opsForValue().setIfAbsent(LOCK, UUID.randomUUID(), DEFAULT_EXPIRE_TIME, TimeUnit.SECONDS);
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return false;
    }

    @Override
    public void unlock() {
        //业务流程结束,释放锁
        redisTemplate.delete(LOCK);
        //将watchdog线程停止
        watchDog.setBusinessDone(false);
    }

    @Override
    public Condition newCondition() {
        return null;
    }
}

再来看一下WatchDog的实现,比较简单,主要完成续约的问题。

public class WatchDog extends Thread{
    //业务流程是否完成
    private boolean businessDone;

    private RedisTemplate redisTemplate;

    public WatchDog(boolean businessDone,RedisTemplate redisTemplate){
        this.businessDone = businessDone;
        this.redisTemplate = redisTemplate;
    }

    public boolean isBusinessDone() {
        return businessDone;
    }

    public void setBusinessDone(boolean businessDone) {
        this.businessDone = businessDone;
    }

    @Override
    public void run() {
        while(businessDone){
            //如果锁的剩余过期时间小于10s ,则将其重置
            if(redisTemplate.getExpire(DefaultLock.LOCK) < 10L){
                redisTemplate.expire(DefaultLock.LOCK,30L, TimeUnit.SECONDS);
            }
            //为避免空转太频繁,适当让线程sleep
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}


三、关于使用

使用上比较简单,用ioc注入DefaultLock实例即可。

 

public class ZmqTestController {
    @Autowired
    DefaultLock defaultLock;

    //模拟商品数量
    private Integer count = 50;

    @RequestMapping("/decrease")
    @ResponseBody
    public String testDistributeLock(){
        try {
            //加上锁
            defaultLock.lock();
            if(count < 1){
                log.info("库存不足");
                return "失败";
            }
            count--;
            log.info("当前线程:{},库存扣减成功,剩余库存:{}",Thread.currentThread().getId(),count);
        }catch (Exception e){
            log.info(e.getMessage());
        }finally {
            //释放锁
            defaultLock.unlock();
        }
        return "成功";
 

 

在分布式系统中,实现一个高效的分布式锁机制是保障资源互斥访问的关键。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 设置带有过期时间和条件判断的锁。 ###
评论 4
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值