下载nuget包
cs
<ItemGroup>
<PackageReference Include="Caching.CSRedis" Version="3.8.800" />
</ItemGroup>
操作String
cs
try
{
string RedisConStr = "127.0.0.1,defaultDatabase=0,poolsize=3,tryit=0";//连接池最大为3;
CSRedisClient rds = new CSRedisClient(RedisConStr);
rds.Set("key1", "这是第一个");
string sRsult = rds.Get("key1");
rds.Append("key2", "ddfgh"); //追加,没有则生成键值对key2 : ddfgh
string s = rds.GetRange("key2", 0, -1);//输出所有:ddfgh
//得到value
MemoryStream ms = new MemoryStream();
rds.Get("key1", ms); //获取到字符串保存到内存流中去
string result2 = Encoding.UTF8.GetString(ms.ToArray());
Console.WriteLine(result2);
//设置多个keyvalue
rds.MSet("key3", "str3", "key4", "str4", "key5", "7");
//得到多个
string[] sarr = rds.MGet("key3", "key4");
foreach (var sa in sarr)
{
Console.WriteLine(sa);//str3 str4
}
//只对数字有用,否则报错
rds.IncrBy("key5", 1);//:8
//得到类型
string s = rds.ObjectEncoding("key5");//int
//全部清空
rds.NodesServerManager.FlushAll();
//先得到旧值再设置新值
string s = rds.GetSet<string>("key1", "这是第一个?");
Console.WriteLine(s);
}
catch
{
Console.WriteLine("error");
throw new Exception("eroor");
}
操作hash
Redis 中如何保证缓存和数据库双写时的数据一致性?
无论先操作db还是cache,都会有各自的问题,根本原因是cache和db的更新不是一个原子操作,因此总会有不一致的问题。想要彻底解决这种问题必须将cache和db的更新操作归在一个事务之下(例如使用一些分布式事务,或者强一致性的分布式协议)。
或者采用串行化,可以保证强一致性。

写请求为什么更新数据库后是删除缓存而不是更新缓存?

注意看上面的图片,当有两个写请求的线程,线程一比线程二先执行,反而是线程二先执行完。这时候,缓存保存的是A的数据(老数据),数据库保存的是B的数据(新数据),数据不一致了。
写请求时,为什么更新数据库,然后再删除缓存?

如果采用写请求,先删除缓存,再更新数据库就会出现如上图的情况,线程B读到的是老的数据,并且缓存中也保存的是老的数据。
写请求时,先更新数据,后删除缓存一定没有问题吗

可以看到一个读请求和一个写请求,读请求可能会读取到旧的数据,或者当写请求删除缓存失败,读请求会一直读取的是旧的缓存数据。只不过是这种情况,相对于其他的实现方式概率要低很多。
缓存延时双删
如果想再次降低读到旧数据的办法,用双删除策略。就是在更新数据库前,先删除一次缓存,这样可以进一步降低读到旧数据的概率,但依然会有概率读到旧数据。

