2024年10月

一、项目简介

使用过ABP框架的童鞋应该知道它也自带了一款免费的Blazor UI主题,它的页面是长这样的:
image

个人感觉不太美观,于是网上搜了很多Blazor开源组件库,发现有一款样式非常不错的组件库,名叫:Radzen,它的组件库案例网址是:
Sample Blazor Dashboard | Free UI Components by Radzen
,比较符合我的审美,于是使用它开发了基于ABP框架的UI主题,项目名称叫Abp.RadzenUI,已在Github上开源:
GitHub - ShaoHans/Abp.RadzenUI: Abp RadzenUI is a UI theme built on the Abp framework and developed using the Radzen Blazor component
,欢迎大家star。已经提供了基本的功能:登录(支持多租户)、角色管理、用户管理、权限分配、租户管理、多语言切换、免费主题样式切换、侧边栏菜单等;

二、UI展示

1.登录页面,支持多租户的切换登录

image

2.用户列表

image

3.权限分配

image

4.支持多语言切换

image

5.支持多主题切换

image

要体验更多的功能,你可以下载本项目到本地亲自体验

三、如何使用

  1. 使用ABP CLI工具创建一个新的Abp Blazor Server应用,例如项目名称叫CRM
    abp new CRM -u blazor-server -dbms PostgreSQL -m none --theme leptonx-lite -csf
  2. 在CRM.Blazor项目安装AbpRadzen.Blazor.Server.UI包
    dotnet add package AbpRadzen.Blazor.Server.UI
  3. 移除CRM.Blazor项目中与leptonx-lite主题相关的nuget包和代码
    主要是 CRMBlazorModule 类中的代码需要精简,可以参考示例项目中的CRMBlazorWebModule.cs文件代码,你可以直接将它的代码覆盖你的代码;
    然后删除Pages目录中自带的razor页面文件。
  4. 对 Abp RadzenUI 进行配置
    将 ConfigureAbpRadzenUI 方法添加到ConfigService方法中
private void ConfigureAbpRadzenUI()
{
    // Configure AbpRadzenUI
    Configure<AbpRadzenUIOptions>(options =>
    {
        // 这句代码很重要,它会将你在Blazor Web项目中新建的razor页面组件添加到Router中,这样就可以访问到了
        options.RouterAdditionalAssemblies = [typeof(Home).Assembly];

        // 配置页面标题栏
        //options.TitleBar = new TitleBarSettings
        //{
        //    ShowLanguageMenu = false, // 是否显示多语言按钮菜单
        //    Title = "CRM" // 标题栏名称:一般是系统名称
        //};
        //options.LoginPage = new LoginPageSettings
        //{
        //    LogoPath = "xxx/xx.png" // 登录页面的logo图片
        //};
        //options.Theme = new ThemeSettings
        //{
        //    Default = "material",
        //    EnablePremiumTheme = true,
        //};
    });

    // 多租户配置, 这个会影响到登录页面是否展示租户信息
    Configure<AbpMultiTenancyOptions>(options =>
    {
        options.IsEnabled = MultiTenancyConsts.IsEnabled;
    });

    // Configure AbpLocalizationOptions
    Configure<AbpLocalizationOptions>(options =>
    {
        // 配置多语言资源,需要继承AbpRadzenUIResource,它包含了需要用到的多语言信息
        var crmResource = options.Resources.Get<CRMResource>();
        crmResource.AddBaseTypes(typeof(AbpRadzenUIResource));

        // 配置多语言菜单中显示的语言
        options.Languages.Clear();
        options.Languages.Add(new LanguageInfo("en", "en", "English"));
        options.Languages.Add(new LanguageInfo("fr", "fr", "Français"));
        options.Languages.Add(new LanguageInfo("zh-Hans", "zh-Hans", "简体中文"));
    });

    // 配置侧边栏菜单
    Configure<AbpNavigationOptions>(options =>
    {
        options.MenuContributors.Add(new CRMMenuContributor());
    });
}

最后在OnApplicationInitialization方法的最后添加以下代码,使用RadzenUI

app.UseRadzenUI();

关于更多的配置可以参考本项目的示例代码:
Abp.RadzenUI/samples/CRM.Blazor.Web/CRMBlazorWebModule.cs at main · ShaoHans/Abp.RadzenUI · GitHub
5. 配置侧边栏菜单
当你添加了新的razor页面组件后,需要在CRMMenuContributor类文件中进行配置,这样它就会显示在页面的侧边栏菜单中

