单机Redis实现分布式锁

本文介绍了一种使用Redis实现分布式锁的方法,通过setnx指令结合expire过期时间来保证锁的安全性和有效性。文章提供了Java实现的代码示例,包括获取锁与释放锁的正确方式,并对比了错误的实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

思路

先拿 setnx 来争抢锁,抢到之后,再用 expire 给锁加一个过期时间防止锁忘记了释放。

但是如果在 setnx 之后执行 expire之前进程意外 crash 或者要重启维护了,那会怎么样?这个锁就永远得不到释放了。

set 指令有非常复杂的参数,可以同时把 setnx 和expire 合成一条指令来用的。

package com.xp.lock;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.params.SetParams;

import java.util.Collections;

public class SingleRedisLock {
    private static final String LOCK_SUCCESS = "OK";
    //这个参数我们填的是NX,意思是SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已
    //经存在,则不做任何操作;
    private static final String SET_IF_NOT_EXIST = "NX";
    //这个参数我们传的是PX,意思是我们要给这个key加一个过期的设置,具体时
    //间由第五个参数决定。
    private static final String SET_WITH_EXPIRE_TIME = "PX";
    private static final Long RELEASE_SUCCESS = 1L;
    private static SingleRedisLock singleRedisLock;
    private static JedisPool jedisPool;
    public SingleRedisLock() {
        this.jedisPool = new JedisPool();
    }

    public static SingleRedisLock instance() {
        if (singleRedisLock == null) {
            synchronized (SingleRedisLock.class) {
                if (singleRedisLock == null) {
                    singleRedisLock = new SingleRedisLock();
                }
            }
        }
        return singleRedisLock;
    }

    /**
     * 尝试获取分布式锁
     *
     * @param lockKey    锁 我们使用key来当锁,因为key是唯一的。
     * @param requestId  请求标识 我们传的是requestId,很多童鞋可能不明白,有key作为锁不就够了吗,为什么还要用到value?原因就是我们在上面讲到可靠性时,分布式锁要满足第四个条件 , 解铃还须系铃人,通过给value赋值为requestId,我们就知道这把锁是哪个请求加的了,在解锁的时候就可以有依据。requestId可以使用UUID.randomUUID().toString()方法生成。
     * @param expireTime 具体超期时间 与第四个参数相呼应,代表key的过期时间。
     * @return 是否获取成功
     */
    public static boolean tryGetDistributedLock(String lockKey, String requestId, int expireTime) {
        Jedis jedis = jedisPool.getResource();
        SetParams setParams = new SetParams();
        setParams.nx();
        setParams.px(expireTime);
        String result = jedis.set(lockKey, requestId, setParams);

        if (LOCK_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }

    /**
     * 释放分布式锁
     *
     * @param lockKey   锁
     * @param requestId 请求标识
     * @return 是否释放成功
     */
    public static boolean releaseDistributedLock(String lockKey, String requestId) {
        Jedis jedis = jedisPool.getResource();
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
        if (RELEASE_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }
}

IndexCount服务类:

package com.xp.lock;·

package com.xp.lock;
import org.springframework.stereotype.Service;

@Service
public class IndexCount {
    private Long count=0l;

    public Long getCount() {
        return count;
    }

    public void setCount(Long count) {
        this.count = count;
    }
}

获取锁方法说明:

执行上面获取锁方法就只会导致两种结果:

  1. 当前没有锁(key不存在),那么就进行加锁操作,并对锁设置个有效期,同时value表示加锁的客户端。

  2. 已有锁存在,不做任何操作。

我们的加锁代码满足我们可靠性里描述的三个条件:

我这里使用的Jedis的版本是3.3.0,必须使用SetParams对象传入nx和expire相关设置。

  1. 首先,setParams.nx()可以保证如果已有key存在,则函数不会调用成功,也就是只有一个客户端能持有锁,满足互斥性。
  2. 其次,由于我们通过 setParams.px(expireTime)对锁设置了过期时间,即使锁的持有者后续发生崩溃而没有解锁,锁也会因为到了过期时间而自动解锁(即key被删除),不会发生死锁。
  3. 最后,因为我们将value赋值为requestId,代表加锁的客户端请求标识,那么在客户端在解锁的时候就可以进行校验是否是同一个客户端。

释放锁方法说明:

  1. 第一行代码,我们写了一个简单的Lua脚本代码,
  2. 第二行代码,我们将Lua代码传到redis.eval()方法里,并使参数KEYS[1]赋值为lockKey,ARGV[1]赋值为requestId。eval()方法是将Lua代码交给Redis服务端执行。

这段Lua代码的功能:

首先获取锁对应的value值,检查是否与requestId相等,如果相等则删除锁(解锁)。

为了要确保上述操作是原子性的。就使用在eval命令执行Lua代码的时候,Lua代码将被当成一个命令去执行,并且直到eval命令执行完成,Redis才会执行其他命令


错误实现:

1.错误加锁

	/**
     *
     * @param acquireTimeout 获取锁的超时时间
     * @param timeOut 锁的过期时间
     * @return
     */
    public String getRedisLock(Long acquireTimeout, Long timeOut) {
        Jedis conn = null;
        try {
            conn = jedisPool.getResource();
            String identifierValue = UUID.randomUUID().toString();
            int expireLock = (int) (timeOut / 1000);
            Long endTime = System.currentTimeMillis() + acquireTimeout;
            while (System.currentTimeMillis() < endTime) {
                if (conn.setnx(redislockKey, identifierValue) == 1) {
                    conn.expire(redislockKey, expireLock);
                    return identifierValue;
                }
			}
        } catch (Exception e) {
            e.printStackTrace();
            if (conn != null) {
                conn.close();
            }
        }
        return null;
    }

注意问题:

要为锁的value设置一个唯一的值,这样就避免了任意线程都能释放锁,因为如果业务时间小于锁的过期时间,锁被释放而业务没有执行完,另一线程获得锁,但会因第一个线程最后的释放锁而受到影响

conn.setnx和con.expire应该用lua脚本,保证其原子操作,上述代码就明显错误了,如果conn.setnx执行完后,redis服务器宕机了那么会导致锁永远无法释放

2.错误释放锁

	/**
     *
     * @param identifierValue 锁的value
     */

    public void unRedisLock(String identifierValue) {
        Jedis conn = null;
        conn = jedisPool.getResource();
        try {
            if (conn.get(redislockKey).equals(identifierValue)) {
                System.out.println("释放锁" + Thread.currentThread().getName() + ",identifierValue: " + identifierValue);
                conn.del(redislockKey);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (conn != null) {
                conn.close();
            }
        }
    }

此处同样要用lua脚本来保证conn.get和conn.del的原子性操作,如果执行到conn.get后刚好锁过期了,而另一线程获得锁,但conn.del会把锁删掉,虽然判断了锁的value后再删除仍会出现一个线程删除了另一线程获得的锁


THE END.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值