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
    }
}
相关推荐
数据最前线6 分钟前
雪上加霜,Oracle向蓝色巨人最后的阵地发起挑战
数据库·oracle
于眠牧北26 分钟前
Charles配置指定域名抓取和雷电模拟器中抓取APP接口配置
数据库·redis·缓存
福娃筱欢42 分钟前
57014 canceling statement due to user request(金仓 KingbaseES)
数据库·kingbase·金仓数据库
ClouGence1 小时前
SAP HANA 数据同步遇到长事务,如何兼顾实时性与完整性?
数据库·后端
数据库小学妹1 小时前
数据库管理工具有哪些?2026主流工具分类与选型对照
数据库·数据库管理工具·数据库工具·信创数据库·数据库客户端
bingHHB1 小时前
销售订单同步实战:MES生产领料/补料→金蝶材料出库
数据库·集成测试
yexianglunbai1 小时前
冷备份与热备份的区别详解
数据库
ZhengEnCi1 小时前
SQL 与 NoSQL 本质差异完全指南:从数据模型到选型决策的深度剖析
数据库
龙腾AI白云1 小时前
数字孪生:与人工智能融合的技术图景
大数据·数据库·人工智能·知识图谱
夜雪一千2 小时前
MySQL中修改JSON类型的数据实战
数据库·mysql·json