redis分布式锁的实现

因为项目中需要用到分布式锁,所以研究了下实现方式。碰到很多坑以及误导人的博客。这里写下自己的感受,大家看的时候还是要抱着怀疑的态度来看实现的合理性。

一般分布式锁的实现方式就两种(基于数据库的就不考虑了):

  1. 基于Redis的分布式锁;
  2. 基于ZooKeeper的分布式锁。

我之所以选择redis是因为redis快,单线程的原因。像使用数据库来做锁就会导致数据库压力太大且性能不高,在高并发的情况下不可取。不使用zookeeper是因为我使用的是eureka,不想再多引用一个中间件。

分布式锁必须满足的条件:

  • 互斥性:因为redis是单线程的,所以这点很容易做到
  • 不会发生死锁:网上很多资料使用setnx和expire做锁其实保证不了原子性,一旦在这两步中间业务代码报错无法执行,就出现了死锁。
  • 容错性:部署redis集群
  • 锁拥有者唯一标识:我这里没实现这点,因为业务代码中只有在锁被获取到了才能解这把锁,也就默认保证了这点

废话不多说,贴代码:

package com.mozi.common.handler;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

/**
 * @author lp
 * @create: 2018/10/12 14:22
 * @description: redis分布式锁, 使用时具体根据业务来设置超时时间和锁的持有时间!!!
 * 注意: 锁的key尽可能使用业务变量, 保证锁的细粒度, 避免串行化
 */
@Slf4j
@Component
public class DistributedLockHandler {

    private static final long LOCK_TRY_INTERVAL = 50L;// 默认多久尝试获取一次锁, 需考虑redis服务器压力

    private static final long LOCK_TRY_TIMEOUT = 200L;// 默认尝试多久, 需考虑并发压力

    private static final long DEFAULT_EXPIRE_TIME = 3000L;   // 默认key过期时间, 需考虑业务执行时长

    private static final String LOCK_SUCCESS = "OK";    // set方法执行成功后的返回值

    private static final String SET_IF_NOT_EXIST = "NX";    // SET IF NOT EXIST,key存在,进行set操作。若key已经存在,则不做任何操作

    private static final String SET_WITH_EXPIRE_TIME = "PX";    // 当设置为PX,表示设置一个过期时间

    private static final String DEFAULT_VALUE = "v"; // set方法的value字段, 这里默认设置v

    @Autowired
    private JedisPool jedisPool;

    public Jedis getJedis() {
        return jedisPool.getResource();
    }

    /**
     * 尝试获取全局锁
     *
     * @param key 锁名
     * @return true 获取成功,false获取失败
     */
    public boolean tryLock(String key) {
        return getLock(key, DEFAULT_VALUE, LOCK_TRY_TIMEOUT, LOCK_TRY_INTERVAL, DEFAULT_EXPIRE_TIME);
    }

    /**
     * 尝试获取全局锁
     *
     * @param key     锁名
     * @param timeout 获取超时时间 单位ms
     * @return true 获取成功,false获取失败
     */
    public boolean tryLock(String key, long timeout) {
        return getLock(key, DEFAULT_VALUE, timeout, LOCK_TRY_INTERVAL, DEFAULT_EXPIRE_TIME);
    }

    /**
     * 尝试获取全局锁
     *
     * @param key         锁名
     * @param timeout     获取锁的超时时间
     * @param tryInterval 多少毫秒尝试获取一次
     * @return true 获取成功,false获取失败
     */
    public boolean tryLock(String key, long timeout, long tryInterval) {
        return getLock(key, DEFAULT_VALUE, timeout, tryInterval, DEFAULT_EXPIRE_TIME);
    }

    /**
     * 尝试获取全局锁
     *
     * @param key            锁名
     * @param timeout        获取锁的超时时间
     * @param tryInterval    多少毫秒尝试获取一次
     * @param lockExpireTime 锁的过期
     * @return true 获取成功,false获取失败
     */
    public boolean tryLock(String key, long timeout, long tryInterval, long lockExpireTime) {
        return getLock(key, DEFAULT_VALUE, timeout, tryInterval, lockExpireTime);
    }

