Redis既可以作为数据库使用,也可以作为中间件
数据库时,Redis是一种key-value型的内存数据库,支持数据持久化,可以将数据保存到磁盘上。它的主要特点是读写速度非常快,因为它将所有数据都存储在内存中,而且支持多种数据结构,如字符串、哈希表、列表、集合和有序集合等。
中间件时,Redis通常用于缓存数据和分布式锁的实现。缓存数据可以减轻后端数据库的压力,提高系统的响应速度。分布式锁可以协调不同的进程或线程对共享资源的访问,防止并发访问问题导致的数据不一致或其他异常情况
1.查找 keys pattern 【类似mysql模糊查询 * 匹配0到多个字符,?匹配一个字符,[]匹配中括号一个字符】
keys * 查看所有key keys A? 查看所有A开头长度2的字符串 A2,AA,Ap keys A[12] 查看 A1,A2 2.判断key是否存在 exists kk 【返回存在个数,不存在0】
举例: exists k1 k2 k3 k4 结果:2 说明里面2个存在
3.移动key到指定数据库 move k1 2
注意: move key index 【redis默认16个库,用户默认使用第0库】0.1.2...15
举例:move k1 3
exists k1 —》0 【 k1移走了,不在原0库,故不存在
select 3 —》选择 3库
exists k1 —》1 【 说明移动到1库中了
4.查看key的剩余时间 单位秒
举例: ttl k1 显示数值,但是注意:一般正值,负值时 -1—》key存在但是没设置生存时间。-2—》该key不存在
5.设置key的最大生存时间 expire key seconds 【时间、秒】
举例:expire k1 15 k1存在时间15秒
6.查看key的类型 type key
7.重命名key rename oldKey newKey
举例: rename k1 k56 键k1 名改为 k56
8.删除指定key 值也会跟着删除
举例:del k1 del k1 k2 k3 删除三个key
9.减去某值 decrby k1 2
10.增加某值 incrby k1 3 incrby k1 -3 也可加负数 为减去3
参考:序言 · Redis快速入门 · 看云 (kancloud.cn)
org.springframework.boot
spring-boot-starter-redis
${spring-boot.version}
spring: redis: database: 0 # Redis数据库索引(默认为0) host: localhost # Redis服务器地址 port: 6379 # Redis服务器端口 password: # Redis服务器连接密码(默认为空) timeout: 0 # 连接超时时间(毫秒) lettuce: pool: max-active: 8 # 连接池最大连接数(使用负值表示没有限制) max-wait: -1 # 连接池最大阻塞等待时间(使用负值表示没有限制) max-idle: 8 # 连接池中的最大空闲连接 min-idle: 0 # 连接池中的最小空闲连接
Redis本身提供了一下一种序列化的方式: 存储格式:key:value形式!value有多种数据结构
JdkSerializationRedisSerializer
进行序列化时,被序列化的 Java 类需要实现 Serializable
接口,否则会抛出 NotSerializableException
异常如果我们存储的是String类型,默认使用的是StringRedisSerializer 这种序列化方式。如果我们存储的是对象,默认使用的是 JdkSerializationRedisSerializer,也就是Jdk的序列化方式(通过ObjectOutputStream和ObjectInputStream实现,缺点是我们无法直观看到存储的对象内容)。
//导入包
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
//通用化配置
@Configuration
public class redisComConfig {
//Redis链接工厂
@Autowired
private RedisConnectionFactory redisConnectionFactory;
//缓存操作组件RedisTemplate的自定义配置
@Bean
public RedisTemplate redisTemplate(){
//定义RedisTemplate实例
RedisTemplate redisTemplate=new RedisTemplate();
//设置Redis的链接工厂
redisTemplate.setConnectionFactory(redisConnectionFactory);
//TODO:指定大Key序列化策略为String序列化,Value为JDK自带的序列化策略
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
//TODO:指定hashKey序列化策略为String序列化-针对hash散列存储
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
return redisTemplate;
}
//缓存操作组件StringRedisTemplate
@Bean
public StringRedisTemplate stringRedisTemplate(){
//采用默认配置即可-后续有自定义配置时则在此处添加即可
StringRedisTemplate stringRedisTemplate=new StringRedisTemplate();
stringRedisTemplate.setConnectionFactory(redisConnectionFactory);
return stringRedisTemplate;
}
}
//单元测试类
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
@Slf4j
public class RedisTest {
//由于之前已经自定义注入RedisTemplate组件,因而在此可以直接自动装配
@Autowired
private RedisTemplate redisTemplate;
//定义JSON序列化与反序列化框架类
@Autowired
private ObjectMapper objectMapper;
//===========字符串
//采用RedisTemplate将字符串信息写入缓存中并读取出来
@Test
public void one(){
//定义字符串内容及存入缓存的key
final String content="RedisTemplate信息";
final String key="redis:template:one:string";
//Redis通用的操作组件
ValueOperationsvalueOperations=redisTemplate.opsForValue();
//将字符串信息写入缓存中
log.info("写入缓存中的内容:{} ", content);
valueOperations.set(key, content);
//从缓存中读取内容
Object result=valueOperations.get(key);
log.info("读取出来的内容:{} ", result);
//==================对象
//构造对象信息
Dog dog=new Dpg(1, "华仔!");
//将序列化后的信息写入缓存中
final String key="redis:template:two:object";
final String content=objectMapper.writeValueAsString(dog);
valueOperations.set(key, content);
log.info("写入缓存对象的信息:{} ", user);
//从缓存中读取内容
Object result=valueOperations.get(key);
if (result! =null){
Dog dog2=objectMapper.readValue(result.toString(), Dog.class);
log.info("读取缓存内容并反序列化后的结果:{} ", dog2);
}
//=========== 也可写作 //灵活点 转换字符串就好
valueOperations.set(key,SON.toJSONString(dog));
JSONObject.parseObject(result.toString(), Dog.class);
}
}
import org.springframework.data.redis.connection.DataType;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ZSetOperations.TypedTuple;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* Redis工具类*/
@Component
public class RedisUtil {
private StringRedisTemplate redisTemplate;
@Autowired
public void setRedisTemplate(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
public StringRedisTemplate getRedisTemplate() {
return this.redisTemplate;
}
/** -------------------key相关操作--------------------- */
/**
* 删除key
* @param key
*/
public void delete(String key) {
redisTemplate.delete(key);
}
/**
* 批量删除key
* @param keys
*/
public void delete(Collection keys) {
redisTemplate.delete(keys);
}
/**
* 序列化key
* @param key
* @return 返回一个字节数组
*/
public byte[] dump(String key) {
return redisTemplate.dump(key);
}
/**
* 是否存在key
* @param key
* @return
*/
public Boolean hasKey(String key) {
return redisTemplate.hasKey(key);
}
/**
* 设置过期时间
*
* @param key
* @param timeout
* @param unit
* @return
*/
public Boolean expire(String key, long timeout, TimeUnit unit) {
return redisTemplate.expire(key, timeout, unit);
}
/**
* 设置过期时间
*
* @param key
* @param date
* @return
*/
public Boolean expireAt(String key, Date date) {
return redisTemplate.expireAt(key, date);
}
/**
* 查找匹配的key
*
* @param pattern
* @return
*/
public Set keys(String pattern) {
return redisTemplate.keys(pattern);
}
/**
* 将当前数据库的 key 移动到给定的数据库 db 当中
*
* @param key
* @param dbIndex
* @return
*/
public Boolean move(String key, int dbIndex) {
return redisTemplate.move(key, dbIndex);
}
/**
* 移除 key 的过期时间,key 将持久保持
*
* @param key
* @return
*/
public Boolean persist(String key) {
return redisTemplate.persist(key);
}
/**
* 返回 key 的剩余的过期时间
*
* @param key
* @param unit
* @return
*/
public Long getExpire(String key, TimeUnit unit) {
return redisTemplate.getExpire(key, unit);
}
/**
* 返回 key 的剩余的过期时间
*
* @param key
* @return
*/
public Long getExpire(String key) {
return redisTemplate.getExpire(key);
}
/**
* 从当前数据库中随机返回一个 key
*
* @return
*/
public String randomKey() {
return redisTemplate.randomKey();
}
/**
* 修改 key 的名称
*
* @param oldKey
* @param newKey
*/
public void rename(String oldKey, String newKey) {
redisTemplate.rename(oldKey, newKey);
}
/**
* 仅当 newkey 不存在时,将 oldKey 改名为 newkey
*
* @param oldKey
* @param newKey
* @return
*/
public Boolean renameIfAbsent(String oldKey, String newKey) {
return redisTemplate.renameIfAbsent(oldKey, newKey);
}
/**
* 返回 key 所储存的值的类型
*
* @param key
* @return
*/
public DataType type(String key) {
return redisTemplate.type(key);
}
/** -------------------string相关操作--------------------- */
/**
* 设置指定 key 的值
* @param key
* @param value
*/
public void set(String key, String value) {
redisTemplate.opsForValue().set(key, value);
}
/**
* 获取指定 key 的值
* @param key
* @return
*/
public String get(String key) {
return redisTemplate.opsForValue().get(key);
}
/**
* 返回 key 中字符串值的子字符
* @param key
* @param start
* @param end
* @return
*/
public String getRange(String key, long start, long end) {
return redisTemplate.opsForValue().get(key, start, end);
}
/**
* 将给定 key 的值设为 value ,并返回 key 的旧值(old value)
*
* @param key
* @param value
* @return
*/
public String getAndSet(String key, String value) {
return redisTemplate.opsForValue().getAndSet(key, value);
}
/**
* 对 key 所储存的字符串值,获取指定偏移量上的位(bit)
*
* @param key
* @param offset
* @return
*/
public Boolean getBit(String key, long offset) {
return redisTemplate.opsForValue().getBit(key, offset);
}
/**
* 批量获取
*
* @param keys
* @return
*/
public List multiGet(Collection keys) {
return redisTemplate.opsForValue().multiGet(keys);
}
/**
* 设置ASCII码, 字符串'a'的ASCII码是97, 转为二进制是'01100001', 此方法是将二进制第offset位值变为value
*
* @param key 位置
* @param value
* 值,true为1, false为0
* @return
*/
public boolean setBit(String key, long offset, boolean value) {
return redisTemplate.opsForValue().setBit(key, offset, value);
}
/**
* 将值 value 关联到 key ,并将 key 的过期时间设为 timeout
*
* @param key
* @param value
* @param timeout
* 过期时间
* @param unit
* 时间单位, 天:TimeUnit.DAYS 小时:TimeUnit.HOURS 分钟:TimeUnit.MINUTES
* 秒:TimeUnit.SECONDS 毫秒:TimeUnit.MILLISECONDS
*/
public void setEx(String key, String value, long timeout, TimeUnit unit) {
redisTemplate.opsForValue().set(key, value, timeout, unit);
}
/**
* 只有在 key 不存在时设置 key 的值
*
* @param key
* @param value
* @return 之前已经存在返回false,不存在返回true
*/
public boolean setIfAbsent(String key, String value) {
return redisTemplate.opsForValue().setIfAbsent(key, value);
}
/**
* 用 value 参数覆写给定 key 所储存的字符串值,从偏移量 offset 开始
*
* @param key
* @param value
* @param offset
* 从指定位置开始覆写
*/
public void setRange(String key, String value, long offset) {
redisTemplate.opsForValue().set(key, value, offset);
}
/**
* 获取字符串的长度
*
* @param key
* @return
*/
public Long size(String key) {
return redisTemplate.opsForValue().size(key);
}
/**
* 批量添加
*
* @param maps
*/
public void multiSet(Map maps) {
redisTemplate.opsForValue().multiSet(maps);
}
/**
* 同时设置一个或多个 key-value 对,当且仅当所有给定 key 都不存在
*
* @param maps
* @return 之前已经存在返回false,不存在返回true
*/
public boolean multiSetIfAbsent(Map maps) {
return redisTemplate.opsForValue().multiSetIfAbsent(maps);
}
/**
* 增加(自增长), 负数则为自减
*
* @param key
* @return
*/
public Long incrBy(String key, long increment) {
return redisTemplate.opsForValue().increment(key, increment);
}
/**
*
* @param key
* @return
*/
public Double incrByFloat(String key, double increment) {
return redisTemplate.opsForValue().increment(key, increment);
}
/**
* 追加到末尾
*
* @param key
* @param value
* @return
*/
public Integer append(String key, String value) {
return redisTemplate.opsForValue().append(key, value);
}
/** -------------------hash相关操作------------------------- */
/**
* 获取存储在哈希表中指定字段的值
*
* @param key
* @param field
* @return
*/
public Object hGet(String key, String field) {
return redisTemplate.opsForHash().get(key, field);
}
/**
* 获取所有给定字段的值
*
* @param key
* @return
*/
public Map
package com.vinjcent.utils;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.List;
import java.util.Map;
/**
* Json序列化工具类
*/
public class JsonUtils {
public static final ObjectMapper OBJ_MAPPER = new ObjectMapper();
/**
* 普通对象之间类型的转化
*
* @param source 原对象
* @param target 目标类型
* @param 目标参数类型
* @return object after transformation
*/
public static T objParse(Object source, Class target) {
try {
if (source.getClass().equals(target)) {
return OBJ_MAPPER.convertValue(source, target);
}
} catch (Exception ignored) {
}
return null;
}
/**
* 普通列表之间类型的转化
*
* @param source 原列表
* @param target 目标列表类型
* @param 目标列表参数类型
* @return list after transformation
*/
public static List listParse(List source, Class target) {
try {
return OBJ_MAPPER.convertValue(source, OBJ_MAPPER.getTypeFactory().constructCollectionType(List.class, target));
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 普通哈希列表之间类型的转化
*
* @param source 原哈希列表
* @param keyType 键类型
* @param valueType 值类型
* @param 原键类型
* @param 原值类型
* @param 目标键类型
* @param 目标值类型
* @return map after transformation
*/
public static Map mapParse(Map source, Class keyType, Class valueType) {
try {
return OBJ_MAPPER.convertValue(source, OBJ_MAPPER.getTypeFactory().constructMapType(Map.class, keyType, valueType));
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 将对象转换成json字符串(序列化)
*
* @param obj 原对象
* @return string after serialized object
*/
public static String objToJson(Object obj) {
if (obj == null) {
return null;
}
try {
return OBJ_MAPPER.writeValueAsString(obj);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return "";
}
/**
* 将json转化为对象(反序列化)
*
* @param source 原对象json
* @param target 目标类型
* @param 目标类参数类型
* @return deserialized object
*/
public static T jsonToObj(String source, Class target) {
OBJ_MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
try {
return OBJ_MAPPER.readValue(source, target);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 将列表json转化为对象集合
*
* @param source 原对象json
* @param target 目标类型
* @param 目标类参数类型
* @return deserialized object collection
*/
public static List jsonToList(String source, Class target) {
try {
return OBJ_MAPPER.readValue(source, OBJ_MAPPER.getTypeFactory().constructCollectionType(List.class, target));
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 将哈希表json转化为对象集合
*
* @param source 原对象json
* @param keyType 键类型
* @param valueType 值类型
* @param 键参数类型
* @param 值参数类型
* @return deserialized map collection
*/
public static Map jsonToMap(String source, Class keyType, Class valueType) {
try {
return OBJ_MAPPER.readValue(source, OBJ_MAPPER.getTypeFactory().constructMapType(Map.class, keyType, valueType));
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
两种方式:
RDB:定期通过异步的方式将数据保存到磁盘上 半持久化 【指定的时间间隔内将内存中的数据集快照写入磁盘,即snapshot快照,恢复时也是直接将快照文件读到内存中】
Redis会开启异步线程自动的备份数据文件 文件后缀 .rdb
Redis默认停机时会执行一次RDB
Redis会执行RDB每十五分钟修改1次,每五分钟修改10次,每一分钟修改10000次即进行一次RDB持久化场景:
定期备份数据;
数据集比较大时,启动时加载数据速度更快。
AOF:保存每一次的数据变化(增量保存),是全持久化 (追加文件)
Redis处理的每一个写命令都会记录在AOF文件 文件以 .aof 扩展名保存
常用操作:开启AOF、修改记录频率、设置触发阈值场景:
实时持久化,适用于需要实时同步数据到磁盘的场景;
对于需要追溯操作历史的场景,AOF更有优势。
RDB与AOF持久化机制都可以使用
SAVE
或BGSAVE
命令。save:Redis会停止处理任何客户端请求,直到保存操作完成。在此期间,Redis不会响应任何客户端请求,并且任何进来的请求都将排队等待保存操作完成 【同步地将数据保存到磁盘】阻塞调用
bgsave:创建一个子进程来执行保存操作,而主要的Redis进程会继续处理客户端请求 非阻塞调用
有一点需要注意,虽然bgsave操作不会阻塞主进程,但是fork操作会阻塞主进程,频繁地进行快照同样会对Redis性能造成很大的影响。
Redis会单独创建(fork)一个子进程来进行持久化,会先把数据写入到一个临时文件中,待持久化过程都结束后,再用这个临时文件替换上一个持久化完成的文件,即上一个快照文件。如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是特别敏感,那么RDB的方式就比AOF的方式更加高效,RDB的一个缺点就是最后一次持久化的数据可能会丢失。
配置:在redis配置文件redis.conf中dbfilename dump.rdb #备份后产生的快照文件名为dump.rdb 可修改
save 900 1 # save 秒钟 写操作次数 15分钟1次改动 redis自动保存快照 禁用RDB,不设置save指令或给save传入空字符串即可 如:save “”
stop-writes-on-bgsave-error yes #当Redis无法写入磁盘时,服务器会直接关掉Redis的写操作,默认为Yes,推荐开启rdbcompression yes # Redis会采用LZF算法进行压缩不想消耗CPU进行压缩,也可以选择设置为no
rdbchecksum yes #快照存储后,可以让Redis使用CRC64算法进行数据校验,但是这样做会增加约10%的性能消耗得到最大的性能提升,可以关闭,推荐开启rdb-del-sync-files no #主从复制时,是否删除用于同步的从机上的 RDB 文件 [只有当从机的 RDB 和 AOF 持久化功能都未开启时才生效]
dir ./ #快照文件的存放路径,这个配置项一定是个目录,而不能是文件名。默认是和当前配置文件保存在同一目录。
命令:
save:save命令可用于手动保存快照,但是当save时只管保存,其他全部阻塞,不建议使用该命令进行手动保存快照。
bgsave:redis会在后台异步进行快照操作,快照同时还可以响应客户端请求
lastsave:获取最后一次成功执行快照的时间
flushall:也会产生快照文件,但是里面是空的,毫无意义
日志的形式来记录每个操作(增量保存),将Redis执行过的所有写指令记录下来(读操作不记录),只许追加文件但不允许改写文件
Redis重启时会根据日志的内容将写指令从头到尾执行一次已完成数据恢复工作。
配置:仍然在redis配置文件redis.conf中
appendonly yes # 默认no,修改为yes,开启aof功能
appendfilename "备份的文件名称.aof" #备份的文件名称
appendfsync always # 记录频率 always.:始终同步 everysec:每秒将写操作记入日志一次 no:Redis不主动同步,把同步的时机交给操作系统
auto-aof-rewrite-percentage 100 # AOF文件比上次文件 增长超过多少百分比则触发重写
auto-aof-rewrite-min-size 64mb # AOF文件体积最小多大以上才触发重写
【Linux】Redis高可用概述1(RDB和AOF的备份恢复)_如果rdb和aof同时开启,怎么恢复-CSDN博客备份讲解带图案例
RDB持久化的优点:
- 性能:RDB持久化是将Redis在内存中的数据快照保存到磁盘,因此在恢复数据时速度较快,适用于大规模数据集和快速恢复的场景。
- 空间效率:RDB文件通常比AOF文件小,因为RDB是压缩的二进制文件,只保存了数据快照。
RDB持久化的缺点:
- 数据丢失风险:由于RDB是定期执行的快照操作,如果Redis在最近一次快照之后崩溃,可能会发生数据丢失。
- 恢复时间:在发生故障时,需要从最近一次的RDB文件中恢复数据,这可能导致一段时间内的数据丢失。
AOF持久化的优点:
- 数据安全性:AOF持久化记录了所有的写操作,因此具有更好的数据安全性,能够提供更好的持久性保证。
- 恢复精确性:AOF文件可以更准确地恢复数据,因为它包含了每个写操作的详细日志。
AOF持久化的缺点:
- 性能:AOF文件通常比RDB文件大,且写入操作需要实时追加到AOF文件中,因此相比RDB持久化,AOF对性能有一定的影响。
- 恢复时间:在发生故障时,Redis需要通过重新执行AOF文件中的写操作来恢复数据,这可能需要更长的时间。
一、AOF文件存在且没有损坏,Redis将使用AOF文件中的操作日志来还原数据
二、以下情况下,Redis才会考虑使用RDB文件进行数据恢复:
AOF文件不存在或已损坏:如果Redis无法找到有效的AOF文件,或者AOF文件已损坏,它将尝试使用RDB文件进行恢复。
配置文件中指定了使用RDB文件进行恢复:你可以在Redis配置文件(redis.conf)中使用
appendonly no
将AOF持久化关闭,并将save
选项配置为适当的RDB快照策略。这将使Redis在重启时使用RDB文件进行恢复。
一个key/热门数据过期或被删除瞬间,此时大量请求访问此数据/大并发下,直接请求数据库。导致数据库无法承受巨大的并发压力。
解决方案:用互斥锁(mutex)或分布式锁(distributed lock)来保护缓存失效时的并发访问,避免对数据库的直接打击。
恶意查询一个不存在的数据,由于缓存和数据库都没有相应的数据,每次查询都会直接访问数据库。这可能是攻击者故意进行的,也可能是由于业务逻辑错误导致的。
解决方案:Bitmap布隆过滤器、缓存空对象
缓存中大量的数据同时过期或失效,导致大量请求直接访问数据库,引起数据库压力剧增,甚至导致数据库崩溃 【服务器重启期间,大量缓存集中某个时间点失效】
解决方案:设置不同的缓存失效时间、使用热点数据预加载、使用多级缓存、redis高可用(集群部署)、限流降级
某些分布式系统中,为了提高性能和可用性,可以使用多个副本或缓存来存储相同的数据。当数据发生变化时,需要确保所有副本或缓存中的数据都保持一致。
概括:涉及到数据库和缓存双写,就必然会存在不一致的问题
主要细分为最终一致性 和 强一致性
1.业务场景对数据有强一致性要求,那就不能放缓存
2.使用了缓存,开发者只能降低不一致发生的概率,无法完全避免。因此,有强一致性要求的数据,不能放缓存
同步写(Synchronous Write): 在进行写操作时,需要等待所有副本或缓存完成数据更新后才返回成功。这种方式可以保证数据的强一致性,但会增加写操作的延迟。
异步写(Asynchronous Write): 在进行写操作时,只更新主副本或主缓存,然后异步地将变更传播给其他副本或缓存。虽然写操作的延迟较低,但在异步传播的过程中可能存在数据不一致的风险。
两阶段提交(Two-Phase Commit,2PC): 2PC 是一种协调多个节点的分布式事务协议。在进行写操作时,首先向所有副本或缓存发送准备请求,待所有副本或缓存都准备好后,再发送提交请求。如果任何一个副本或缓存出现问题,则发送回滚请求。2PC 可以保证多个节点的数据一致性,但可能存在单点故障和性能瓶颈。
基于版本号或时间戳的冲突解决: 每个副本或缓存都维护一个版本号或时间戳,写操作时先增加版本号或时间戳,然后将更新广播给其他副本或缓存。当副本或缓存收到更新请求时,会比较版本号或时间戳,如果发现冲突,则根据一定的规则解决冲突。
某个用户每天的访问量特别大怎么办?难道频繁操作数据库中 num 字段 吗?每访问一次就写一次数据库会卡死的!嘿嘿。redis缓存来实现访问量的增加
id | login_code | num |
1 | xiao_ming | 12 |
2 | xiao_bai | 120 |
// 引入RedisTemplate
@Autowired
private RedisTemplate redisTemplate;
// 增加num
public void increaseNumByCode(String loginCode) {
// 拼接缓存的key值
String key = "code_type:" + loginCode;
// 尝试从缓存中获取num的值
Integer num = (Integer) redisTemplate.opsForValue().get(key);
if (num == null) {
// 如果缓存中不存在该name的num,则新建一个缓存并将num初始化为1
num=1;
} else {
// 如果缓存中存在该name的num,则将num加1
num += 1;
}
//缓存(1天超时)
redisTemplate.opsForValue().set(key, num,1,TimeUnit.DAYS);
}
代码来源:redis应用——实现访问量案例(redis+定时任务+分布式锁)_redis记录访问量 如何保存数据库-CSDN博客
@Scheduled(cron = "0 0,25 9 * * ? ")
public void doCacheVisit() {
log.info("定时任务1=>开始向数据库储存访问数据");
RLock lock = redissonClient.getLock("bode:scheduleTask:updateVisit:lock"); // 设置锁
try {
// 只有一个线程能获取到锁
if (lock.tryLock(0, -1, TimeUnit.MILLISECONDS)) {
log.info("getLock: " + Thread.currentThread().getId());
// 获取所有缓存访问量的key
Collection keys = redisCacheUtil.keys("visit:*");
// 遍历key对每个用户的数据库数据进行更新操作
keys.forEach(key -> {
Integer visit = redisCacheUtil.getCacheObject(key); // 访问量
Long id = Long.parseLong(key.replaceAll("visit:", "")); // 该用户id
// 更新数据库
User user = new User();
user.setId(id);
user.setVisit(visit);
userService.updateById(user);
});
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 只能释放自己的锁
if (lock.isHeldByCurrentThread()) {
log.info("unLock: " + Thread.currentThread().getId());
lock.unlock();
}
}
log.info("定时任务1=>访问数据储存完成");
}
Redis 命令参考 — Redis 命令参考 (redisfans.com)
黑马Redis6高级篇_redis 黑马-CSDN博客 ——写的挺全的
分布式中间件之Redis_redis是数据库还是中间件-CSDN博客