JAVA开发(基于redis的分布式锁)

在分布式系统中,对一块数据的使用,特别是修改操作。微服务环境下,微服务本身是控制不了服务之间的事务的,有时候是需要控制数据的一致性,就需要使用分布式锁。基于redis的分布式锁是一种很好的解决方案,redis是每个服务共享的资源。当某个服务去申请访问资源时,都去获取redis的允许,这其实就达到了一种锁的机制。

Redis的分布式锁主要用于解决在分布式系统中保证互斥性的问题,以防止资源竞争导致的数据不一致或系统错误。以下是一些使用Redis分布式锁的具体场景:

  1. 对共享资源的互斥访问:例如,对公共数据库的事务操作或者对公共缓存的写操作。在处理这类问题时,分布式锁允许一个时间只允许一个节点访问某个资源,防止多个节点同时进行操作引发数据不一致问题。
  2. 分布式爬虫:例如,对同一个网站的抓取操作。由于网站数据是共享的,如果多个节点同时进行抓取,可能会导致数据重复或者数据缺失。通过使用分布式锁,可以确保每次只有一个节点进行抓取操作,避免数据的不一致性。
  3. 分布式任务调度:例如,对同一个任务的并发执行。这种情况下,任务需要在多个节点上并发执行以提高效率,但是同时也要保证任务执行的顺序和一致性。通过使用分布式锁,可以控制任务的执行顺序,避免并发执行导致的问题。
  4. 分布式缓存更新:例如,对缓存数据的更新操作。当需要对缓存进行更新时,为了保证数据的一致性,需要先获取锁,然后进行更新操作,最后释放锁。这样可以避免多个节点同时更新缓存导致的数据不一致问题。
  5. 分布式限流:例如,对某一段时间内的访问流量进行限制。通过使用分布式锁,可以限制在一定时间内只允许一定数量的节点访问某个资源,避免因为过多的请求导致系统崩溃或者数据错误。

此外,还有其他的业务场景也可能会使用到Redis的分布式锁,例如在微服务架构中,为了保证服务之间的互斥性和一致性,也可以使用Redis的分布式锁。总的来说,只要是在分布式系统中需要保证互斥性的场景,都可以考虑使用Redis的分布式锁。

springBoot的引入依赖

        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>

        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </dependency>

工具类:


 

import java.util.Arrays;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.data.redis.core.types.Expiration;
import org.springframework.stereotype.Component;

import cn.hutool.core.lang.UUID;

/**
 * @author:茅河野人
 * @Description:redis分布式锁工具类
 * @date:2023年2月6日
 */
@Component
public class RedisLockUtils implements AutoCloseable {
    @Autowired
    private RedisTemplate redisTemplate;
    private String key;
    private String value;
    private int expireTime;

    public RedisLockUtils(RedisTemplate redisTemplate, String key, int expireTime) {
        this.redisTemplate = redisTemplate;
        this.key = key;
        this.expireTime = expireTime;
        this.value = UUID.randomUUID().toString();
    }

    /**
     * 获取分布式锁
     */
    public boolean getLock() {
        RedisCallback<Boolean> redisCallback = connection -> {
            // 设置NX
            RedisStringCommands.SetOption setOption = RedisStringCommands.SetOption.ifAbsent();
            // 设置过期时间
            Expiration expiration = Expiration.seconds(expireTime);
            // 序列化key
            byte[] redisKey = redisTemplate.getKeySerializer().serialize(key);
            // 序列化value
            byte[] redisValue = redisTemplate.getValueSerializer().serialize(value);
            // 执行setnx操作
            Boolean result = connection.set(redisKey, redisValue, expiration, setOption);
            return result;
        };
        // 获取分布式锁
        Boolean lock = (Boolean) redisTemplate.execute(redisCallback);
        return lock;
    }

    /**
     * 解锁
     */
    public boolean unLock() {
        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";
        RedisScript<Boolean> redisScript = RedisScript.of(script, Boolean.class);
        List<String> keys = Arrays.asList(key);
        Boolean result = (Boolean) redisTemplate.execute(redisScript, keys, value);
        return result;
    }

    /**
     * 自动关闭
     */
    @Override
    public void close() {
        unLock();
    }

}

调用:


@RequestMapping("redisLock")
public String redisLock(){
    try (RedisLockUtils redisLock = new RedisLockUtils(redisTemplate,"redisKey",30)){
        if (redisLock.getLock()) {
            //延时模拟业务代码执行
            Thread.sleep(15000);
        }
    }catch (Exception e) {
        e.printStackTrace();
    }
    return "方法执行完成";
}

或者使用以下这个工具类