上述中(延迟N秒)的时间一定要大于请求2将数据库旧数据写入redis的时间,这个时间短则几百毫秒,长则几秒,具体根据自己的业务而定。这个休眠时间 = B读取业务逻辑数据的耗时 + 几百毫秒。
原因:如果延迟时间小于请求2写入redis的时间,会导致请求1清除缓存的时机过早,请求2又会将旧的数据写入redis的尴尬。。。
删除缓存重试机制
延时双删和普通写操作的删除操作都有可能会操作失败,导致数据不一致,删除重试机制就是为了保证删除可靠性。(删除失败的key放到消息队列中)这种机制会造成大量的业务代码入侵。
java
// 业务更新接口(带重试机制)
public void updateUser(User user) {
// 1. 更新数据库
userMapper.update(user);
String cacheKey = "user:" + user.getId() + ":info";
try {
// 2. 尝试删除缓存
redisTemplate.delete(cacheKey);
} catch (Exception e) {
// 3. 删除失败,丢到MQ重试(业务代码入侵点)
MqMessage message = new MqMessage(cacheKey, 0); // 0=初始重试次数
rocketMQTemplate.send("cache-delete-retry-topic", message);
log.error("删除缓存失败,已发送到MQ重试", e);
}
}
// MQ消费者(单独服务,无业务入侵)
@Consumer(topic = "cache-delete-retry-topic")
public void handleCacheDelete(MqMessage message) {
String cacheKey = message.getCacheKey();
int retryCount = message.getRetryCount();
try {
redisTemplate.delete(cacheKey);
// 成功:确认消息,MQ删除
} catch (Exception e) {
if (retryCount < 3) {
// 重试次数不够,延时3s重新入队
message.setRetryCount(retryCount + 1);
rocketMQTemplate.sendDelay("cache-delete-retry-topic", message, 3);
} else {
// 重试上限,告警
alertService.send("缓存删除失败,key=" + cacheKey);
}
}
}
同步biglog异步删除缓存
重试删除缓存机制还可以,就是会造成好多业务代码入侵。通过数据库的binlog来异步淘汰key
redis进阶
Redis 中的内存淘汰机制
设置方式: config set maxmemory-policy volatile-lru
no-eviction: 禁止驱逐数据(当内存达到限制时,就报错)
allkeys-lru: 从redis 中回收最近使用最少的键
volatile-lru: 从设置了过期时间的键中,回收最近使用最少的键
allkeys-random:随机回收redis中的键
volitile-random:从设置了过期时间的键中,随机回收
volitile-ttl:从设置了过期时间的键中,回收存活时间较少的键
Redis.config
启动时候,通过配置文件启动
配置文件unit单位对大小写不敏感,
redis RDB
RDB方式,Redis会是通过一个fork程来做持久化的工作。fork线程先将数据写入到一个临时文件中,写入成功之后,再替换掉之前的 RDB 文件,不会影响主线程的工作,所以这种方式对Redis的性能影响很小。
bash
save 900 1 代表 900s 之内有 1个 key发生修改,则发起内存快照保存
save 300 10 代表 300s 之内有 10个 key发生修改,则发起内存快照保存
#当RDB持久化出现错误后,是否依然进行继续进行工作,yes:不能进行工作,no:可以继续进行工作
stop-writes-on-bgsave-error yes
#使用压缩rdb文件,rdb文件压缩使用LZF压缩算法,yes:压缩,但是需要一些cpu的消耗。no:不压缩,需要更多的磁盘空间
rdbcompression yes
#rdb文件的名称
dbfilename dump.rdb
#数据目录,数据库的写入会在这个目录。rdb、aof文件也会写在这个目录
dir /var/lib/redis
save,满足规则触发一次
flushall触发一次
退出redis触发一次
redisAOF
AOF方式默认是通过一秒一次的一个后台线程 fsync 来进行数据备份操作的,操作方式是 append-only (只追加)的方式
bash
appendfsync yes # 开启AOF功能
appendfsync no # 表示不执行fsync,由操作系统保证数据同步到磁盘,速度最快。由操作系统决定何时同步
appendfsync always # 每次有数据修改时都会写入AOF文件
appendfsync everysec # 每秒同步一次,该策略为AOF的缺省策略
#aof文件名, 保存目录由 dir 参数决定
appendfilename "appendonly.aof"
# AOF文件重写时,是否需要将写指令通过追加的方式追加到原有的AOF文件中。
#(yes只写入到内存缓冲区,no写入到内存缓冲区同时追加到原有AOF文件)
no-appendfsync-on-rewrite no
#aof自动重写配置。当目前aof文件大小超过上一次重写的aof文件大小的百分之多少进行重写,
#当前AOF文件大小是上次日志重写得到AOF文件大小的二倍(设置为100)时,自动启动新的日志重写过程。
auto-aof-rewrite-percentage 100
#设置允许重写的最小aof文件大小,避免了达到约定百分比但尺寸仍然很小的情况还要重写
auto-aof-rewrite-min-size 64mb
#aof文件可能在尾部是不完整的,当redis启动的时候,aof文件的数据被载入内存。重启可能发生在
redis所在的主机操作系统宕机后,尤其在ext4文件系统没有加上data=ordered选项(redis宕机或者异
常终止不会造成尾部不完整现象。)出现这种现象,可以选择让redis退出,或者导入尽可能多的数
据。
如果选择的是yes,当截断的aof文件被导入的时候,会自动发布一个log给客户端然后load。如果
是no,用户必须手动redis-check-aof修复AOF文件才可以。
aof-load-truncated yes #重写 / 宕机后的容错配置
shuatdown。清除rdb文件,手动破坏aof文件


redis-check-aof修复aof



总结
RDB是保存快照,手动save,或者条件触发,将内存数据保存为rdb文件,替换旧的快照,可能丢失。
AOF是记录写命令,按照保存策略,由子进程进行保存,通常按照appendfsync everysec:每秒同步 1 次(默认,性能和安全折中)
| 维度 | RDB | AOF |
|---|---|---|
| 存储内容 | 内存数据的二进制快照(数据本身) | 写操作命令的文本日志(操作过程) |
| 文件大小 | 小(压缩后) | 大(命令日志,易膨胀) |
| 恢复速度 | 快(直接加载二进制数据) | 慢(需要重新执行所有命令) |
| 数据安全性 | 可能丢失数据(丢失两次快照间的修改) | 丢失数据少(取决于同步策略,默认最多丢 1 秒) |
| 写性能 | 好(仅 fork 子进程时短暂阻塞,后续不影响) | 略差(需追加命令到文件,同步磁盘有开销) |
| 兼容性 | 支持所有 Redis 版本 | 部分旧版本不兼容新命令格式 |
| 运维成本 | 低(无需额外处理文件膨胀) | 中等(需关注 AOF 重写,避免文件过大) |
发布订阅

