Redis-Redisson

中文文档地址

Redis

字符串(string)、散列(hash)、列表(list)、集合(set)、有序集合(zset)
image.png
SpringData是Spring中数据操作的模块,包括对各种数据库的集成,其中对Redis的集成模块就叫SpringDataRedis
image.png
RedisTemplate基础信息及序列化规则及配置
redis持久化配置-RDB和AOF

特点:

  1. 提供了对不同Redis客户端的整合(Lettuce和Jedis)
  2. 提供了RedisTemplate统一API来操作Redis
  3. 支持Redis的发布订阅模型
  4. 支持Redis哨兵和Redis集群
  5. 支持基于Lettuce的响应式编程
  6. 支持基于JDK、JSON、字符串、Spring独享的数据序列化及反序列化
  7. 支持基于Redis的JDKCollection实现

SpringDataRedis中提供了RedisTemplate工具类,其中封装了各种对Redis的操作。并且将不同的数据类型的操作API封装到了不同的类型中:
image.png

redis分布式锁

image.png

redisson点击进入详细介绍

redisson简洁版了解

redisson详细锁分类介绍(代码版)-使用说明

//exists',KEYS[1])==0 不存在,没锁
"if (redis.call('exists',KEYS[1])==0) then "+       --看有没有锁
  // 命令:hset,1:第一回
	"redis.call('hset',KEYS[1],ARGV[2],1) ; "+       --无锁 加锁 
	// 配置锁的生命周期 
	"redis.call('pexpire',KEYS[1],ARGV[1]) ; "+      
	"return nil; end ;" +

//可重入操作,判断是不是我加的锁
"if (redis.call('hexists',KEYS[1],ARGV[2]) ==1 ) then "+  --我加的锁
   //hincrby 在原来的锁上加1
	"redis.call('hincrby',KEYS[1],ARGV[2],1) ; "+  --重入锁
	"redis.call('pexpire',KEYS[1],ARGV[1]) ; "+  
	"return nil; end ;" +

//否则,锁存在,返回锁的有效期,决定下次执行脚本时间
"return redis.call('pttl',KEYS[1]) ;"  --不能加锁,返回锁的时间

上面的lua(俗称胶水语言)脚本比较重要,主要是为了执行命令的原子性解释一下:
KEYS[1]代表你的key
ARGV[1]代表你的key的存活时间,默认存活30秒
ARGV[2]代表的是请求加锁的客户端ID,后面的1则理解为加锁的次数,简单理解就是 如果该客户端多次对key加锁时,就会执行hincrby原子加1命令
第一段if就是判断你的key是否存在,如果不存在,就执行redis call(hset key ARGV[2],1)加锁和设置redis call(pexpire key ARGV[1])存活时间;
当第二个客户来加锁时,第一个if判断已存在key,就执行第二个if判断key的hash是否存在客户端2的ID,很明显不是;
则进入到最后的return返回该key的剩余存活时间
当加锁成功后会在后台启动一个watch dog(看门狗)线程,key的默认存活时间为30秒,则watch dog每隔10秒钟就会检查一下客户端1是否还持有该锁,如果持有,就会不断的延长锁key的存活时间
所以这里建议大家在设置key的存活时间时,最好大于10秒,延续时间也大于等于10秒

image.png

问题:

Redlock和Redisson都是Redis分布式锁的实现方式,但它们有以下区别:

  1. 实现方式不同:Redlock是基于多个Redis节点(对外暴露多个redis服务器)的互相协作来实现分布式锁,而Redisson是基于Redis的单节点实现(对外暴露一个服务器)。
  2. 可靠性不同:Redlock的可靠性比Redisson低,因为它需要多个Redis节点的协作,如果其中一个节点出现问题,就可能导致锁失效(少数服从多数,当有锁的挂机后,锁可能失效)。而Redisson只需要一个Redis节点,可靠性更高(内部处理redis集群)。
  3. 性能不同:Redlock的性能比Redisson低,因为它需要多个Redis节点的协作,而Redisson只需要一个Redis节点,性能更高。
  4. 功能不同:Redisson除了实现分布式锁外,还提供了其他的分布式功能,如分布式集合、分布式Map等,而Redlock只实现了分布式锁。

