大神敖 丙redis文章 点击查看
redis事务
Redis事务的主要作用就是串联多个命令防止别的命令插队
注意:redis的事务和mysql事务回滚有点区别。reids命令在组队中redis命令没有出错,而提交命令时出错,出错命令不执行,其他正确的命令是会正常执行的。
事务的生命周期:
事务的创建:使用MULTI开启一个事务
加入队列:在开启事务的时候,每次操作的命令将会被插入到一个队列中,同时这个命令并不会被真的执行
EXEC命令进行提交事务
常用的关于事务的命令有:
MULTI:使用该命令,标记一个事务块的开始,通常在执行之后会回复OK,(但不一定真的OK),这个时候用户可以输入多个操作来代替逐条操作,redis会将这些操作放入队列中。
EXEC:执行这个事务内的所有命令
DISCARD:放弃事务,即该事务内的所有命令都将取消
WATCH:监控一个或者多个key,如果这些key在提交事务(EXEC)之前被其他用户修改过,那么事务将执行失败,需要重新获取最新数据重头操作(类似于乐观锁)。
UNWATCH:取消WATCH命令对多有key的监控,所有监控锁将会被取消。
redis乐观锁:
通过watch命令进行监控事务
redis删除策略
- 惰性删除
当客户端进行读/写该数据的时候判断,是否过期,假如没过期则返回;假如已过期,删除返回不存在 - 定期删除
redis默认每100ms检查是否有过期的key,有过期key则删除。注意:redis不是每隔100ms将所有的key检查一次,而是随机抽取进行检查(如果每隔100ms全部检查,redis直接进去icu)。因此如果只采用定期删除策略,会导致很多key到时见没有删除。
redis数据一致性
在使用redis时,需要保持redis和数据库数据的一致性,最流行的解决方案之一就是延时双删策略
为什么要进行延迟双删?
一般我们在更新数据库数据时,需要同步redis中缓存的数据,所以存在以下两种方法:
- 先执行update操作,再执行缓存清除。
- 先执行缓存清除,再执行update操作。
这两种方案的弊端是当存在并发请求时,很容易出现以下问题:
当请求1执行update操作后,还未来得及进行缓存清除,此时请求2查询到并使用了redis中的旧数据;
当请求1执行清除缓存后,还未进行update操作,此时请求2进行查询到了旧数据并写入了redis
如何实现延迟双删?
所以此时我们需要执行的步骤为:先进行缓存清除,再执行update,最后(延迟N秒)再执行缓存清除。
public void write(String key, Object data) {
Redis.delKey(key);
db.updateData(data);
//需要注意的点 上述中(延迟N秒)的时间要大于一次写操作的时间,一般为3-5秒。
//原因:如果延迟时间小于写入redis的时间,会导致请求1清除了缓存,但是请求2缓存还未写入的尴尬。。。
// ps:一般写入的时间会远小于5秒
Thread.sleep(3000);
Redis.delKey(key);
}
redis分布式锁:
1.使用setnx上锁(setnx key value),通过del释放锁(del key)
2.锁一直没有释放,设置过期时间,自动释放(set key value nx ex 过期时间/秒)
UUID防误删
场景:
如果业务逻辑的执行时间是5s。
1.业务逻辑1没执行完,3秒后锁被自动释放。
2.业务逻辑2获取到锁,执行业务逻辑,3秒后锁被自动释放。
3.业务逻辑3获取到锁,执行业务逻辑
4.业务逻辑1执行完成,开始调用del释放锁,这时释放的是业务逻辑3的锁,导致业务逻辑3的业务只执行1s就被别人释放。
出现没有上锁的情况
解决:
java代码:
@PostMapping("/testLock")
public void testLock() {
String uuId = UUID.randomUUID().toString();
/*获取锁*/
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuId, 3, TimeUnit.SECONDS);
/*获取锁成功*/
if (lock) {
Object num = redisTemplate.opsForValue().get("num");
if (StringUtils.isEmpty(num)) {
return;
}
int tic = Integer.parseInt(num + "");
redisTemplate.opsForValue().set("num", ++tic);
/*获取锁的uuid,只能释放自己的锁*/
String uuidLock = (String)redisTemplate.opsForValue().get("lock");
if(uuId.equals(uuidLock)){
/* 释放锁,del*/
redisTemplate.delete("lock");
}
} else {
try {
Thread.sleep(100);
testLock();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
LUA脚本保证删除的原子性
1.业务逻辑1执行删除时,查询到的lock值确实和uuid相等
uuid=v1
set(lock,uuid);
2.业务逻辑1执行删除前,lock刚好过期时间已到,被redis自动释放
在redis中没有了lock,没有了锁。
3.业务逻辑2获取了lock
index2线程获取到了cpu的资源,开始执行方法
uuid=v2
set(lock,uuid);
4.业务逻辑执行删除,此时会把业务逻辑2的lock删除
删除的业务逻辑2的锁
解决:
@GetMapping("testLockLua")
public void testLockLua() {
//1 声明一个uuid ,将做为一个value 放入我们的key所对应的值中
String uuid = UUID.randomUUID().toString();
//2 定义一个锁:lua 脚本可以使用同一把锁,来实现删除!
String locKey = "lock";
// 3 获取锁
Boolean lock = redisTemplate.opsForValue().setIfAbsent(locKey, uuid, 3, TimeUnit.SECONDS);
// 第一种: lock 与过期时间中间不写任何的代码。
// redisTemplate.expire("lock",10, TimeUnit.SECONDS);//设置过期时间
// 如果true
if (lock) {
// 执行的业务逻辑开始
// 获取缓存中的num 数据
Object value = redisTemplate.opsForValue().get("num");
// 如果是空直接返回
if (StringUtils.isEmpty(value)) {
return;
}
// 不是空 如果说在这出现了异常! 那么delete 就删除失败! 也就是说锁永远存在!
int num = Integer.parseInt(value + "");
// 使num 每次+1 放入缓存
redisTemplate.opsForValue().set("num", String.valueOf(++num));
/*使用lua脚本来锁*/
// 定义lua 脚本
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
// 使用redis执行lua执行
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setScriptText(script);
// 设置一下返回值类型 为Long
// 因为删除判断的时候,返回的0,给其封装为数据类型。如果不封装那么默认返回String 类型,
// 那么返回字符串与0 会有发生错误。
redisScript.setResultType(Long.class);
// 第一个要是script 脚本 ,第二个需要判断的key,第三个就是key所对应的值。
redisTemplate.execute(redisScript, Arrays.asList(locKey), uuid);
} else {
// 其他线程等待
try {
Thread.sleep(1000);
testLockLua();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Redis 的持久化机制是什么?各自的优缺点?
Redis 提供两种持久化机制 RDB(默认) 和 AOF 机制:
RDB持久化:
RDB :Redis DataBase缩写,快照是,Redis默认的持久化方式。按照一定的时间间隔将内存的数据以快照的形式保存到硬盘中,对应产生的数据文件为dump.rdb。通过配置文件中的save参数来定义快照的周期。
优点:
1.只有一个文件 dump.rdb,方便持久化,容灾性好。
2.性能最大化,fork 子进程来完成写操作,让主进程继续处理命令,保证了 redis 的高性能
3.数据集大时,比 AOF 的启动效率更高。
缺点:
数据安全性低。RDB 是间隔一段时间进行持久化,如果持久化之间 redis 发生故障,会发生数据丢失。
AOF持久化:
AOF:Append Only File缩写,将Redis执行的每条写命令记录到单独的aof日志文件中,当重启Redis服务时,会从持久化的日志文件中恢复数据。
当两种方式同时开启时,数据恢复时,Redis会优先选择AOF恢复。
优点
1.数据安全,可以配置每进行一次命令操作就记录到 aof 文件中一次。
2.通过 append 模式写文件,即使中途服务器宕机,可以通过 redis-check-aof 工具解决数据一致性问题。
缺点
1.AOF 文件比 RDB 文件大,且恢复速度慢。
2.数据集大时,比 rdb 启动效率低
redis中主从复制、哨兵模式
主从模式是最简单的实现高可用的方案,核心就是主从同步。
主从切换技术的方法是:当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用。这不是一种推荐的方式,更多时候,我们优先考虑哨兵模式。
哨兵sentinel的功能比单纯的主从架构全面的多了,它具备自动故障转移、集群监控、消息通知等功能
哨兵模式作用:
发送命令,等待Redis服务器(包括主服务器和从服务器)返回监控其运行状态;
哨兵监测到主节点宕机,会自动将从节点切换成主节点,然后通过发布订阅模式通知其他的从节点,修改配置文件,让它们切换主机;
多个从服务器将其转成主服务器,选择条件依次为:
1.选择优先级靠前的
优先级在redis.conf中默认:replica-priority 100 值越小优先级越高
2.选择偏移量最大的
偏移量是指获得原主机数据最全的
3.选择runid最小的服务器
每个redis实例启动后都会随机生成一个40位的runid
哨兵之间还会相互监控,从而达到高可用。