Redis主从复制
启动redis

查看信息

认老大

主机也显示有从机

真实的主从配置应该在配置文件中配置,这样的话是永久的,我们这里使用的是命令,暂时的。
如果命令行配置的从机,宕机后重启会变成主机模式。

细节:
主机可以写,从机只能读不能写。


成成递进
把81的主机配成80就行

某朝篡位,从机变主机
老大断开连接,使用Slaveof no one让自己变成主机,其他节点手动连接到这个最新的主节点,如果老大修复,就重新连接。
哨兵模式


在kconfig目录下,创建一个哨兵文件,例如sentinel1.config
bash
sentinel monitor mymaster 192.168.200.128 6379 1
sentinel down-after-milliseconds mymaster 10000
sentinel failover-timeout mymaster 60000
sentinel parallel-syncs mymaster 1
# mymaster 主节点名,可以任意起名,但必须和后面的配置保持一致。
# 192.168.200.129 6379 主节点连接地址。
# 1:quorum(仲裁数) 最少需要 1 个哨兵节点判断 "主节点不可用",才能正式认定主节点故障(触发后续故障转移)。
# sentinel down-after-milliseconds mymaster 10000:设置Sentinel认为服务器已经断线所需的毫秒数。
# sentinel failover-timeout mymaster 60000:设置failover(故障转移)的过期时间。60s内故障转移没完成就失败就隔60s再试。
# sentinel parallel-syncs mymaster 1 设置在执行故障转移时, 最多可以有多少个从服务器同时对
新的主服务器进行同步。
6379宕机,故障转移

如果曾经的6379恢复了,ta只能被归并到新的主机下,当做从机,这就是哨兵模式规则

哨兵模式确定,扩容太麻烦




Redis缓存穿透和雪崩
缓存雪崩:缓存同一时间大面积的失效,后面的请求都会落到数据库上,造成数据库短时间内承受大量的数据请求
解决方案:缓存数据的过期时间随机设置,防止同一时间大量的数据过期的情况发生
缓存穿透:是指缓存和数据库中都没有数据,导致所有的请求都落到数据库上。数据库短时间承受大量的请求而崩掉
缓存取不到,数据库也娶不到的数据, 设置为key---null 的方式
布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap 中去
缓存击穿: 高并发查询同一条数据,但是redis 中的数据过期了。导致请求发送到了数据库,造成缓存击穿
解决方案: 设置热点数据永不过期;
使用互斥锁。分布式情况下使用分布式的锁
redis分布式锁
小徐同学开发一个抢购功能

但用户太多,服务器压力太大,小徐就使用nginx进行水平扩展服务,又出现超卖问题