import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import javax.annotation.Resource;

import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;

import com.google.gson.Gson;

import cn.ctg.common.enums.EnumType;
import cn.ctg.common.util.constants.Constant;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisCluster;

/**
 * Redis工具类
 *
 */
@Component
public class RedisUtils {
    private final Logger logger = LoggerFactory.getLogger(this.getClass());
    @Autowired
    private RedisTemplate redisTemplate;
    
    @Resource(name = "redisTemplate")
    private ValueOperations<String, String> valueOperations;
 
    @Autowired
    private RedisExtendService redisExtendService;

    /** 加分布式锁的LUA脚本 */
    private static final String LOCK_LUA =
        "if redis.call('setNx',KEYS[1],ARGV[1])==1  then return redis.call('expire',KEYS[1],ARGV[2])  else  return 0 end";

    /** 计数器的LUA脚本 */
    private static final String INCR_LUA =
        "local current = redis.call('incr',KEYS[1]);" +
            " local t = redis.call('ttl',KEYS[1]); " +
            "if t == -1 then  " +
            "redis.call('expire',KEYS[1],ARGV[1]) " +
            "end; " +
            "return current";

    /** 解锁的LUA脚本 */
    private static final String UNLOCK_LUA =
        "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
    private static final Long SUCCESS = 1L;

    /** 互斥锁过期时间(分钟) */
    private static final long MUTEX_WAIT_MILLISECONDS = 50;
    /** 编号规则生成线程等待次数 ((10 * 1000) / 50) + 1 */
    public static final long RULE_CODE_THREAD_WAIT_COUNT = 200;
    /** 互斥锁等待时间(毫秒) */
    private static final long MUTEX_EXPIRE_MINUTES = 3;
    
    /**
         * 不设置过期时长
    */
    public final static long NOT_EXPIRE = -1;
    
    /**
         * 默认过期时长,单位:秒 
    */
    public final static long DEFAULT_EXPIRE = 7200; // 2小时
    
    /**
                * 会员卡缓存失效时间  2小时
     */
    public final static long CARD_DEFAULT_EXPIRE = 7200;
    
    /**
               * 默认过期时长,1天
     */
    public final static long DEFAULT_A_DAY = 86400;
    
    /**
               * 默认过期时长,1分钟
     */
    public final static long DEFAULT_A_MIN = 60 ;
    
    /**
            * 默认过期时长,2分钟
    */
    public final static long DEFAULT_TWO_MIN = 120 ;

    /**
     * 保存数据
     *
     * @param key
     * @param value
     * @param expire 过期时间,单位s
     */
    public void set(String key, Object value, long expire) {
        String valueJson = toJson(value);
        valueOperations.set(key, valueJson);
        if (expire != NOT_EXPIRE) {
            redisTemplate.expire(key, expire, TimeUnit.SECONDS);
        }
        redisExtendService.redisDataChange(key, valueJson, EnumType.CRUD_TYPE.CREATE.getValue());
    }

    /**
     * 判断key是否存在
     *
     * @param key
     */
    public Boolean hasKey(String key) {
        if (StringUtils.isNotBlank(key)) {
            return valueOperations.getOperations().hasKey(key);
        }
        return Boolean.FALSE;
    }

    /**
     * @param key
     * @param value
     */
    public void set(String key, Object value) {
        set(key, value, NOT_EXPIRE);
    }

    public <T> T get(String key, Class<T> clazz, long expire) {
        String value = Convert.toStr(valueOperations.get(key));
        if (expire != NOT_EXPIRE) {
            redisTemplate.expire(key, expire, TimeUnit.SECONDS);
        }
        return value == null ? null : fromJson(value, clazz);
    }

    /**
     * 批量从Redis中获取数据
     *
     * @param valueMap 需要存储的数据集合
     * @param expire 过期时间,秒
     * @return java.util.List<T> 返回值
     */
    public void batchSet(Map<String, String> valueMap, long expire) {
        valueOperations.multiSet(valueMap);
        if (expire != NOT_EXPIRE) {
            for (String key : valueMap.keySet()) {
                redisTemplate.expire(key, expire, TimeUnit.SECONDS);
            }
        }
    }

    /**
     * 批量删除
     *
     * @param keys 需要删除的KEY集合
     * @return void
     */
    public void batchDelete(Collection<String> keys) {
        redisTemplate.delete(keys);
    }

