草庐IT

.NetCore下基于FreeRedis实现的Redis6.0客户端缓存之缓存键条件优雅过滤

好风凭借力 2023-03-28 原文

前言

众所周知内存缓存(MemoryCache)数据是从内存中获取,性能表现上是最优的,但是内存缓存有一个缺点就是不支持分布式,数据在各个部署节点上各存一份,每份缓存的过期时间不一致,会导致幻读等各种问题,所以我们实现分布式缓存通常会用上Redis

但如果在高并发的情况下读取Redis的缓存,会进行频繁的网络I/O,假如有一些不经常变动的热点缓存,这不就会白白浪费了带宽,并且读到数据以后可能还需要进行反序列化,还影响了CPU性能,造成资源的浪费

从Redis 6.0开始有一个重要特性就是支持客户端缓存(仅支持String类型),效果跟内存缓存是一样的,数据都是从内存中获取,如果服务端缓存数据发送变动,会在极短的时间内通知到所有客户端进行数据同步

在 .NetCore 环境中,我们常用的Redis组件是 StackExchangeRedis 和 CSRedisCore,但是都不支持6.0的客户端缓存这一特性,CSRedisCore 的作者在前两年又重新开发了一个叫 FreeRedis 的组件,并支持了客户端缓存

我们当时为了实现某个对性能有较高要求的产品需求,但不想额外增加硬件上的资源,急需使用上这一特性,在调研后发现了这个组件,经过测试后发现没什么问题就直接用上了

不过我们的主力组件还是CSRedisCore,FreeRedis基本只是用到了客户端缓存,因为当时的版本还不支持异步方法,我记得是今年才加上的

FreeRedis组件介绍原文,有关客户端缓存具体实现原理看看这篇就够了:FreeRedis

目前FreeRedis在我司项目中也已经稳定运行了一年多,这里分享一下我们在项目中的实际用法

 

扩展前

为什么要扩展?因为当看过官方的Demo以后,其中让我比较难受的是本地缓存键的过滤条件设置

 

我想到的有三种方式配置这个条件

第一种:在具体实现某个缓存的地方,才设置过滤条件

缺点:

每次都得写一遍有点冗余,而且查看源码可以发现UseClientSideCaching这个方法每次都会实例一个叫ClientSideCachingContext的类,并在里面添加订阅、添加拦截器等一系列操作

这种方式我测试过,虽然每次都调用一下不影响最后客户端缓存效果,但RedisClient中的拦截器是一直在新增的,这上线后不得崩了?

所以意味具体业务实现代码中每次还实现一下不重复调用UseClientSideCaching的特殊逻辑,即使实现了,但每个不重复的Key都会往RedisClient新增一个拦截器,极力不推荐这种方式!

 

第二种:在同一个地方把所有需要进行本地缓存的键一口气设置好过滤条件

缺点:

时间长了以后,这里会写得非常的长,非常的丑陋,而且你并不知道哪些键已经废弃以及对应的业务

当然项目是从头到尾是你一个人负责开发的或需要本地缓存的Key并不多的时候,这种方式其实也够了

 

第三种:所有用到客户端缓存的键约定好一个统一命名前缀,那么过滤条件这里只需要写一个 StartWith(命名前缀) 的条件就行了

缺点:

需要给团队提前培训下这个注意项,但是时间长了以后,大伙完全不知道后面匹配的那么多键对应是什么业务

某些业务可能一口气需要用到了好几个缓存Key组合进行实现,但其中只有一个Key需要本地缓存,那么这个Key的前缀和其他Key的业务命名前缀就不统一了,虽然没什么问题,但是在客户端工具中查看键值时没放在一起,不利于查找

在Key不多且项目参与人数不多的情况下,用这个方式是最简单方便的

 

 三种方式在实现好用程度上排个序: 第三种 > 第二种 > 第一种

 

扩展后

三种方式在我司项目中其实都不好用,我们项目中之前的所有缓存都是一个缓存实现对应一个缓存类,每个缓存类会继承一个对应该缓存用的Redis数据结构基类,例如CacheBaseStringCacheBaseSetCacheBaseSortedSetCacheBaseList...等

基类中已经实现好了对应数据结构通用的方法,例如CacheBaseString中已经实现了Get Set Del Expire这样的通用方法,在派生的缓存类中只要重写基类的抽象方法,设置下Key的命名缓存过期时间,一个缓存实现就结束了,这样便于管理和使用,团队的小伙伴几年来也都习惯了这种用法