因为同步锁只能锁住单个进程,其他进程还能请求。小徐发现有分布式锁这个技术,就用redis实现分布式锁,使用setnx。一个请求进来,setnx,其他请求进来发现有值返回false。不要忘记设置过期时间,因为如果请求服务器挂掉,没有过期时间,会阻塞其他请求。
但是问题又来了,如果请求业务时间大于过期时间,请求二在请求1未处理完还没有释放锁的情况下,发现setnx返回true(因为setnx过期了),就继续抢购。
问题:当2锁过期,线程还处理业务中,2当处理完业务,释放其他线程的锁,
解决:加长时间,添加子线程每10s确认一下线程是否在线,在线重置过期时间。给锁加UUID
cs
using StackExchange.Redis;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace RedisDistributedLockDemo
{
/// <summary>
/// Redis 分布式锁(支持自动续期、防误删、原子操作)
/// </summary>
public class RedisDistributedLock : IDisposable
{
#region 私有字段
private readonly IDatabase _redisDb; // Redis 数据库对象
private readonly string _lockKey; // 锁的 Key
private readonly string _clientId; // 客户端唯一标识(防误删)
private readonly TimeSpan _lockExpiry; // 锁初始过期时间
private readonly TimeSpan _renewInterval; // 续期间隔(建议为过期时间的 1/3)
private readonly CancellationTokenSource _cts = new(); // 取消令牌(控制续期线程)
private bool _isLocked; // 是否成功获取锁
#endregion
#region 构造函数
/// <summary>
/// 初始化分布式锁
/// </summary>
/// <param name="redisConnection">Redis 连接(ConnectionMultiplexer)</param>
/// <param name="lockKey">锁的 Key(如:"stock_lock_1001")</param>
/// <param name="lockExpiry">锁初始过期时间(默认 30 秒)</param>
/// <param name="renewInterval">续期间隔(默认 10 秒,建议为过期时间的 1/3)</param>
public RedisDistributedLock(
IConnectionMultiplexer redisConnection,
string lockKey,
TimeSpan? lockExpiry = null,
TimeSpan? renewInterval = null)
{
_redisDb = redisConnection.GetDatabase();
_lockKey = lockKey ?? throw new ArgumentNullException(nameof(lockKey));
_clientId = Guid.NewGuid().ToString("N"); // 生成唯一标识(无分隔符的 Guid)
_lockExpiry = lockExpiry ?? TimeSpan.FromSeconds(30);
_renewInterval = renewInterval ?? TimeSpan.FromSeconds(10);
// 校验参数合理性(续期间隔不能大于过期时间)
if (_renewInterval >= _lockExpiry)
{
throw new ArgumentException("续期间隔必须小于锁过期时间");
}
}
#endregion
#region 核心方法:加锁(异步)
/// <summary>
/// 尝试获取锁(异步)
/// </summary>
/// <param name="waitTimeout">获取锁的最大等待时间(默认 0 秒,即不等待)</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>true=获取成功,false=获取失败</returns>
public async Task<bool> TryLockAsync(
TimeSpan waitTimeout = default,
CancellationToken cancellationToken = default)
{
var endTime = DateTimeOffset.UtcNow.Add(waitTimeout); // 等待截止时间
// 循环尝试获取锁(直到超时或成功)
do
{
// 核心加锁命令:SET key value NX PX milliseconds
// NX:仅当 key 不存在时设置(保证唯一锁)
// PX:设置过期时间(防止锁泄露)
var lockResult = await _redisDb.StringSetAsync(
key: _lockKey,
value: _clientId,
expiry: _lockExpiry,
when: When.NotExists, // 等价于 NX
flags: CommandFlags.None);
if (lockResult)
{
_isLocked = true;
// 启动后台续期任务(异步,不阻塞当前线程)
_ = StartRenewTaskAsync();
return true;
}
// 未获取到锁,短暂休眠后重试(避免频繁请求 Redis)
await Task.Delay(100, cancellationToken); // 100 毫秒重试一次
// 检查是否超时或取消
} while (DateTimeOffset.UtcNow < endTime && !cancellationToken.IsCancellationRequested);
return false; // 超时或取消,获取锁失败
}
#endregion
#region 核心方法:释放锁(异步)
/// <summary>
/// 释放锁(异步,原子操作)
/// </summary>
public async Task UnlockAsync()
{
if (!_isLocked) return;
try
{
// Lua 脚本:校验锁的持有者是否是当前客户端,是则删除(原子操作)
const string unlockScript = @"
if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('DEL', KEYS[1]) -- 释放锁
else
return 0 -- 不是自己的锁,不操作
end";
// 执行 Lua 脚本(KEYS[1] = _lockKey,ARGV[1] = _clientId)
await _redisDb.ScriptEvaluateAsync(
script: unlockScript,
keys: new RedisKey[] { _lockKey },
values: new RedisValue[] { _clientId });
}
finally
{
_isLocked = false;
_cts.Cancel(); // 取消续期任务
}
}
#endregion
#region 内部方法:启动续期任务
/// <summary>
/// 后台续期任务(自动延长锁的过期时间)
/// </summary>
private async Task StartRenewTaskAsync()
{
try
{
while (!_cts.IsCancellationRequested && _isLocked)
{
// 等待续期间隔(如 10 秒)
await Task.Delay(_renewInterval, _cts.Token);
if (!_isLocked) break;
// 续期命令:SET key value XX PX milliseconds
// XX:仅当 key 存在时设置(避免创建新锁)
// PX:更新过期时间
await _redisDb.StringSetAsync(
key: _lockKey,
value: _clientId,
expiry: _lockExpiry,
when: When.Exists, // 等价于 XX
flags: CommandFlags.None);
}
}
catch (OperationCanceledException)
{
// 续期任务被取消(正常释放锁场景),忽略异常
}
catch (Exception ex)
{
// 记录续期失败日志(如:Redis 连接异常)
Console.WriteLine($"锁续期失败,Key: {_lockKey},异常:{ex.Message}");
}
}
#endregion
#region IDisposable 释放资源
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
_cts.Cancel();
_cts.Dispose();
// 异步释放锁(非阻塞,避免 Dispose 等待)
_ = UnlockAsync();
}
}
~RedisDistributedLock() => Dispose(false);
#endregion
}
}