四、添加自己的页面

比如你现在要做一个商品管理的增删改查功能,你只要定义一个IProductAppService接口并继承ABP的ICrudAppService接口:
public interface IProductAppService : ICrudAppService<ProductDto, Guid, GetProductsInput, CreateProductDto, UpdateProductDto> { }
然后实现IProductAppService接口:

public class ProductAppService
    : CrudAppService<
        Product,
        ProductDto,
        Guid,
        GetProductsInput,
        CreateProductDto,
        UpdateProductDto
    >,
        IProductAppService{}

一个简单的增删改查业务代码就搞定了,而且接口带了权限验证,完全不用写那么多代码,当然一些其他业务逻辑也可以通过override的方式去实现。
接下来就是增加产品的列表页面,razor页面需要继承下面这个组件:
@inherits AbpCrudPageBase<IProductAppService, ProductDto, Guid, GetProductsInput, CreateProductDto, UpdateProductDto>
这个组件将CRUD的代码都实现了,你只需要编写DataGrid显示列的代码,以及创建产品、编辑产品弹框的代码,强烈建议你把项目代码下载下来学习一下,实现一个后台管理系统真的太简单了。

五、RadzenDataGrid的过滤功能介绍

列表页面都有下面类似的筛选功能:
image

RadzenDataGrid组件也支持这种筛选,它会把所有列头的筛选条件最后组装成一个过滤字符串,放到了LoadDataArgs类的Filter参数中,这个过滤字符串类似这样:
(Name == null ? "" : Name).Contains("App") and StockCount < 10000 and Status = 0
你的查询接口只需要定义一个Filter属性接受这个字符串,通过这个字符串就能查到数据,当然这得归功于强大的工具包:
Microsoft.EntityFrameworkCore.DynamicLinq
,感兴趣的可以去查阅资料学习一下。

protected override async Task<IQueryable<Product>> CreateFilteredQueryAsync(
    GetProductsInput input
)
{
    var query = await base.CreateFilteredQueryAsync(input);

    /*
     在 CRM.EntityFrameworkCore 项目上安装包: Microsoft.EntityFrameworkCore.DynamicLinq
     然后引用命名空间 : using System.Linq.Dynamic.Core;
     Dynamic LINQ会自动将过滤字符串转成动态查询表达式
     */
    if (!string.IsNullOrEmpty(input.Filter))
    {
        query = query.Where(input.Filter);
    }

    return query;
}

六、总结

以上就是对我这个开源项目(
https://github.com/ShaoHans/Abp.RadzenUI
)简单介绍,如果你熟悉ABP且希望使用它开发一个后台管理系统,不妨一试,有什么问题欢迎大家提issue。

  1. 如何进行线上debug。
  2. 如何在gateway自定义路由规则去进行请求分发,让请求打到集群模式下我们想要的节点。

drawing

1.配置remote debug

1.在启动参数配置参数:

-Xdebug
-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=6364

drawing

2.新建remote
drawing

3.启动remote

2.gateway改造

我个人的需求是把某个用户的请求,打到我想要的节点(开启了debug模式的节点),避免因为集群节点的负载导致请求打到其他节点,也避免影响其他用户的正常请求流程。

我的做法是拦截请求投中的Authorization=xxx进行判断,因此参考了gateway的路由策略:

gateway 路由匹配策略

而gateway自带的路由策略是通过regix(正则匹配)来实现的,因此我做了以下改造:

1.新增HeaderValueRoutePredicateFactory

public class HeaderValueRoutePredicateFactory extends AbstractRoutePredicateFactory<HeaderValueRoutePredicateFactory.Config> {

    private static final String KEY_1 = "headerName";
    private static final String KEY_2 = "headerValue";

    public HeaderValueRoutePredicateFactory() {
        super(Config.class);
    }

    @Override
    public List<String> shortcutFieldOrder() {
        return Arrays.asList(KEY_1, KEY_2);
    }

    @Override
    public Predicate<ServerWebExchange> apply(Config config) {
        return new GatewayPredicate() {
            @Override
            public boolean test(ServerWebExchange exchange) {
                List<String> values = exchange.getRequest().getHeaders().getOrDefault(config.getHeaderName(),
                        Collections.emptyList());
                if (values.isEmpty()) {
                    return false;
                }

                boolean match = values.stream().allMatch(item -> item.equals(config.getHeaderValue()));
                if (match) {
                    log.info("debug模式route策略已触发");
                }

                return match;
            }

            @Override
            public Object getConfig() {
                return config;
            }

            @Override
            public String toString() {
                return String.format("HeaderName: %s headerValue=%s", config.getHeaderName(), config.getHeaderValue());
            }
        };
    }