所以基于这个要求,我们对FreeRedis的客户端缓存实现进行一下扩展,首先客户端缓存只支持String类型,所以就是再写一个String结构的ClientSideCacheBase就好了,最麻烦的就是如何优雅的统一实现Key的过滤条件

可以发现UseClientSideCaching中KeyFilter是个Lambda Func委托,返回一个布尔值

 

那么我马上想到的就是表达式树,我们在各种高度封装的ORM中经常能看到使用表达式树去组装SQL的Where条件

同样的原理,我们也可以通过在项目启动时通过反射拿到所有派生类,并调用基类中的一个抽象方法,最后合并表达树,返回一个Func给这个KeyFilter

1. 首先我们先设计一下基类

其中核心的两个方法就是 Key的抽象过滤条件的抽象,其中的 FreeRedisService 是已经实现好的一个FreeRedisClient,需要在IOC容器中注入为单例,所以在这基类的构造函数中,必须传入IServiceProvider,从容器拿到FreeRedisService实例才能实现下面那些通用方法

    /// <summary>
    /// Redis6.0客户端缓存实现基类
    /// </summary>
    public abstract class ClienSideCacheBase
    {
        /// <summary>
        /// RedisService
        /// </summary>
        private static FreeRedisService _redisService;

        /// <summary>
        /// 获取RedisKey
        /// </summary>
        /// <returns></returns>
        protected abstract string GetRedisKey();

        /// <summary>
        /// 设置客户端缓存Key过滤条件
        /// </summary>
        /// <returns></returns>
        public abstract Expression<Func<string,bool>> SetCacheKeyFilter();

        /// <summary>
        /// 私有构造函数
        /// </summary>
        private ClienSideCacheBase() { }

        /// <summary>
        /// 构造函数
        /// </summary>
        /// <param name="serviceProvider"></param>
        public ClienSideCacheBase(IServiceProvider serviceProvider)
        {
            _redisService = serviceProvider.GetService<FreeRedisService>();
        }

        /// <summary>
        /// 获取值
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <returns></returns>
        public T Get<T>()
        {
            return _redisService.Instance.Get<T>(GetRedisKey());
        }

        /// <summary>
        /// 设置值
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="data"></param>
        /// <returns></returns>
        public bool Set<T>(T data)
        {
            _redisService.Instance.Set(GetRedisKey(),data);
            return true;
        }

        /// <summary>
        /// 设置值
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="data"></param>
        /// <param name="seconds"></param>
        /// <returns></returns>
        public bool Set<T>(T data,int seconds)
        {
            _redisService.Instance.Set(GetRedisKey(),data,TimeSpan.FromSeconds(seconds));
            return true;
        }

        /// <summary>
        /// 设置值
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="data"></param>
        /// <param name="expired"></param>
        /// <returns></returns>
        public bool Set<T>(T data,TimeSpan expired)
        {
            _redisService.Instance.Set(GetRedisKey(),data,expired);
            return true;
        }

        /// <summary>
        /// 设置值
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="data"></param>
        /// <param name="expiredAt"></param>
        /// <returns></returns>
        public bool Set<T>(T data,DateTime expiredAt)
        {
            _redisService.Instance.Set(GetRedisKey(),data,TimeSpan.FromSeconds(expiredAt.Subtract(DateTime.Now).TotalSeconds));
            return true;
        }

        /// <summary>
        /// 设置过期时间
        /// </summary>
        /// <returns></returns>
        public bool SetExpire(int seconds)
        {
            return _redisService.Instance.Expire(GetRedisKey(),TimeSpan.FromSeconds(seconds));
        }

        /// <summary>
        /// 设置过期时间
        /// </summary>
        /// <returns></returns>
        public bool SetExpire(TimeSpan expired)
        {
            return _redisService.Instance.Expire(GetRedisKey(),expired);
        }

        /// <summary>
        /// 设置过期时间
        /// </summary>
        /// <returns></returns>
        public bool SetExpireAt(DateTime expiredTime)
        {
            return _redisService.Instance.ExpireAt(GetRedisKey(),expiredTime);
        }

        /// <summary>
        /// 移除缓存
        /// </summary>
        /// <returns></returns>
        public long Remove()
        {
            return _redisService.Instance.Del(GetRedisKey());
        }

        /// <summary>
        /// 缓存是否存在
        /// </summary>
        /// <returns></returns>
        public bool Exists()
        {
            return _redisService.Instance.Exists(GetRedisKey());
        }
    }

