MicroService(Redis)

下载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
    }
}
相关推荐
腾讯云数据库2 小时前
「腾讯云NoSQL」技术之MongoDB篇:MongoDB 5.0→8.0 balance性能提升40%内幕揭秘
数据库·nosql
一 乐2 小时前
远程在线诊疗|在线诊疗|基于java和小程序的在线诊疗系统小程序设计与实现(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·小程序
CodeCraft Studio2 小时前
Excel处理控件Aspose.Cells教程:如何使用C#在Excel中添加、编辑和更新切片器
ui·c#·excel·aspose·excel切片器·创建表格切片器
落叶的悲哀2 小时前
mysql tidb like查询有换行符内容问题解决
数据库·mysql·tidb
wangchen_03 小时前
MySQL索引
数据库·mysql
哈__3 小时前
数据库迁移实操与金仓数据库技术优势:从语法兼容到自动化落地
数据库
.NET修仙日记3 小时前
第四章:C# 面向对象编程详解:从类与对象到完整项目实践
开发语言·c#·.net·源码·教程·.net core
蟹至之3 小时前
增删查改(其一) —— insert插入 与 select条件查询
数据库·mysql·增删查改
Yeats_Liao4 小时前
时序数据库系列(七):性能监控实战指标收集
数据库·后端·时序数据库