RedisTemplate是什么

RedisTemplate是Spring提供的对Redis操作的封装,通过提供一系列的操作方法,避免了手动管理连接池、线程安全等问题,同时还支持事务等高级特性。总的来说,Jedis更加轻量级、灵活,适合小型项目;而RedisTemplate则更加适合大型项目,提供了更加完善的封装和高级特性。

Redisson不支持幂等性

参考地址:点此

1、数据库唯一主键

(唯一索引/主键)
主要是利用数据库中主键唯一约束的特性,一般来说唯一主键比较适用于“插入”时的幂等性,其能保证一张表中只能存在一条带该唯一主键的记录。
使用数据库唯一主键完成幂等性时需要注意的是,该主键一般来说并不是使用数据库中自增主键,而是使用分布式 ID 充当主键,这样才能能保证在分布式环境下 ID 的全局唯一性。

2、数据库乐观锁

(更新操作下,通过上状态的变化来进行更新操作)
一般只能适用于执行更新操作的过程,我们可以提前在对应的数据表中多添加一个字段,充当当前数据的版本标识。
这样每次对该数据库该表的这条数据执行更新时,都会将该版本标识作为一个条件,值为上次待更新数据中的版本标识的值。

3、防重 Token 令牌

(看第五点-意思一致)
针对客户端连续点击或者调用方的超时重试等情况,例如提交订单,此种操作就可以用 Token 的机制实现防止重复提交。
简单的说就是调用方在调用接口的时候先向后端请求一个全局 ID(Token),请求的时候携带这个全局 ID 一起请求(Token 最好将其放到 Headers 中),后端需要对这个 Token 作为 Key,用户信息作为 Value 到 Redis 中进行键值内容校验,如果 Key 存在且 Value 匹配就执行删除命令,然后正常执行后面的业务逻辑。如果不存在对应的 Key 或 Value 不匹配就返回重复执行的错误信息,这样来保证幂等操作。

4、下游传递唯一序列号

((参考第五点-进阶)通过redis缓存特性,先缓存key到redis中,保证后续数据不存在重复的key)
所谓请求序列号,其实就是每次向服务端请求时候附带一个短时间内唯一不重复的序列号,该序列号可以是一个有序 ID,也可以是一个订单号,一般由下游生成,在调用上游服务端接口时附加该序列号和用于认证的 ID。
当上游服务器收到请求信息后拿取该 序列号 和下游 认证ID 进行组合,形成用于操作 Redis 的 Key,然后到 Redis 中查询是否存在对应的 Key 的键值对,根据其结果:

  1. 如果存在,就说明已经对该下游的该序列号的请求进行了业务处理,这时可以直接响应重复请求的错误信息。
  2. 如果不存在,就以该 Key 作为 Redis 的键,以下游关键信息作为存储的值(例如下游商传递的一些业务逻辑信息),将该键值对存储到 Redis 中 ,然后再正常执行对应的业务逻辑即可。

5、redis锁操作-手动写lua脚本

(判断是否加锁成功,成功则继续执行,失败则返回)
image.png

代码示例:

两种redis分布式锁方式

package com.demo.service.impl;

import com.demo.config.RedisUtil;
import com.demo.config.ThreadPoolTaskExecutorConfig;
import com.demo.mapper.DemoMapper;
import com.demo.service.TestService;
import jakarta.annotation.Resource;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.Callable;
import java.util.concurrent.Executor;

@Slf4j
@Service
@RequiredArgsConstructor
public class TestServiceImpl implements TestService {
    
    

    @Resource
    private DemoMapper mapper;

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private RedisUtil redisLock;

    @Resource
    private RedissonClient redissonClient;