具体继承用法如下:

    /// <summary>
    /// 实现客户端缓存Demo1
    /// </summary>
    public class ClientSideDemoOneCache : ClienSideCacheBase
    {
        /// <summary>
        /// 构造函数
        /// </summary>
        /// <param name="serviceProvider"></param>
        public ClientSideDemoOneCache(IServiceProvider serviceProvider) : base(serviceProvider) { }

        /// <summary>
        /// 设置Key过滤规则
        /// </summary>
        /// <returns></returns>
        public override Expression<Func<string,bool>> SetCacheKeyFilter()
        {
            return o => o == GetRedisKey();
        }

        /// <summary>
        /// 获取缓存的Key
        /// </summary>
        /// <returns></returns>
        protected override string GetRedisKey()
        {
            return "DemoOneRedisKey";
        }
    }
    
    /// <summary>
    /// 实现客户端缓存Demo2
    /// </summary>
    public class ClientSideDemoTwoCache : ClienSideCacheBase
    {
        /// <summary>
        /// 构造函数
        /// </summary>
        /// <param name="serviceProvider"></param>
        public ClientSideDemoTwoCache(IServiceProvider serviceProvider) : base(serviceProvider) { }

        /// <summary>
        /// 设置Key过滤规则
        /// </summary>
        /// <returns></returns>
        public override Expression<Func<string,bool>> SetCacheKeyFilter()
        {
            return o => o.StartsWith(GetRedisKey());
        }

        /// <summary>
        /// 获取缓存的Key
        /// </summary>
        /// <returns></returns>
        protected override string GetRedisKey()
        {
            return "DemoTwoRedisKey";
        }
    }

 

2. FreeRedisService的实现

其中关键代码就是一次性设置好项目中所有本地缓存的过滤条件,FreeRedisService最终会注册为一个单例

    public class FreeRedisService
    {
        /// <summary>
        /// RedisClient
        /// </summary>
        private static RedisClient _redisClient;

        /// <summary>
        /// 初始化配置
        /// </summary>
        private FreeRedisOption _redisOption;

        /// <summary>
        /// 构造函数
        /// </summary>
        public FreeRedisService(FreeRedisOption redisOption)
        {
            if (redisOption == null) {
                throw new NullReferenceException("初始化配置为空");
            }
            _redisOption = redisOption;
            InitRedisClient();
        }

        /// <summary>
        /// 懒加载Redis客户端
        /// </summary>
        private readonly static Lazy<RedisClient> redisClientLazy = new Lazy<RedisClient>(() => {
            var r = _redisClient;
            r.Serialize = obj => JsonConvert.SerializeObject(obj);
            r.Deserialize = (json,type) => JsonConvert.DeserializeObject(json,type);
            r.Notice += (s,e) => Console.WriteLine(e.Log);
            return r;
        });

        private static readonly object obj = new object();

        /// <summary>
        /// 初始化Redis
        /// </summary>
        /// <returns></returns>
        bool InitRedisClient()
        {
            if (_redisClient == null) {
                lock (obj) {
                    if (_redisClient == null) {
                        _redisClient = new RedisClient($"{_redisOption.RedisHost}:{_redisOption.RedisPort},password={_redisOption.RedisPassword},defaultDatabase={_redisOption.DefaultIndex},poolsize={_redisOption.Poolsize},ssl=false,writeBuffer=10240,prefix={_redisOption.Prefix},asyncPipeline={_redisOption.asyncPipeline},connectTimeout={_redisOption.ConnectTimeout},abortConnect=false");
                        //设置客户端缓存
                        if (_redisOption.UseClientSideCache) {
                            if (_redisOption.ClientSideCacheKeyFilter == null) {
                                throw new NullReferenceException("如果开启客户端缓存,必须设置客户端缓存Key过滤条件");
                            }
                            _redisClient.UseClientSideCaching(new ClientSideCachingOptions() {
                                Capacity = 0,  //本地缓存的容量,0不限制
                                KeyFilter = _redisOption.ClientSideCacheKeyFilter,  //过滤哪些键能被本地缓存
                                CheckExpired = (key,dt) => DateTime.Now.Subtract(dt) > TimeSpan.FromSeconds(3)  //检查长期未使用的缓存
                            });
                        }
                        return true;
                    }
                }
            }
            return _redisClient != null;
        }

        /// <summary>
        /// 获取Client实例
        /// </summary>
        public RedisClient Instance {
            get {
                if (InitRedisClient()) {
                    return redisClientLazy.Value;
                }
                throw new NullReferenceException("Redis不可用");
            }
        }
    }

 