    /**
     * 尝试获取全局锁, 只尝试一次
     *
     * @param key 锁名
     * @return true 获取成功,false获取失败
     */
    public boolean onceTryLock(String key) {
        return getLock(key, DEFAULT_VALUE, DEFAULT_EXPIRE_TIME);
    }

    /**
     * 尝试获取全局锁, 只尝试一次
     *
     * @param key            锁名
     * @param lockExpireTime 锁的过期
     * @return true 获取成功,false获取失败
     */
    public boolean onceTryLock(String key, long lockExpireTime) {
        return getLock(key, DEFAULT_VALUE, lockExpireTime);
    }

    /**
     * 获取全局锁
     *
     * @param key         锁名
     * @param value       锁value, 如果要保证加锁和解锁是同一个客户端的话, 这个参数用来指定特定客户端
     * @param expireTime  锁的超时时间
     * @param timeout     获取锁的超时时间
     * @param tryInterval 多少ms尝试一次
     * @return
     */
    public boolean getLock(String key, String value, long timeout, long tryInterval, long expireTime) {
        try (Jedis jedis = getJedis()) {
            // 锁如果为空, 获取锁失败
            if (StringUtils.isEmpty(key) || StringUtils.isEmpty(value)) {
                return false;
            }
            long startTime = System.currentTimeMillis();  // 开始时间戳
            do {
                String result = jedis.set(key, value, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
                if (LOCK_SUCCESS.equals(result)) {  // 返回成功,表示加锁成功
                    return true;
                }
                if (System.currentTimeMillis() - startTime > timeout) { // 尝试超过了设定超时时间后直接跳出循环,获取锁失败
                    log.info("获取锁超时: {}", System.currentTimeMillis() - startTime);
                    return false;
                }
                Thread.sleep(tryInterval);  // 循环时设置时间差
            }
            while (true);   // 只要锁存在,循环
        } catch (InterruptedException e) {
            log.error(e.getMessage());
            return false;
        }
    }

    /**
     * 获取全局锁(无超时后循环重试机制,拿不到直接返回false)
     *
     * @param key        锁名
     * @param value      锁value, 如果要保证加锁和解锁是同一个客户端的话, 这个参数用来指定特定客户端
     * @param expireTime 超时时间
     * @return
     */
    public boolean getLock(String key, String value, long expireTime) {
        try (Jedis jedis = getJedis()) {
            if (StringUtils.isEmpty(key) || StringUtils.isEmpty(value)) {
                return false;
            }
            // 参数: key, value, key不存在set操作存在就不做任何操作, 可设置超时时间, 具体超时时间
            String result = jedis.set(key, value, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
            if (LOCK_SUCCESS.equals(result)) {  // 返回成功,表示加锁成功
                return true;
            }
        } catch (Exception e) {
            log.error(e.getMessage());
            return false;
        }
        return false;
    }

    /**
     * 释放锁
     *
     * @param key 锁名
     */
    public void releaseLock(String key) {
        try (Jedis jedis = getJedis()) {
            if (!StringUtils.isEmpty(key)) {
                Long del = jedis.del(key);
                log.info("锁名:{},是否释放成功:{}", key, del);
            }
        } catch (Exception e) {
            log.error(e.getMessage());
        }
    }

}

锁的实现主要还是靠jedis的set(x,x,x,x,x)方法,稍微解释下这个方法的参数和返回值:

  • 第一个参数key,key用来当锁,业务上注意key的粒度,粒度最好保证到对应数据库中一行数据
  • 第二个参数value,在我这篇博客中给的默认值v,这个字段可以用来做加锁和解锁人的唯一标识,比如传入uuid,那么解锁时也必须是这个uuid才能解锁成功。这点其实可以在业务代码中规避,只要在加锁成功或才能解锁就规避了这点
  • 第三个参数nxxx,这个参数我默认给nx,意思就是当key不存在时,我们进行set操作;若key已经存在,则不做任何操作
  • 第四个参数expx,我默认给的PX,意思表示可以给这个key设置一个过期时间,这样就规避了死锁的发生
  • 第五个参数time,与第四个参数对应,表示过期时间时长
  • 返回值:加锁成功返回OK,否则null

 

这篇文章很多地方参考了https://blog.csdn.net/forezp/article/details/68957681

但他这篇博客实现的锁有几个bug !!! 当初改的我都要吐血了, 不信的可以去试探一波

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值