    @Override
    public String insert() {
    
    
        ThreadPoolTaskExecutorConfig threadPoolTaskExecutorConfig = new ThreadPoolTaskExecutorConfig();

        Callable<Object> objectCallable = new Callable<>() {
    
    
            @Override
            public Object call() throws Exception {
    
    
                return null;
            }
        };

        // 创建线程池
        Executor executor = threadPoolTaskExecutorConfig.getAsyncExecutor();
        String rel=null;
            // 线程方法
            executor.execute(() -> {
    
    
                try {
    
    
                    String yyyyMMddhhmmss = new SimpleDateFormat("yyyyMMddhhmmssSSS").format(new Date());

                    String sout = sout(yyyyMMddhhmmss);
                } catch (Exception e) {
    
    
                    throw new RuntimeException(e);
                }
            });
            return rel;
    }

    private RLock rLock;


    /**
     * 加redis分布式锁
     *
     * @throws Exception
     */
    private String sout(String yyyyMMddhhmmss) {
    
    

        // 当前线程name
        String name = Thread.currentThread().getName();
        long id = Thread.currentThread().getId();


        // 手动lua脚本加锁操作
        try {
    
    
            if (redisLock.lock(yyyyMMddhhmmss, yyyyMMddhhmmss, 10)) {
    
    
                int insert = mapper.insert(yyyyMMddhhmmss);
                return "枷锁成功";
                // System.out.println(yyyyMMddhhmmss + "枷锁chenggong");
            } else {
    
    
//                System.out.println(yyyyMMddhhmmss + "枷锁失败");
                return "枷锁失败";
            }
        } catch (Exception e) {
    
    
            throw new RuntimeException();
        } finally {
    
    
            redisLock.unlock(yyyyMMddhhmmss, yyyyMMddhhmmss);
        }

        // redisson加锁
//        RLock lock = redissonClient.getLock(yyyyMMddhhmmss);
//        try {
    
    
//            boolean b = lock.tryLock(1,15,TimeUnit.SECONDS);
//
//            // 数据库新增操作
//            int insert = mapper.insert(yyyyMMddhhmmss);
//            if(b){
    
    
//                System.out.println(name+","+id+","+yyyyMMddhhmmss+"成功;;;;;;;;;;");
//            }else{
    
    
//                System.out.println(name+","+id+","+yyyyMMddhhmmss+"失败了————————————————————————");
//            }
//        } catch (Exception e) {
    
    
//            System.out.println(name+","+id+","+yyyyMMddhhmmss+"失败了————————————————————————");
//            throw new RuntimeException(e);
//        } finally {
    
    
//            // 判断锁是否存在
//            boolean locked = lock.isLocked();
//            // 判断锁是否被当前线程保持
//            boolean heldByCurrentThread = lock.isHeldByCurrentThread();
//            log.info("{}:获取锁状态:{} 是否当前线程保留:{}", name, locked, heldByCurrentThread);
//            if (locked && heldByCurrentThread) {
    
    
//                lock.unlock();
//                System.out.println(name+","+id+","+yyyyMMddhhmmss+"释放锁}}}}}}}}}}}}}}}}");
//            } else {
    
    
//                log.info("{}:未获得锁不用释放", name);
//            }
//
//        }


    }
}

Redis

package com.demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericToStringSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * redis默认配置
 */
@Configuration
public class RedisConfig {
    
    
    @Bean
    public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){
    
    
        RedisTemplate redisTemplate = new RedisTemplate();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        // 创建序列化列
        GenericToStringSerializer genericToStringSerializer = new GenericToStringSerializer(String.class);
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(genericToStringSerializer);
        return redisTemplate;
    }
}

配置redis分布式锁

package com.demo.config;

import com.alibaba.fastjson.JSON;
import jakarta.annotation.Resource;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

/**
 * redis配置项
 */
@Slf4j
@Component
public class RedisUtil {
    
    
    @Resource
    private RedisTemplate redisTemplate;
    private static Map<String, LockInfo> lockInfoMap = new ConcurrentHashMap<>();
    private static final Long SUCCESS = 1L;