3. 反射遍历获取所有过滤条件

我们写一个反射的方法,去遍历所有的缓存派生类,并调用其中重写过的过滤条件抽象方法,最后合并为一个表达式树,Or这个方法是一个自定义扩展方法,具体看Github完整项目

    /// <summary>
    /// 构建Redis客户端缓存Key条件
    /// </summary>
    public class ClientSideCacheKeyBuilder
    {
        /// <summary>
        /// 具体缓存业务实现所在项目程序集
        /// </summary>
        const string DefaultDllName = "Hy.Components.Api";

        /// <summary>
        /// 构建表达式树
        /// </summary>
        /// <param name="serviceProvider">serviceProvider</param>
        /// <param name="dllName">当前类所在的项目dll名</param>
        /// <returns></returns>
        public static Func<string,bool> Build(IServiceProvider serviceProvider,string dllName = DefaultDllName)
        {
            Expression<Func<string,bool>> expression = o => false; //默认false
            var baseClass = typeof(ClienSideCacheBase);
            Assembly ass = Assembly.LoadFrom($"{AppDomain.CurrentDomain.BaseDirectory}{dllName}.dll");
            Type[] types = ass.GetTypes();
            foreach (Type item in types) {
                if (item.IsInterface || item.IsEnum || item.GetCustomAttribute(typeof(ObsoleteAttribute)) != null) {
                    continue;
                }
                //判读基类
                if (item != null && item.BaseType == baseClass) {
                    var instance = (ClienSideCacheBase)Activator.CreateInstance(item,serviceProvider); //这里参数带入IServiceProvider纯粹为了创建实例不报错
                    var expr = instance.SetCacheKeyFilter();
                    expression = expression.Or(expr); //合并树
                }
            }
            return expression.Compile();
        }
    }

 

4. 将FreeRedis服务在IOC容器中注入

我们在项目启动时,调用上面的Build方法,将返回的Func委托传入到FreeRedisService中即可,这里我是写了一个IServiceCollection的扩展方法

    public static class ServiceCollectionExtensions
    {
        /// <summary>
        /// ServiceInject
        /// </summary>
        /// <param name="services"></param>
        public static void AddRedisService(this IServiceCollection services,IConfiguration configuration)
        {
            var clientCacheKeyFilter = ClientSideCacheKeyBuilder.Build(services.BuildServiceProvider()); //构造过滤条件
            var option = GetRedisOption(configuration,clientCacheKeyFilter); //组装Redis初始配置
            services.AddSingleton(c => new FreeRedisService(option)); //FreeRedis注入为单例
        }

        /// <summary>
        /// 获取配置
        /// </summary>
        /// <param name="configuration"></param>
        /// <param name="clientSideCacheKeyFilter"></param>
        /// <returns></returns>
        static FreeRedisOption GetRedisOption(IConfiguration configuration,Func<string,bool> clientSideCacheKeyFilter = null)
        {
            return new FreeRedisOption() {
                RedisHost = configuration.GetSection("Redis:RedisHost").Value,
                RedisPassword = configuration.GetSection("Redis:RedisPassword").Value,
                RedisPort = Convert.ToInt32(configuration.GetSection("Redis:RedisPort").Value),
                SyncTimeout = 5000,
                ConnectTimeout = 15000,
                DefaultIndex = 0,
                Poolsize = 5,
                UseClientSideCache = clientSideCacheKeyFilter != null,
                ClientSideCacheKeyFilter = clientSideCacheKeyFilter
            };
        }
    }

在项目IOC容器中注入,以下为.Net6的Program模板

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

builder.Services.AddHealthChecks();

//注入Redis服务
builder.Services.AddRedisService(builder.Configuration);

//可选:注入客户端缓存具体实现类。 如果实现有很多,这里会有一大堆注入代码。在代码中直接实例化类并传入IServiceProvider也一样的
builder.Services.AddSingleton<ClientSideDemoOneCache>();
builder.Services.AddSingleton<ClientSideDemoTwoCache>();

//构建WebApplication
var app = builder.Build();

app.UseAuthorization();

app.MapControllers();

app.UseHealthChecks("/health");

app.Run();

 

5. 最后看下我们在业务代码中的具体用法