    public static class Config {

        private String headerName;

        private String headerValue;

        public String getHeaderName() {
            return headerName;
        }

        public Config setHeaderName(String headerName) {
            this.headerName = headerName;
            return this;
        }

        public String getHeaderValue() {
            return headerValue;
        }

        public Config setHeaderValue(String headerValue) {
            this.headerValue = headerValue;
            return this;
        }
    }

}

代码笔记:

  • 获取系统缓存路由策略(可以看网关中配置的全部策略)
    org.springframework.cloud.gateway.route.CachingRouteLocator#getRoutes
  • 获取路由策略(根据请求条件匹配)
    org.springframework.cloud.gateway.handler.RoutePredicateHandlerMapping#getHandlerInternal
  • 获取路由策略(根据请求条件匹配)
    org.springframework.cloud.gateway.handler.RoutePredicateHandlerMapping#lookupRoute


2.配置类

@Configuration
public class Config {

    @Bean
    public HeaderValueRoutePredicateFactory headerValueRoutePredicateFactory() {
        return new HeaderValueRoutePredicateFactory();
    }

}

3.修改gateway配置

spring:
  cloud:
    gateway:
      routes:
        # 当请求头中带有Authorization=ff4a4ce5-5276-4263-b817-34d1ce553421切路径为/ims/**会被该规则路由
        - id: ims
          uri: lb://ims
          predicates:
            - Path=/ims/**
          filters:
            - StripPrefix=1
        # 当请求头中带有Authorization=ff4a4ce5-5276-4263-b817-34d1ce553421切路径为/ims/**会被该规则路由
        - id: ims-debug
          uri: lb://ims-debug
          # 配置-1是为了让该路由策略在id: ims 前面进行判断,否则会触发id=ims的路由策略,不会触发id=ims-debug的路由策略
          order: -1
          predicates:
            - Path=/ims/**
            - HeaderValue=Authorization,ff4a4ce5-5276-4263-b817-34d1ce553421
          filters:
            - StripPrefix=1

3.nacos配置

1.正常的节点配置

-Dspring.application.name=ims

2.debug的节点配置

-Dspring.application.name=ims-debug

本质上ims和ims-debug是同一服务,只是服务名不同,区分开是为了方便路由

4.其他问题

  1. 正常情况下,如果是生产环境,ims-debug服务即使开启了debug模式,但会因为网络问题导致办公网的本地无法进行连接。这种情况我们可以让运维申请跳板机、VPN来对接专用网络,另外debug的对外端口也要申请好。
  2. 我在网上还看到其他的线上debug方式
    点击跳转他人链接
    ,因此我这种方式未必适合所有人(但是都没有考虑集群下请求如果不进入当前节点的问题),本人只是折中。
  3. 即使你学会了也没什么用,正常情况下不会让开发者进行线上debug。作者本人也只是心血来潮。

浅析Redis

什么是Redis

Redis本质上是一个Key-Value类型的内存数据库,整个数据库加载在内存当中操作,定期通过异步操作把数据库中的数据flush到硬盘上进行保存。

因为是纯内存操作,Redis的性能非常出色,每秒可以处理超过 10万次读写操作,是已知性能最快的Key-Value 数据库。

Redis底层

Redis的底层请见
https://www.bozhu12.cc/backend/redis2/#_1-前言
这篇文章 讲的非常详细

Redis的线程模型

redis 内部使用
文件事件处理器 file event handler
,它是单线程的,所以redis才叫做单线程模型。它采用
IO多路复用机制
同时监听多个 socket,将产生事件的 socket 压入内存队列中,事件分派器根据 socket 上的事件类型来选择对应的事件处理器进行处理。

文件事件处理器的结构:

  • 多个 socket
  • IO 多路复用程序
  • 文件事件分派器
  • 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)

1725246285647

1725248167657

  1. Redis 启动初始化的时候,Redis 会将连接应答处理器与 AE_READABLE 事件关联起来。
  2. 如果一个客户端跟 Redis 发起连接,此时 Redis 会产生一个 AE_READABLE 事件,由于开始之初 AE_READABLE 是与连接应答处理器关联,所以由连接应答处理器来处理该事件,这时连接应答处理器会与客户端建立连接,创建客户端响应的 socket,同时将这个 socket 的 AE_READABLE 事件与命令请求处理器关联起来。
  3. 如果这个时间客户端向 Redis 发送一个命令(set k1 v1),这时 socket 会产生一个 AE_READABLE 事件,IO 多路复用程序会将该事件压入队列中,此时事件分派器从队列中取得该事件,由于该 socket 的 AE_READABLE 事件已经和命令请求处理器关联了,因此事件分派器会将该事件交给命令请求处理器处理,命令请求处理器读取事件中的命令并完成。操作完成后,Redis 会将该 socket 的 AE_WRITABLE 事件与命令回复处理器关联。
  4. 如果客户端已经准备好接受数据后,Redis 中的该 socket 会产生一个 AE_WRITABLE 事件,同样会压入队列然后被事件派发器取出交给相对应的命令回复处理器,由该命令回复处理器将准备好的响应数据写入 socket 中,供客户端读取。
  5. 命令回复处理器写完后,就会删除该 socket 的 AE_WRITABLE 事件与命令回复处理器的关联关系。

单线程处理流程

  1. 主线程处理网络 I/O 和命令执行:
    • 在单线程模式下,Redis 的主线程既负责从客户端读取请求,也负责执行命令和发送响应。所有的工作都是按照请求的顺序,依次完成。
    • 主线程会轮询所有的客户端连接,一个一个地处理请求。
  2. 处理客户端 A 的请求:
    • 主线程首先从客户端 A 读取
      SET key1 value1
      请求。
    • 读取完成后,主线程立即解析并执行该命令,将
      key1
      设置为
      value1
    • 然后,主线程将
      OK
      结果发送回客户端 A。
  3. 处理客户端 B 的请求:
    • 接下来,主线程从客户端 B 读取
      GET key1
      请求。
    • 读取完成后,主线程解析并执行该命令,查询
      key1
      的值,得到
      value1
    • 主线程将结果
      value1
      返回给客户端 B。
  4. 处理客户端 C 的请求:
    • 最后,主线程从客户端 C 读取
      SET key2 value2
      请求。
    • 主线程解析并执行该命令,将
      key2
      设置为
      value2
    • 然后将
      OK
      结果返回给客户端 C。

具体步骤解释

  • 步骤 1:网络 I/O 和命令执行的顺序处理
    • Redis 依次轮询客户端 A、B、C 的连接,并从中读取请求数据。在主线程中,网络 I/O 和命令执行都是同步完成的,意味着 Redis 会处理完一个客户端的所有操作,才会继续处理下一个客户端的请求。
  • 步骤 2:命令解析与执行
    • 当主线程读取了一个完整的命令后,它会立即解析命令并执行。例如,主线程从客户端 A 读取
      SET key1 value1
      后,立即将
      key1
      设置为
      value1
      ,并返回
      OK
  • 步骤 3:响应回写
    • 主线程执行完命令后,会立刻将响应结果发送回客户端。例如,客户端 B 请求
      GET key1
      ,主线程查询后,立即将查询结果
      value1
      发送给客户端 B。

多线程机制

1725248255687

客户端请求示例

假设有 3 个客户端同时向 Redis 发送请求:

  1. 客户端 A 发送
    SET key1 value1
  2. 客户端 B 发送
    GET key1
  3. 客户端 C 发送
    SET key2 value2

多线程 I/O 处理流程

  1. 网络 I/O 阶段:
    • Redis 的 4 个 I/O 线程开始工作,每个线程负责从不同客户端接收数据。例如:
      • I/O 线程 1 从客户端 A 读取
        SET key1 value1
        的请求。
      • I/O 线程 2 从客户端 B 读取
        GET key1
        的请求。
      • I/O 线程 3 从客户端 C 读取
        SET key2 value2
        的请求。
  2. 主线程命令解析与执行:
    • 一旦 I/O 线程从客户端接收到完整的请求数据后,它们会将数据传递给 Redis 的主线程。
    • 主线程负责解析命令并执行它们:
      • 首先,主线程处理
        SET key1 value1
        ,将
        key1
        设置为
        value1
      • 然后,主线程处理
        GET key1
        ,读取并返回
        key1
        的值(
        value1
        )。
      • 最后,主线程处理
        SET key2 value2
        ,将
        key2
        设置为
        value2
  3. 网络响应阶段:
    • 命令执行完成后,主线程将结果传递回 I/O 线程:
      • I/O 线程 1 将
        OK
        响应返回给客户端 A。
      • I/O 线程 2 将
        value1
        返回给客户端 B。
      • I/O 线程 3 将
        OK
        返回给客户端 C。

内存淘汰底层原理

1. 淘汰过程

Redis 内存淘汰执行流程如下:

1.每次当 Redis 执行命令时,若设置了最大内存大小 maxmemory,并设置了淘汰策略式,则会尝试进行一次 Key 淘汰;

2.Redis 首先会评估已使用内存(这里不包含主从复制使用的两个缓冲区占用的内存)是否大于 maxmemory,如果没有则直接返回,否则将计算当前需要释放多少内存,随后开始根据策略淘汰符合条件的 Key;当开始进行淘汰时,将会依次对每个数据库进行抽样,抽样的数据范围由策略决定,而样本数量则由 maxmemory-samples配置决定;

3.完成抽样后,Redis 会尝试将样本放入提前初始化好 EvictionPoolLRU 数组中,它相当于一个临时缓冲区,当数组填满以后即将里面全部的 Key 进行删除。

4.若一次删除后内存仍然不足,则再次重复上一步骤,将样本中的剩余 Key 再次填入数组中进行删除,直到释放了足够的内存,或者本次抽样的所有 Key 都被删除完毕(如果此时内存还是不足,那么就重新执行一次淘汰流程)。

在抽样这一步,涉及到从字典中随机抽样这个过程,由于哈希表的 Key 是散列分布的,因此会有很多桶都是空的,纯随机效率可能会很低。因此,Redis 采用了一个特别的做法,那就是先连续遍历数个桶,如果都是空的,再随机调到另一个位置,再连续遍历几个桶……如此循环,直到结束抽样。

你可以参照源码理解这个过程:

unsigned int dictGetSomeKeys(dict *d, dictEntry **des, unsigned int count) {
    unsigned long j; /* internal hash table id, 0 or 1. */
    unsigned long tables; /* 1 or 2 tables? */
    unsigned long stored = 0, maxsizemask;
    unsigned long maxsteps;