    /**
     * 批量从Redis中获取数据
     *
     * @param keyList 需要获取的Key集合
     * @param clazz 需要转换的类型
     * @return java.util.List<T> 返回值
     */
    public <T> Map<String, T> batchGet(List<String> keyList, Class<T> clazz) {
        List<String> objectList = valueOperations.multiGet(keyList);
        Map<String, T> map = new LinkedHashMap<>(objectList.size());
        for (int i = 0; i < keyList.size(); i++) {
            String value = Convert.toStr(objectList.get(i));
            if (!String.class.equals(clazz)) {
                map.put(keyList.get(i), fromJson(value, clazz));
            } else {
                map.put(keyList.get(i), (T)value);
            }
        }
        return map;
    }

    public <T> T get(String key, Class<T> clazz) {
        return get(key, clazz, NOT_EXPIRE);
    }

    /**
     * 使用 父编码+当前编码获取+集团+语言 获取名称
     *
     * @param code 父编码
     * @param dictCode 当前编码
     * @param language 语言 UserUtils.getLanguage()
     * @param groupId 集团ID
     */
    public String get(String code, String dictCode, String language, String groupId) {
        if (StringUtils.isBlank(dictCode)) {
            return "";
        }
        String key = RedisKeys.getSysDictKey(code, dictCode, language, groupId);
        return get(key, NOT_EXPIRE);
    }

    public String get(String key, long expire) {
        String value = Convert.toStr(valueOperations.get(key));
        if (expire != NOT_EXPIRE) {
            redisTemplate.expire(key, expire, TimeUnit.SECONDS);
        }
        return value;
    }

    public String get(String key) {
        return get(key, NOT_EXPIRE);
    }

    public void delete(String key) {
        redisTemplate.delete(key);
        redisExtendService.redisDataChange(key, "", EnumType.CRUD_TYPE.DELETE.getValue());
    }

    /**
     * Object转成JSON数据
     */
    private String toJson(Object object) {
        if (object instanceof Integer || object instanceof Long || object instanceof Float || object instanceof Double
            || object instanceof Boolean || object instanceof String) {
            return String.valueOf(object);
        }
        return new Gson().toJson(object);
    }

    /**
     * JSON数据,转成Object
     */
    private <T> T fromJson(String json, Class<T> clazz) {
        return new Gson().fromJson(json, clazz);
    }

    /**
     * 获取分布式锁,默认过期时间3分钟
     *
     * @param key 锁的KEY
     * @return java.lang.Boolean true为获取到锁,可以下一步业务, false为没有获取到锁
     */
    public Boolean setMutexLock(String key) {
        return setMutexLockAndExpire(key, getMutexLockExpireMinutes(), TimeUnit.MINUTES);
    }

    /**
     * 获取分布式锁,带Redis事务
     *
     * @param key 锁的KEY
     * @param timeout 锁时效时间,默认单位:秒
     * @param unit 锁失效时间单位,为null则默认秒
     * @return java.lang.Boolean true为获取到锁,可以下一步业务, false为没有获取到锁
     */
    public Boolean setMutexLockAndExpire(String key, long timeout, TimeUnit unit) {
        return setMutexLockAndExpire(key, Constant.RESULT_1, timeout, unit);
    }

    /**
     * 获取分布式锁,带Redis事务
     * 适用于同一业务,不同的请求用不同的锁,把value当成
     * @param key 锁的KEY
     * @param value 锁的值,一定要跟解锁的值一样,否则会导致无法解锁
     * @param timeout 锁时效时间,默认单位:秒
     * @param unit 锁失效时间单位,为null则默认秒
     * @return java.lang.Boolean true为获取到锁,可以下一步业务, false为没有获取到锁
     */
    public Boolean setMutexLockAndExpire(String key, String value, long timeout, TimeUnit unit) {
        value = StrUtil.appendIfMissing(StrUtil.prependIfMissing(value,"\""),"\"");
        Long result = executeLua(key, value, LOCK_LUA, timeout, unit, Long.class);
        return SUCCESS.equals(result);
    }

    /**
     * 解锁
     *
     * @param key 锁的Key
     * @return boolean
     */
    public boolean unlock(String key) {
        return unlock(key, Constant.RESULT_1);
    }

    /**
     * 解锁
     *
     * @param key 锁的Key
     * @param value 锁的value,一定要跟加锁的value一致,否则会认为不是同一个锁,不会释放
     * @return boolean
     */
    public boolean unlock(String key, String value) {
        value = StrUtil.appendIfMissing(StrUtil.prependIfMissing(value,"\""),"\"");

        Long result = executeLua(key, value, UNLOCK_LUA,null, null, Long.class);

        return SUCCESS.equals(result);
    }