其中的ClientSideDemoOneCache这个实例,我们可以通过直接实例化并传入IServiceProvider的方式使用,也可以通过构造函数注入,前提是在上面IOC容器中注入过了

    [ApiController]
    [Route("[controller]")]
    public class HomeController : ControllerBase
    {
        private readonly ILogger<HomeController> _logger;
        private readonly IServiceProvider _serviceProvider;
        private readonly ClientSideDemoOneCache _clientSideDemoOneCache;

        public HomeController(ILogger<HomeController> logger,IServiceProvider serviceProvider,ClientSideDemoOneCache clientSideDemoOneCache)
        {
            _logger = logger;
            _serviceProvider = serviceProvider;
            _clientSideDemoOneCache = clientSideDemoOneCache;
        }

        #region 可通过启动不同端口的Api,分别调用以下接口对同一个Key进行操作,测试客户端缓存是否生效以及是否及时同步

        /// <summary>
        /// 测试get
        /// </summary>
        /// <returns></returns>
        [HttpGet, Route("getvalue")]
        public string TestGetValue()
        {
            ClientSideDemoOneCache cacheOne = new ClientSideDemoOneCache(_serviceProvider);
            //cacheOne = _clientSideDemoOneCache; //通过容器拿到实例
            var value = cacheOne.Get<string>();
            return value ?? "缓存空了";
        }

        /// <summary>
        /// 测试set
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        [HttpGet, Route("setvalue")]
        public string TestSetValue([FromQuery] string value)
        {
            ClientSideDemoOneCache cacheOne = new ClientSideDemoOneCache(_serviceProvider);
            cacheOne.Set(value);
            return "OK";
        }

        /// <summary>
        /// 测试del
        /// </summary>
        /// <returns></returns>
        [HttpGet, Route("delvalue")]
        public string TestDelValue()
        {
            ClientSideDemoOneCache cacheOne = new ClientSideDemoOneCache(_serviceProvider);
            cacheOne.Remove();
            return "OK";
        }

        #endregion
    }

 

6. 单机测试

1. 启动项目看一下,先设置一个值,可以看到在Redis中已经添加成功

 

Redis客户端:

 

2. 再获取一下值,成功拿到

 

3. 再次刷新一下,我们看下打印出来的日志,可以发现第一次是从服务端取值,第二次显示从本地取值,说明过滤条件已经生效了

 

 7. 在本机开启两个Api服务,模拟分布式测试

1. 通过2个不同的端口启动两个Api服务,可以看到目前拿到都是同一个值

2. 我们通过其中一个服务修改一下值,发现另外一边马上就变化了

3. 再次刷新一下getvalue接口,看下日志,发现第一次的值222222是从服务端获取,第二次又是从本地获取了

4. 接着我们再通过其中一个服务,删掉这个Key,发现另一边马上就获取不到值了

 

以上的完整代码已经放到Github上查看完整代码

 

原创作者:Harry

原文出处:https://www.cnblogs.com/simendancer/articles/17052784.html