​
    if (dictSize(d) < count) count = dictSize(d);
    maxsteps = count*10;
​
    // 如果字典正在迁移,则协助迁移
    for (j = 0; j < count; j++) {
        if (dictIsRehashing(d))
            _dictRehashStep(d);
        else
            break;
    }
​
    tables = dictIsRehashing(d) ? 2 : 1;
    maxsizemask = d->ht[0].sizemask;
    if (tables > 1 && maxsizemask < d->ht[1].sizemask)
        maxsizemask = d->ht[1].sizemask;
​
    unsigned long i = random() & maxsizemask;
    unsigned long emptylen = 0;
​
    // 当已经采集到足够的样本,或者重试已达上限则结束采样
    while(stored < count && maxsteps--) {
        for (j = 0; j < tables; j++) {
            if (tables == 2 && j == 0 && i < (unsigned long) d->rehashidx) {
                if (i >= d->ht[1].size)
                    i = d->rehashidx;
                else
                    continue;
            }
​
            // 如果一个库的到期字典已经处理完毕,则处理下一个库
            if (i >= d->ht[j].size) continue;
            dictEntry *he = d->ht[j].table[i];
​
            // 连续遍历多个桶,如果多个桶都是空的,那么随机跳到另一个位置,然后再重复此步骤           
            if (he == NULL) {
                emptylen++;
                if (emptylen >= 5 && emptylen > count) {
                    i = random() & maxsizemask;
                    emptylen = 0;
                }
            } else {
                emptylen = 0;
                while (he) {
                    *des = he;
                    des++;
                    he = he->next;
                    stored++;
                    if (stored == count) return stored;
                }
            }
        }
​
        // 查找下一个桶
        i = (i+1) & maxsizemask;
    }
    return stored;
}