    /**
     * 获取等待锁,如果没有获取到锁就一直等待获取,直到超过waitTime的时间
     *
     * @param key 锁的key
     * @param timeout 锁的超时时间
     * @param unit 锁的超时时间单位
     * @param waitTime 获取锁时的等待时间,一直等不超时则填-1,单位:毫秒
     * @return java.lang.Boolean true为获取到锁,可以下一步业务, false为没有获取到锁
     */
    public Boolean setMutexWaitLock(String key, long timeout, TimeUnit unit, long waitTime) {
        long start = System.currentTimeMillis();
        while (true) {
            boolean result = setMutexLockAndExpire(key, timeout, unit);
            if (result) {
                return true;
            } else {
                long current = System.currentTimeMillis();
                // 超过等待时间还没获取到锁则返回false
                if (waitTime > 0 && (current - start > waitTime)) {
                    logger.warn("redis分布式锁获取失败,key[{}],等待时间[{}]", key, waitTime);
                    return false;
                }
                // 等待100毫秒后重试
                ThreadUtil.sleep(100);
            }
        }
    }

    public long getMutexLockExpireMinutes() {
        return MUTEX_EXPIRE_MINUTES;
    }

    /**
     * 获取自增序列号
     *
     * @param key 序列号的KEY
     * @param seq 自增值,默认自增1
     * @return java.lang.Long 自增后的值
     */
    public Long incr(String key, Long seq, long timeout, TimeUnit unit) {
        return executeLua(key, null, INCR_LUA, timeout, unit, Long.class);
    }

    /**
     * 执行LUA脚本
     *
     * @param key redisKey
     * @param value 值
     * @param lua lua脚本
     * @param timeout 超时时间
     * @param unit 超时单位
     * @param clazz 返回值类型
     * @return T 返回值
     */
    public <T> T executeLua(String key, Object value, String lua, Long timeout, TimeUnit unit, Class<T> clazz){
        // 有时间单位则转成秒,否则默认秒
        if (unit != null) {
            timeout = unit.toSeconds(timeout);
        }
        List<String> args = new ArrayList<>(2);
        if(value != null){
            args.add(Convert.toStr(value));
        }
        if(timeout != null){
            args.add(Convert.toStr(timeout));
        }
        //spring自带的执行脚本方法中,集群模式直接抛出不支持执行脚本异常,此处拿到原redis的connection执行脚本
        T result = (T)redisTemplate.execute(new RedisCallback<T>() {
            @Override
            public T doInRedis(RedisConnection connection) throws DataAccessException {
                Object nativeConnection = connection.getNativeConnection();
                // 集群模式和单点模式虽然执行脚本的方法一样,但是没有共同的接口,所以只能分开执行
                // 集群
                if (nativeConnection instanceof JedisCluster) {
                    return (T) ((JedisCluster) nativeConnection).eval(lua, Collections.singletonList(key), args);
                }

                // 单点
                else if (nativeConnection instanceof RedisProperties.Jedis) {
                    return (T) ((Jedis) nativeConnection).eval(lua, Collections.singletonList(key), args);
                }
                return null;
            }
        });
        return result;
    }



    public void expire(String key, long timeout, TimeUnit unit) {
        try {
            redisTemplate.expire(key, timeout, unit);
        } catch (Exception e) {
            logger.error("设置缓存过期时间失败,key={},timeout={},unit={}", key, timeout, unit, e);
        }
    }

    /**
     * 获取互斥线程等待时间
     *
     * @return
     */
    public long getMutexThreadWaitMilliseconds() {
        return MUTEX_WAIT_MILLISECONDS;
    }

    public Set<String> getKeys(String key) {
        return redisTemplate.keys(key);
    }

    /**
     * 获取随机秒数 如:getRandomTime(30, 7)返回30天到第37天的随机秒数,即时效时间最小为30天,最大为37天
     *
     * @param afterDays N天之后
     * @param rangeDay 日期范围
     * @return java.lang.Long 秒数
     */
    public static Long getRandomTime(int afterDays, int rangeDay) {
        Calendar calendar = Calendar.getInstance();
        long curTime = calendar.getTimeInMillis();
        calendar.add(Calendar.DAY_OF_MONTH, afterDays);
        long minTime = calendar.getTimeInMillis();
        calendar.add(Calendar.DAY_OF_MONTH, rangeDay);
        long maxTime = calendar.getTimeInMillis();
        long randomTime = RandomUtil.randomLong(minTime, maxTime);
        return (randomTime - curTime) / 1000;
    }

    /**
     * 获取30天内的随机秒数
     *
     * @return long 返回1天后30天内的随机秒数
     */
    public static long getRandomTime() {
        return getRandomTime(1, 30);
    }


    public void setnx(String key,String value){


    }


}
  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

奋力向前123

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值