有关.NetCore下基于FreeRedis实现的Redis6.0客户端缓存之缓存键条件优雅过滤的更多相关文章

  1. ruby - 如何在 Ubuntu 中清除 Ruby Phusion Passenger 的缓存? - 2

    我试过重新启动apache,缓存的页面仍然出现,所以一定有一个文件夹在某个地方。我没有“公共(public)/缓存”,那么我还应该查看哪些其他地方?是否有一个URL标志也可以触发此效果? 最佳答案 您需要触摸一个文件才能清除phusion,例如:touch/webapps/mycook/tmp/restart.txt参见docs 关于ruby-如何在Ubuntu中清除RubyPhusionPassenger的缓存?,我们在StackOverflow上找到一个类似的问题:

  2. ruby-on-rails - Ruby on Rails 计数器缓存错误 - 2

    尝试在我的RoR应用程序中实现计数器缓存列时出现错误Unknownkey(s):counter_cache。我在这个问题中实现了模型关联:Modelassociationquestion这是我的迁移:classAddVideoVotesCountToVideos0Video.reset_column_informationVideo.find(:all).eachdo|p|p.update_attributes:videos_votes_count,p.video_votes.lengthendenddefself.downremove_column:videos,:video_vot

  3. 叮咚买菜基于 Apache Doris 统一 OLAP 引擎的应用实践 - 2

    导读:随着叮咚买菜业务的发展,不同的业务场景对数据分析提出了不同的需求,他们希望引入一款实时OLAP数据库,构建一个灵活的多维实时查询和分析的平台,统一数据的接入和查询方案,解决各业务线对数据高效实时查询和精细化运营的需求。经过调研选型,最终引入ApacheDoris作为最终的OLAP分析引擎,Doris作为核心的OLAP引擎支持复杂地分析操作、提供多维的数据视图,在叮咚买菜数十个业务场景中广泛应用。作者|叮咚买菜资深数据工程师韩青叮咚买菜创立于2017年5月,是一家专注美好食物的创业公司。叮咚买菜专注吃的事业,为满足更多人“想吃什么”而努力,通过美好食材的供应、美好滋味的开发以及美食品牌的孵

  4. 基于C#实现简易绘图工具【100010177】 - 2

    C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.

  5. kvm虚拟机安装centos7基于ubuntu20.04系统 - 2

    需求:要创建虚拟机,就需要给他提供一个虚拟的磁盘,我们就在/opt目录下创建一个10G大小的raw格式的虚拟磁盘CentOS-7-x86_64.raw命令格式:qemu-imgcreate-f磁盘格式磁盘名称磁盘大小qemu-imgcreate-f磁盘格式-o?1.创建磁盘qemu-imgcreate-fraw/opt/CentOS-7-x86_64.raw10G执行效果#ls/opt/CentOS-7-x86_64.raw2.安装虚拟机使用virt-install命令,基于我们提供的系统镜像和虚拟磁盘来创建一个虚拟机,另外在创建虚拟机之前,提前打开vnc客户端,在创建虚拟机的时候,通过vnc

  6. ruby-on-rails - Rails 优雅地处理超时 session ? - 2

    使用rails4,ruby2。我在rails配置中为我的cookiesession设置了30分钟的超时时间。问题是,如果我转到表单,让session超时,然后提交表单,我会收到此ActionController::InvalidAuthenticityToken错误。如何在Rails中优雅地处理这个错误?比如说,重定向到登录屏幕? 最佳答案 在您的ApplicationController:rescue_fromActionController::InvalidAuthenticityTokendoredirect_tosome_p

  7. ruby - 获取数组中的值并最小化某个类属性的最优雅的方法是什么? - 2

    假设我有以下类(class):classPersondefinitialize(name,age)@name=name@age=ageenddefget_agereturn@ageendend我有一组Person对象。是否有一种简洁的、类似于Ruby的方法来获取最小(或最大)年龄的人?如何根据它对它们进行排序? 最佳答案 这样做会:people_array.min_by(&:get_age)people_array.max_by(&:get_age)people_array.sort_by(&:get_age)

  8. ruby-on-rails - 优雅的 Rails : multiple routes, 相同的 Controller Action - 2

    让多条路线去同一条路的最优雅的方式是什么ControllerAction?我有:get'dashboard',to:'dashboard#index'get'dashboard/pending',to:'dashboard#index'get'dashboard/live',to:'dashboard#index'get'dashboard/sold',to:'dashboard#index'这很丑陋。有什么“更优雅”的建议吗?一个类轮的奖励积分。 最佳答案 为什么不只有一个路由和一个Controller操作,并根据传递给它的参数来

  9. ruby - 鸭子输入字符串、符号和数组的优雅方式? - 2

    这是针对我无法破坏的现有公共(public)API,但我确实希望对其进行扩展。目前,该方法采用字符串或符号或任何其他在作为第一个参数传递给send时有意义的内容我想添加发送字符串、符号等列表的功能。我可以只使用is_a吗?数组,但还有其他发送列表的方法,这不是很像ruby​​。我将调用列表中的map,所以第一个倾向是使用respond_to?:map。但是字符串也会响应:map,所以这行不通。 最佳答案 如何将它们全部视为数组?String的行为与仅包含String的Array相同:deffoo(obj,arg)[*arg].eac

  10. ruby-on-rails - (Ruby,Rails) 基于角色的身份验证和用户管理...? - 2

    我正在寻找用于Rails的优质管理插件。似乎大多数现有的插件/gem(例如“restful_authentication”、“acts_as_authenticated”)都围绕着self注册等展开。但是,我正在寻找一种功能齐全的基于管理/管理角色的解决方案——但不是简单地附加到另一个非基于角色的解决方案。如果我找不到,我想我会自己动手......只是不想重新发明轮子。 最佳答案 RyanBates最近做了两个关于授权的railscast(注意身份验证和授权之间的区别;身份验证检查用户是否如她所说的那样,授权检查用户是否有权访问资源

随机推荐