2. LRU 实现

LRU 的全称为 Least Recently Used,也就是最近最少使用。一般来说,LRU 会从一批 Key 中淘汰上次访问时间最早的 key。

它是一种非常常见的缓存回收算法,在诸如 Guava Cache、Caffeine等缓存库中都提供了类似的实现。我们自己也可以基于 JDK 的 LinkedHashMap 实现支持 LRU 算法的缓存功能。
2.1 近似 LRU
传统的 LRU 算法实现通常会维护一个链表,当访问过某个节点后就将该节点移至链表头部。如此反复后,链表的节点就会按最近一次访问时间排序。当缓存数量到达上限后,我们直接移除尾节点,即可移除最近最少访问的缓存。

不过,对于 Redis 来说,如果每个 Key 添加的时候都需要额外的维护并操作这样一条链表,要额外付出的代价显然是不可接受的,因此 Redis 中的 LRU 是近似 LRU(NearlyLRU)。

当每次访问 Key 时,Redis 会在结构体中记录本次访问时间,而当需要淘汰 Key 时,将会从全部数据中进行抽样,然后再移除样本中上次访问时间最早的 key。

它的特点是:

  • 仅当需要时再抽样,因而不需要维护全量数据组成的链表,这避免了额外内存消耗。

  • 访问时仅在结构体上记录操作时间,而不需要操作链表节点,这避免了额外的性能消耗。