    /**
     * 加锁内置自定义字段
     */
    @Data
    public static class LockInfo {
    
    
        private String key;
        private String value;
        private int expireTime;
        //更新时间
        private long renewalTime;
        //更新间隔
        private long renewalInterval;
        public static LockInfo getLockInfo(String key, String value, int expireTime) {
    
    
            LockInfo lockInfo = new LockInfo();
            lockInfo.setKey(key);
            lockInfo.setValue(value);
            lockInfo.setExpireTime(expireTime);
            lockInfo.setRenewalTime(System.currentTimeMillis());
            lockInfo.setRenewalInterval(expireTime * 2000 / 3);
            return lockInfo;
        }
    }

    /**
     * 使用lua脚本更新redis锁的过期时间
     * @param lockKey
     * @param value
     * @return 成功返回true, 失败返回false
     */
    public boolean renewal(String lockKey, String value, int expireTime) {
    
    
        String luaScript =
        """
        if
            redis.call('get', KEYS[1]) == ARGV[1]
        then
            return redis.call('expire', KEYS[1], ARGV[2])
        else
            return 0
        end";
        """;
        DefaultRedisScript<Boolean> redisScript = new DefaultRedisScript<>();
        redisScript.setResultType(Boolean.class);
        redisScript.setScriptText(luaScript);
        List<String> keys = new ArrayList<>();
        keys.add(lockKey);

        Object result = redisTemplate.execute(redisScript, keys, value, expireTime);
        log.info("更新redis锁的过期时间:{}", result);
        return (boolean) result;
    }

    /**
     * 加锁
     * @param lockKey    锁
     * @param value      身份标识(保证锁不会被其他人释放)
     * @param expireTime 锁的过期时间(单位:秒)
     * @return 成功返回true, 失败返回false
     */
    public boolean lock(String lockKey, String value, long expireTime) {
    
    
        return redisTemplate.opsForValue().setIfAbsent(lockKey, value, expireTime, TimeUnit.SECONDS);
    }

    /**
     * redisTemplate解锁
     * @param key
     * @param value
     * @return 成功返回true, 失败返回false
     */
    public boolean unlock(String key, String value) {
    
    
        Object currentValue = redisTemplate.opsForValue().get(key);
        boolean result = false;
        if (!Optional.ofNullable(currentValue).isPresent() && value.equals(currentValue)) {
    
    
            result = redisTemplate.opsForValue().getOperations().delete(key);
        }
        return result;
    }

    /**
     * 定时去检查redis锁的过期时间
     */
    @Scheduled(fixedRate = 5000L)
    @Async("redisExecutor")
    public void renewal() {
    
    
        long now = System.currentTimeMillis();
        for (Map.Entry<String, LockInfo> lockInfoEntry : lockInfoMap.entrySet()) {
    
    
            LockInfo lockInfo = lockInfoEntry.getValue();
            if (lockInfo.getRenewalTime() + lockInfo.getRenewalInterval() < now) {
    
    
                renewal(lockInfo.getKey(), lockInfo.getValue(), lockInfo.getExpireTime());
                lockInfo.setRenewalTime(now);
                log.info("lockInfo {}", JSON.toJSONString(lockInfo));
            }
        }
    }

}

redisson

package com.demo.config;

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * redisson 配置类
 *
 * @author vhukze
 * @date 2023/5/17 - 18:43
 */
@Configuration
public class RedissonConfig {
    
    

    @Value("${spring.redis.host}")
    private String host;

    @Value("${spring.redis.port}")
    private String port;

    @Bean
    public RedissonClient redissonClient() {
    
    
        // redis 地址为127.0.0.1:6379 时, 可以无需配置 一行代码搞定
        RedissonClient redisson = Redisson.create();

        return redisson;
//        Config config = new Config();
//        config.useSingleServer().setAddress("redis://" + host + ":" + port);
//        return Redisson.create(config);
    }
}

猜你喜欢

转载自blog.csdn.net/weixin_44824381/article/details/131310552