中文文档地址
字符串(string)、散列(hash)、列表(list)、集合(set)、有序集合(zset)
SpringData是Spring中数据操作的模块,包括对各种数据库的集成,其中对Redis的集成模块就叫SpringDataRedis
RedisTemplate基础信息及序列化规则及配置
redis持久化配置-RDB和AOF
SpringDataRedis中提供了RedisTemplate工具类,其中封装了各种对Redis的操作。并且将不同的数据类型的操作API封装到了不同的类型中:
//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秒
RedisTemplate是Spring提供的对Redis操作的封装,通过提供一系列的操作方法,避免了手动管理连接池、线程安全等问题,同时还支持事务等高级特性。总的来说,Jedis更加轻量级、灵活,适合小型项目;而RedisTemplate则更加适合大型项目,提供了更加完善的封装和高级特性。
参考地址:点此
(唯一索引/主键)
主要是利用数据库中主键唯一约束的特性,一般来说唯一主键比较适用于“插入”时的幂等性,其能保证一张表中只能存在一条带该唯一主键的记录。
使用数据库唯一主键完成幂等性时需要注意的是,该主键一般来说并不是使用数据库中自增主键,而是使用分布式 ID 充当主键,这样才能能保证在分布式环境下 ID 的全局唯一性。
(更新操作下,通过上状态的变化来进行更新操作)
一般只能适用于执行更新操作的过程,我们可以提前在对应的数据表中多添加一个字段,充当当前数据的版本标识。
这样每次对该数据库该表的这条数据执行更新时,都会将该版本标识作为一个条件,值为上次待更新数据中的版本标识的值。
(看第五点-意思一致)
针对客户端连续点击或者调用方的超时重试等情况,例如提交订单,此种操作就可以用 Token 的机制实现防止重复提交。
简单的说就是调用方在调用接口的时候先向后端请求一个全局 ID(Token),请求的时候携带这个全局 ID 一起请求(Token 最好将其放到 Headers 中),后端需要对这个 Token 作为 Key,用户信息作为 Value 到 Redis 中进行键值内容校验,如果 Key 存在且 Value 匹配就执行删除命令,然后正常执行后面的业务逻辑。如果不存在对应的 Key 或 Value 不匹配就返回重复执行的错误信息,这样来保证幂等操作。
((参考第五点-进阶)通过redis缓存特性,先缓存key到redis中,保证后续数据不存在重复的key)
所谓请求序列号,其实就是每次向服务端请求时候附带一个短时间内唯一不重复的序列号,该序列号可以是一个有序 ID,也可以是一个订单号,一般由下游生成,在调用上游服务端接口时附加该序列号和用于认证的 ID。
当上游服务器收到请求信息后拿取该 序列号 和下游 认证ID 进行组合,形成用于操作 Redis 的 Key,然后到 Redis 中查询是否存在对应的 Key 的键值对,根据其结果:
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);
// }
//
// }
}
}
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));
}
}
}
}
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);
}
}