当然,有利就有弊,这种实现方式也决定 Redis 的 LRU 是并不是百分百准确的,被淘汰的 Key 未必真的就是所有 Key 中最后一次访问时间最早的。


2.2 抽样大小
根据上述的内容,我们不难理解,当抽样的数量越大,LRU 淘汰 Key 就越准确,相对的开销也更大。因此,Redis 允许我们通过 maxmemory-samples 配置采样数量(默认为 5),从而在性能和精度上取得平衡。

3. LFU 实现

LFU 全称为 Least Frequently Used ,也就是最近最不常用。它的特点如下:

  • 同样是基于抽样实现的近似算法,maxmemory-samples 对其同样有效。

  • 比较的不是最后一次访问时间,而是数据的访问频率。当淘汰的时候,优先淘汰范围频率最低 Key。

它的实现与 LRU 基本一致,但是在计数部分则有所改进。

3.1 概率计数器
在 Redis 用来存储数据的结构体 redisObj 中,有一个 24 位的 lru数值字段:

  • 当使用 LRU 算法时,它用于记录最后一次访问时间的时间戳。

  • 当使用 LFU 算法时,它被分为两部分,高 16 位关于记录最近一次访问时间(Last Decrement Time),而低 8 位作为记录访问频率计数器(Logistic Counter)。

LFU 的核心就在于低 8 位表示的访问频率计数器(下面我们简称为 counter),是一个介于 0 ~ 255 的特殊数值,它会每次访问 Key 时,基于时间衰减和概率递增机制动态改变。

| 这种基于概率,使用极小内存对大量事件进行计数的计数器被称为莫里斯计数器,它是一种概率计数法的实现。

3.2 时间衰减
每当访问 Key 时,根据当前实际与该 Key 的最后一次访问时间的时间差对 counter 进行衰减。

衰减值取决于 lfu_decay_time 配置,该配置表示一个衰减周期。我们可以简单的认为,每当时间间隔满足一个衰减周期时,就会对 counter 减一。

比如,我们设置 lfu_decay_time为 1 分钟,那么如果 Key 最后一次访问距离现在已有 3 分 30 秒,那么 counter 就需要减 3。

3.3 概率递增
在完成衰减后,Redis 将根据 lfu_log_factor 配置对应概率值对 counter 进行递增。

这里直接放上源码:

/* Logarithmically increment a counter. The greater is the current counter value
 * the less likely is that it gets really implemented. Saturate it at 255. */
uint8_t LFULogIncr(uint8_t counter) {
    // 若已达最大值 255,直接返回
    if (counter == 255) return 255;
    // 获取一个介于 0 到 1 之间的随机值
    double r = (double)rand()/RAND_MAX;
    // 根据当前 counter 减去初始值得到 baseval
    double baseval = counter - LFU_INIT_VAL; 
    if (baseval < 0) baseval = 0;
    // 使用 baseval*server.lfu_log_factor+1 得到一个概率值 p
    double p = 1.0/(baseval*server.lfu_log_factor+1);
    // 当 r < p 时,递增 counter
    if (r < p) counter++;
    return counter;
}

简而言之,直接从代码上理解,我们可以认为 counter和 lfu_log_factor 越大,则递增的概率越小:

当然,实际上也要考虑到访问次数对其的影响,Redis 官方给出了相关数据:

3.4 计数器的初始值
为了防止新的 Key 由于 counter 为 0 导致直接被淘汰,Redis 会默认将 counter设置为 5。

3.5 抽样大小的选择
值得注意的是,当数据量比较大的时候,如果抽样大小设置的过小,因为一次抽样的样本数量有限,冷热数据因为时间衰减导致的权重差异将会变得不明显,此时 LFU 算法的优势就难以体现,即使的相对较热的数据也有可能被频繁“误伤”。

所以,如果你选择了 LFU 算法作为淘汰策略,并且同时又具备比较大的数据量,那么不妨将抽样大小也设置的大一些。

本文介绍如何操作windows系统光标。正常我们设置/隐藏光标,只能改变当前窗体或者控件范围,无法全局操作windows光标。接到一个需求,想隐藏windows全局的鼠标光标显示,下面讲下如何操作

先了解下系统鼠标光标,在鼠标属性-自定义列表中可以看到一共有13种类型,对应13种工作状态:

操作系统提供了一组预定义的光标,如箭头、手形、沙漏等,位于 C:\Windows\Cursors目录下。

对应的Windows.Input.CursorType枚举:

1   public enumCursorType2 {3 None,4 No,5 Arrow,6 AppStarting,7 Cross,8 Help,9 IBeam,10 SizeAll,11 SizeNESW,12 SizeNS,13 SizeNWSE,14 SizeWE,15 UpArrow,16 Wait,17 Hand,18 Pen,19 ScrollNS,20 ScrollWE,21 ScrollAll,22 ScrollN,23 ScrollS,24 ScrollW,25 ScrollE,26 ScrollNW,27 ScrollNE,28 ScrollSW,29 ScrollSE,30 ArrowCD,31   }

光标显示逻辑:

  • 全局光标设置:在桌面或非控件区域,使用默认系统光标。
  • 窗口控件的设置:每个窗口控件可以设置自己的光标类型。当鼠标移动到该控件上时,将自动切换到该设置的光标。如果未设置则显示系统光标
  • 当鼠标移动、点击或执行其他操作时,系统会检测并相应更新光标形状。应用程序也可以改变拖放等操作的光标

对当前鼠标状态有获取需求的,可以通过GetCursorInfo获取,当前鼠标光标id以及句柄:

1     privateIntPtr GetCurrentCursor()2 {3 CURSORINFO cursorInfo;4         cursorInfo.cbSize = Marshal.SizeOf(typeof(CURSORINFO));5         GetCursorInfo(outcursorInfo);6         var cursorId =cursorInfo.hCursor;7         var cursorHandle =CopyIcon(cursorId);8         returncursorHandle;9     }

那如何隐藏系统光标呢?系统光标可以通过
SetSystemCursor function (winuser.h) - Win32 apps | Microsoft Learn
函数设置,不过貌似没有隐藏光标的入口

可以换个思路,创建一个空白光标即可。我做了一个blank.cur:
自己动手制作 windows鼠标光标文件(.cur格式)-CSDN博客

然后隐藏系统光标:

1   private voidHideCursor()2 {3       _cursorHandle =GetCurrentCursor();4       //替换为空白鼠标光标
5       var cursorFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Blank.cur");6       IntPtr cursor =LoadCursorFromFile(cursorFile);7 SetSystemCursor(cursor, OcrNormal);8  }

恢复系统光标的显示,将之前光标Handle设置回去:

1 var success = SetSystemCursor(_cursorHandle, OcrNormal);

以上是实现了当前光标的替换。但上面有介绍过鼠标光标状态有13种,会根据应用程序状态进行切换,所以其它光标也要处理。

对13种光标都替换为空白光标,13种光标CursorId值在
setSystemCursor
文档有说明:

1     private readonly int[] _systemCursorIds = new int[] { 32512, 32513, 32514, 32515, 32516, 32642, 32643, 32644, 32645, 32646, 32648, 32649, 32650};2     private readonly IntPtr[] _previousCursorHandles = new IntPtr[13];3     private voidHideCursor()4 {5         for (int i = 0; i < _systemCursorIds.Length; i++)6 {7             var cursor =LoadCursor(IntPtr.Zero, _systemCursorIds[i]);8             var cursorHandle =CopyIcon(cursor);9             _previousCursorHandles[i] =cursorHandle;10             //替换为空白鼠标光标
11             var cursorFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Blank.cur");12             IntPtr blankCursor =LoadCursorFromFile(cursorFile);13             SetSystemCursor(blankCursor, (uint)_systemCursorIds[i]);14 }15     }

运行验证:
系统桌面、应用窗体如VisualStudio以及网页等光标编辑状态,都成功隐藏

还原光标状态:

1     private voidShowCursor()2 {3         for (int i = 0; i < _systemCursorIds.Length; i++)4 {5             SetSystemCursor(_previousCursorHandles[i], (uint)_systemCursorIds[i]);6 }7     }

用到的User32及参数类:


1    [DllImport("user32.dll", SetLastError = true)]2    public static extern IntPtr LoadCursor(IntPtr hInstance, intlpCursorName);3    [DllImport("user32.dll")]4    public static externIntPtr CopyIcon(IntPtr cusorId);5    [DllImport("user32.dll")]6    public static extern IntPtr LoadCursorFromFile(stringlpFileName);7    [DllImport("user32.dll")]8    public static extern bool SetSystemCursor(IntPtr hcur, uintid);9    [DllImport("user32.dll")]10    static extern bool GetCursorInfo(outCURSORINFO pci);11 
12 [StructLayout(LayoutKind.Sequential)]13    public structPOINT14 {15        publicInt32 x;16        publicInt32 y;17 }18 
19 [StructLayout(LayoutKind.Sequential)]20    public structCURSORINFO21 {22        public Int32 cbSize;        //Specifies the size, in bytes, of the structure.23                                    //The caller must set this to Marshal.SizeOf(typeof(CURSORINFO)).
24        public Int32 flags;         //Specifies the cursor state. This parameter can be one of the following values:25                                    //0             The cursor is hidden.26                                    //CURSOR_SHOWING    The cursor is showing.
27        public IntPtr hCursor;          //Handle to the cursor.
28        public POINT ptScreenPos;       //A POINT structure that receives the screen coordinates of the cursor.
29    }

View Code

需要说明的是,系统光标修改请谨慎处理,光标修改后人工操作不太容易恢复,对应用程序退出、崩溃等情况做好光标恢复操作。

以上demo代码见:
kybs00/HideSystemCursorDemo: 隐藏windows系统光标 (github.com)

参考资料:

AllAPI.net - Your #1 source for using API-functions in Visual Basic! (mentalis.org)

createCursor 函数 (winuser.h) - Win32 apps | Microsoft Learn

SetSystemCursor function (winuser.h) - Win32 apps | Microsoft Learn

关键字:自定义光标、隐藏/显示光标、windows系统光标显示

参观和调研内蒙包头工业园区有感


有幸参加
包头市科学技术局
举办的走进工业园区活动,活动的主要宗旨:深入实施创新驱动发展战略,以创新为关键引擎塑造城市发展新动能,引导企业关键核心技术研发和产品开发,促进科技成果转化,促成企业间技术交流及达成合作。

(1)第一个走访的企业是光威碳纤,用一句话形容这个企业:
把一个做渔竿的企业做成了军工企业,解决了“碳纤维”卡脖子的问题
。能够把一件事情做好、做到极致,投入研发、勇于创新,这样的企业是值得我们尊重的。

(2)第二个走访的企业是明拓集团,主要是
通过冶炼炉生产不绣钢产品
,属于传统高耗能生产制造企业,也在
逐步实现生产智能化、清洁化
,主要目标是节省成本和对环境的污染,但是
受限于一部分原料要从国外进口和现在的市场形势不好
,企业生产经营情况略差于往年。

(3)科技局、园区管委会和各企业座谈,我是第三个发言,主要介绍了:我们的企业(
东方国信、智冶互联和包头北科亿力
)情况及实力、已经在园区稀土和电石炉企业落地项目的情况及效果、已经在包头本地包钢和二机落地项目的情况及效果,同时也给活动组织方科技局
提出活动建议

乙方企业以实际案例给园区企业进行培训,主要方面包括自动化、信息化、智能化、数据治理等方面
。实现共同提升,双方充分了解之后,才有可能达成合作。事后也接受了包头电视台的采访。

为什么提出这个活动建议呢?第一、我们在该园区有两个项目已经落地了,很熟悉当地的情况;第二、通过这次活动也暴露了一些问题,比如在与企业交流的过程中,我问了和生产相关的一些问题,用户的回答是:
在上生产设备的时候都已经上了
。要用发展的眼光看企业现实情况。所以,一是通过这个平台,企业间还有很大的充分交流的空间;二是企业业务和信息化、智能化相关技术融合不充分。基于以上的情况才提出来这个建议。

包头科技局举办这次活动确实很有意义,活动很充实,能够感受到政府组织搭建平台促成企业间的合作愿意。通过不断丰富活动内容,共建开放和包容生态,
相信在包头市场能够大有作为

广告:欢迎来包头发展及置业
!!!

其他照片:


物联网&大数据技术 QQ群:54256083

物联网&大数据项目 QQ群:727664080

QQ:504547114

微信:wxzz0151

博客:https://www.cnblogs.com/lsjwq

微信公众号:iNeuOS