2023年4月

一、创建ASP.NET Core Web API项目(若项目已创建,则可跳过本节内容)

1、双击打开VS2022。

2、单击“创建新项目”,如下图。

3、选择“ASP.NET Core Web API”类型,然后单击“下一步”,如下图。

4、“项目名称”此处填写为“AllTestDemo”;“位置”此处放在E盘根目录;“解决方案名称”此处默认与“项目名称”保持一致;不勾选“将解决方案和项目放在同一目录中”选择框。然后单击“下一步”,如下图。

5、“框架”此处选择“.NET 5.0”;“身份验证类型”此处选择“无”;勾选“配置HTTPS”选择框;不勾选“启用Docker”选择框;不勾选“启用OpenAPI支持”选择框。然后单击“创建”,如下图。

6、此时就可以得到一份ASP.NET Core Web API框架的项目,如下图。

二、引用NuGet包

1、在(上述)项目中鼠标右键单击“依赖项”,在弹出的菜单中单击“管理NuGet程序包”,如下图。

2、此时在出现的界面中选择“浏览”选项卡,并在搜索框中输入“Swashbuckle.AspNetCore”,在搜索结果中选择如下图中所框选的选项,然后单击右侧的“安装”,如下图。

3、安装完成后可以在“依赖项”中找到刚刚安装的依赖包,如下图。

三、配置服务

1、打开Startup.cs文件,在Startup类的ConfigureServices方法中如下图所示的地方添加如下所示的代码段。

            #region Swagger
            services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("DocV1", new OpenApiInfo
                {
                    Version = "v0.1.0",
                    Title = "ZOHC测试文档",
                    Description = "一个Swagger教程文档",
                    Contact = new OpenApiContact
                    {
                        Name = "张欧昊辰",
                        Email = "izohc@foxmail.com"

                    }
                });
            });
            #endregion

2、再次打开Startup.cs文件,在Startup类的Configure方法中如下图所示的地方添加如下所示的代码段。

            #region Swagger
            app.UseSwagger();
            app.UseSwaggerUI(c =>
            {
                c.SwaggerEndpoint("/swagger/DocV1/swagger.json", "DocV1");
            });
            #endregion

四、查看效果

按F5启动项目,运行成功后,修改浏览器中默认地址
https://localhost:44390/weatherforecast

https://localhost:44390/swagger
并按回车键,便可以看到Swagger界面了,如下图。

-------------------------------本篇文章到此结束-------------------------------------

引言

在我第一次写博客的时候,写的第一篇文章,就是关于表达式树的,链接:
https://www.cnblogs.com/1996-Chinese-Chen/p/14987967.html
,其中,当时一直没有研究Expression.Dynamic的使用方法(因为网上找不到资料),就了解到是程序运行时动态去构建表达式树,举个例子,例如我们需要在我们的查询条件中去构建他是等于或者不等于,这个时候,虽然我们可以定义等于或者不定于 的BinaryExpression,然后在代码中通过switch去进行判断,使用的是Equal还是NotEqual,这中间还需要我们自己去写一个switch,如果使用了Dynamic的方法,我们就只需要找到对应的ExpressionType然后传入创建Binder的方法中,在调用Dynamic方法就可以动态的实现,各种判断操作,或者其他的调用方法,灵活度比switch更高,接下来,我们就看看如何使用Expression.Dynamic方法来实现各种操作吧,一下所有代码操作需要引入Microsoft.CSharp.RuntimeBinder,nuget搜索Microsoft.CSharp即可。方便测试,我新建了一个Test的类,下面会用到

public classTest
{
private List<string> Strings = new List<string>();public eventAction TestEvent;public Test(int a,intb)
{
A
=a;
B
=b;
Strings.Add(
"1");
Strings.Add(
"2");
Strings.Add(
"3");
}
public string this[int Index] { get=> Strings[Index]; set=> Strings[Index]=value; }public int A { get; set; }public int B { get; set; }public intAdd()
{
return A+B;
}
public static intAddOne()
{
return 15;
}
}

二元运算

下面的代码实现一个二元运算,首先Dynamic方法是需要CallBinder参数的,而对应的实现有如下的Binder,我们首先需要去创建对应的Binder,二元运算就使用BinaryOperation方法创建,CSharpBinderFlags是一个枚举类型,它用于指定动态绑定操作的行为,里面可以定义在动态绑定的时候需要执行的一些特殊操作,例如,运算应该在已经检查的上下文中运行,或者使用Invoke等需要使用的一些特殊操作,或者转换的时候等等。第二个参数是ExpressionType,标明我们是那一个二元运算,第三个是当前代码运行的主体类型 that indicates where this operation is used.即这个指示了这个操作被用在哪些地方。第三个是一个CSharpArgumentInfo集合,是我们创建这个站点的时候需要使用的参数数量,如果是调用方法的时候,或者获取实例属性的时候,第一个参数是为实例参数,UseCompileTimeType类型是编译期间确定类型,其中还有IsStaticType,IsRef,IsOUt等各种,供我们使用。

然后我们创建一个dynamic的Expression,传入binder,返回类型是object,然后传入需要计算的两个参数10和1,最后得到委托,运行委托即可。

 CallSiteBinder binder =Binder.BinaryOperation(
CSharpBinderFlags.None,
ExpressionType.LeftShift,
typeof(Program),new[]
{
CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.UseCompileTimeType,
null),
CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.UseCompileTimeType,
null)
});
var dynamic = Expression.Dynamic(binder, typeof(object), Expression.Constant(10), Expression.Constant(1));

Func
<int> func = Expression.Lambda<Func<int>>(Expression.Convert(dynamic, typeof(int))).Compile();
Console.WriteLine(func());

创建实例

从上面的Test类看到,我们定义了两个入参,可能有的人会问了为什么入参是两个Binder为什么定义了三个呢,这是因为,创建性的Binder在创建的时候 参数第一个必须是类型参数,所以此处第一个参数必须是Test的type,然后后面是Static类型的参数,

最后一个参数就是3,调用Dynamic,第二个为返回类型的参数,然后传入对应的参数即可创建对象。

  static int A = 5;

var constructorBinder = Binder.InvokeConstructor(CSharpBinderFlags.None, typeof(Program), new[]
{
CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.UseCompileTimeType,
null),
CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.IsStaticType,
null),
CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.UseCompileTimeType,
null)
});
var createInstance = Expression.Dynamic(constructorBinder, typeof(Test),
Expression.Constant(
typeof(Test)),
Expression.Constant(A),
Expression.Constant(
3));var instance = Expression.Lambda<Func<Test>>(createInstance).Compile()();

调用方法

实例方法

实例方法,使用InvokeMember,第二个参数是调用的方法名称,第三个参数是参数类型,由于我没有定义参数所以为null,然后实例方法我们需要定义一个实例参数,在CSharpArgumentInfo定义,然后调用Dynamic,返回类型必须是Object,因为这块扯犊子的是他直接写死的,如果需要转只有自己到表达式树那块Convert转,调用然后生成委托,返回结果。

var invokeBinder =Binder.InvokeMember(
CSharpBinderFlags.None,
"Add",null,typeof(Program),new[] { CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null) });var invokeDynamic=Expression.Dynamic(invokeBinder, typeof(object),Expression.Constant(instance));var returnVal = Expression.Lambda<Func<object>>(invokeDynamic).Compile()();
Console.WriteLine(returnVal);

静态方法

大体上没有区别,在参数类型需要标记为StaticType。传入的参数不再是实例,而是静态方法所属的类型下,可以看到,返回类型必须是Object,我自己在最后Convert了,源码中的Binder默认写死Object

 var invokeStaticBinder =Binder.InvokeMember(
CSharpBinderFlags.None,
"AddOne",null,typeof(Test),new[] { CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.IsStaticType, null) });var invokeStaticDynamic = Expression.Dynamic(invokeStaticBinder, typeof(object),Expression.Constant(typeof(Test)));var Val = Expression.Lambda<Func<int>>(Expression.Convert(invokeStaticDynamic,typeof(int))).Compile()();
Console.WriteLine(Val);

转换

将int转换为Object类型。

   var bindConvert = Binder.Convert(CSharpBinderFlags.None,typeof(object),typeof(Program));var expressConvert = Expression.Dynamic(bindConvert,typeof(object),Expression.Constant(A));var funcVal=Expression.Lambda<Func<object>>(expressConvert).Compile()();

Set Get属性

下面是Set,第二个参数是设置的属性名称,参数类型是实例,以及设置的属性值,最后生成委托,然后调用即可。

   var bindSet = Binder.SetMember(CSharpBinderFlags.None, "A", typeof(Program), new[]
{
CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None,
null),
CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None,
null)
});
var setExpress = Expression.Dynamic(bindSet,typeof(void), Expression.Constant(instance),Expression.Constant(100));var action = Expression.Lambda<Action>(setExpress).Compile();
action();

然后是Get,参数是实例的,然后返回就行了。

   var bindGet = Binder.GetMember(CSharpBinderFlags.None, "A", typeof(Program), new[]
{
CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None,
null)
});
var getExpress = Expression.Dynamic(bindGet, typeof(object), Expression.Constant(instance));var getFunc= Expression.Lambda<Func<object>>(getExpress).Compile()();
Console.WriteLine(getFunc);

一元运算

一元运算的ExpressionType,参数的定义,Binder和表达式树绑定,生成委托。

  var NegateBinder = Binder.UnaryOperation(CSharpBinderFlags.None,ExpressionType.Negate,typeof(Program),new[]
{
CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.UseCompileTimeType,
null)
});
var NegateExpress = Expression.Dynamic(NegateBinder, typeof(object), Expression.Constant(10));var NegateVal = Expression.Lambda<Func<object>>(NegateExpress).Compile()();

Get Set Index

先Set,第一个参数自变量,第二个为索引,第三个是具体的值,然后表达式树和Binder绑定,生成委托,调用,即可,可以看到上面Test我们定义了一个Index的。

 var setIndex = Binder.SetIndex(CSharpBinderFlags.None, typeof(Test), new[]
{
CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None,
null),
CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None,
null),
CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None,
null)
});
var setIndexExpress = Expression.Dynamic(setIndex,typeof(void),Expression.Constant(instance),Expression.Constant(1),Expression.Constant("cxd"));var SetIndexaction = Expression.Lambda<Action>(setIndexExpress).Compile();
SetIndexaction();

然后是get,自变量,索引,生成委托,返回索引的值。

  var getIndex= Binder.GetIndex(CSharpBinderFlags.None, typeof(Program), new[]
{
CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None,
null),
CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None,
null)
});
var getIndexExpress = Expression.Dynamic(getIndex, typeof(object), Expression.Constant(instance), Expression.Constant(0));var getIndexaction = Expression.Lambda<Func<object>>(getIndexExpress).Compile()();

IsEvent

判断属性是不是事件类型的,第二个是属性名称,返回值是bool。

      var isevent = Binder.IsEvent(CSharpBinderFlags.None, "TestEvent", typeof(Program));//换成非Event就不行
        var iseventExpress = Expression.Dynamic(isevent,typeof(bool),Expression.Constant(instance));var res=Expression.Lambda<Func<bool>>(iseventExpress).Compile()();
Console.WriteLine(res);

Invoke

这个是用来调用委托的,我们定义一个Func的委托,可惜的是,返回值还是只能是object,然后参数参数,然后调用委托,就返回了666666。

 var actions= new Func<object>(()=>666666);var invokeOtherBinder = Binder.Invoke(CSharpBinderFlags.None,typeof(Program),new[]
{
CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.UseCompileTimeType,
null),
});
var expression = Expression.Dynamic(invokeOtherBinder, typeof(object),Expression.Constant(actions));var ra=Expression.Lambda<Func<object>>(expression).Compile()();

结尾

下次再见,欢迎大家加群讨论,我是四川观察,感谢各位看官的支持,谢谢大家,咱们下次再见。


1.Rust中的into函数和from函数是做什么用的?

into
函数是Rust语言中的一个转换函数,它属于
Into
trait。它可以将一个类型转换为另一个类型。实现了
From
trait的类型会自动获得
Into
trait的实现,因此通常建议实现
From
而不是直接实现
Into

。例如,我们可以很容易地将一个
str
转换为
String

当然。这里有一个简单的例子,它演示了如何使用
into
函数将一个
str
转换为
String

let my_str = "hello";
let my_string: String = my_str.into();

在这个例子中,我们定义了一个
str
类型的变量
my_str
,并使用
into
函数将其转换为
String
类型。由于
String
类型实现了
From<&str>
trait,因此我们可以使用
into
函数进行转换

2.Rust中的into和from有什么区别?

into

from
是Rust语言中两个用于类型转换的函数,它们分别属于
Into

From
这两个trait。

From
trait允许您定义如何从另一种类型创建自身类型,从而提供了一种非常简单的机制来在多种类型之间进行转换。例如,我们可以很容易地将一个
str
转换为
String


Into
trait则是
From
trait的反向操作。也就是说,如果您已经为您的类型实现了
From
trait,那么当需要时,
Into
会调用它。通常情况下,使用
Into
trait时需要指定要转换为的类型,因为编译器大多数时候无法确定这一点。但这只是一个小小的代价,因为我们可以免费获得这个功能 。

总之,
into

from
函数都可以用于在不同类型之间进行转换。它们之间的主要区别在于它们所属的trait不同,以及它们在实现和使用时的一些细微差别。

当然。这里有一个简单的例子,它演示了如何使用
from

into
函数进行类型转换:

use std::convert::From;

#[derive(Debug)]
struct Number {
value: i32,
}

impl From<i32> for Number {
fn from(item: i32) -> Self {
Number { value: item }
}
}

fn main() {
let num = Number::from(30);
println!("My number is {:?}", num);

let int = 5;
let num: Number = int.into();
println!("My number is {:?}", num);
}

在这个例子中,我们定义了一个名为
Number
的结构体,并为它实现了
From<i32>
trait。这样,我们就可以使用
from
函数将一个
i32
类型的值转换为
Number
类型。

此外,由于我们已经为
Number
类型实现了
From<i32>
trait,因此我们也可以使用
into
函数将一个
i32
类型的值转换为
Number
类型。

在上面的例子中,我们为
Number
类型实现了
From<i32>
trait,这意味着我们可以使用
from
函数将一个
i32
类型的值转换为
Number
类型。由于我们已经实现了
From<i32>
trait,因此我们也可以使用
into
函数将一个
i32
类型的值转换为
Number
类型。

但是,我们并没有为
i32
类型实现
From<Number>
trait,因此我们不能使用
from

into
函数将一个
Number
类型的值转换为
i32
类型。

3.那我应该多使用from还是into?这两个哪个更好一些?

在实现类型转换时,建议优先实现
From
trait 而不是直接实现
Into
trait。这是因为当您为您的类型实现了
From
trait 时,标准库中的 blanket 实现会自动为您提供
Into
trait 的实现。这样,您就可以同时使用
from

into
函数进行类型转换。

在使用类型转换时,您可以根据具体情况选择使用
from

into
函数。它们之间没有本质的优劣之分,只是语法上略有不同。例如,在上面的例子中,我们可以使用以下两种方式将一个
i32
类型的值转换为
Number
类型:

let num = Number::from(30);

或者

let int = 5;
let num: Number = int.into();

4.Rust中的类型转换有哪些限制?

在Rust中,类型转换必须是明确和安全的。这意味着不能随意地将一个类型转换为另一个类型,而是必须通过实现特定的trait来定义类型之间的转换关系。

例如,要使用
into
函数进行类型转换,目标类型必须实现
From
trait,或者源类型必须实现
Into
trait。这样,编译器才能确保类型转换是安全的,并且不会导致未定义行为。

此外,Rust还提供了一些其他的trait来支持更复杂的类型转换,例如
TryFrom

TryInto
。这些trait允许您定义可能失败的类型转换,并在运行时处理转换失败的情况。

总之,在Rust中进行类型转换时,必须遵守语言规定的安全规则,以确保代码的安全性和可靠性

 

Kafka 环境搭建

kafka 安装、配置、启动、测试说明:

1. 安装:直接官网下载安装包,解压到指定位置即可(kafka 依赖的 Zookeeper 在文件中已包含)
下载地址:https://kafka.apache.org/downloads
示例版本:kafka_2.13-2.8.0.tgz
下载后可本地解压安装,解压位置自选,如 D:\Java 下
解压命令:tar -zxvf kafka_2.13-2.8.0.tgz
PS:可在 idea 命令行窗口或 git 提供的命令窗口中进行命令操作
使用 git 提供的命令窗口:空白文件夹中右键——》Git Bash Here 即可打开

2. 添加地址配置
在 D:\Java\kafka_2.13-2.8.0\config\server.properties 中搜索添加以下两行配置:
listeners=PLAINTEXT://localhost:9092
advertised.listeners=PLAINTEXT://localhost:9092
说明:以上配置默认是注释掉的,可搜索找到,根据需求进行自定义地址配置

重要说明:以下命令操作默认都是在 D:\Java\kafka_2.13-2.8.0\ 即 kafaka 根目录下进行!

3. 使用配置文件方式后台启动/关闭 Zookeeper 服务
启动:bin/zookeeper-server-start.sh -daemon config/zookeeper.properties
关闭【自选】:bin/zookeeper-server-stop.sh -daemon config/zookeeper.properties

4. 使用配置文件方式后台启动/关闭 kafka 服务
启动:bin/kafka-server-start.sh -daemon config/server.properties
关闭【自选】:bin/kafka-server-stop.sh -daemon config/server.properties 

5. 服务测试

5.1 创建主题
bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic Hello-Kafka

5.2 查看主题(可能需要查一会儿)
bin/kafka-topics.sh --list --zookeeper localhost:2181

说明:发送消息和监听消息需要打开两个窗口进行测试!

5.3 发送消息(kafka 根目录下新建窗口)
bin/kafka-console-producer.sh --broker-list localhost:9092 --topic Hello-Kafka
输入以上命令回车后,可继续输入内容测试消息发送

5.4 监听消息(kafka 根目录下新建窗口)
bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic Hello-Kafka --from-beginning
输入以上命令后,可观察消息接收情况,并且可在消息发送窗口继续发送消息测试此监听窗口的接收情况,正常接收,则服务环境搭建成功。

Spring Boot 整合 Kafka

环境:自行创建 Spring Boot 项目,添加测试依赖,并启动 Zookeeper 和 kafka 服务。

注意:Zookeeper 默认好像占用 8080 端口,自己注意端口占用问题。

1. 添加依赖

<!-- spring-kafka -->
<dependency>
    <groupId>org.springframework.kafka</groupId>
    <artifactId>spring-kafka</artifactId>
</dependency>

2. 添加配置

# kafka 配置
spring:
  kafka:
    bootstrap-servers: localhost:9092
    producer:
      # 发生错误后,消息重发的次数。
      retries: 1
      #当有多个消息需要被发送到同一个分区时,生产者会把它们放在同一个批次里。该参数指定了一个批次可以使用的内存大小,按照字节数计算。
      batch-size: 16384
      # 设置生产者内存缓冲区的大小。
      buffer-memory: 33554432
      # 键的序列化方式
      key-serializer: org.apache.kafka.common.serialization.StringSerializer
      # 值的序列化方式
      value-serializer: org.apache.kafka.common.serialization.StringSerializer
      # acks=0 : 生产者在成功写入消息之前不会等待任何来自服务器的响应。
      # acks=1 : 只要集群的首领节点收到消息,生产者就会收到一个来自服务器成功响应。
      # acks=all :只有当所有参与复制的节点全部收到消息时,生产者才会收到一个来自服务器的成功响应。
      acks: 1
    consumer:
      # 自动提交的时间间隔 在spring boot 2.X 版本中这里采用的是值的类型为Duration 需要符合特定的格式,如1S,1M,2H,5D
      auto-commit-interval: 1S
      # 该属性指定了消费者在读取一个没有偏移量的分区或者偏移量无效的情况下该作何处理:
      # latest(默认值)在偏移量无效的情况下,消费者将从最新的记录开始读取数据(在消费者启动之后生成的记录)
      # earliest :在偏移量无效的情况下,消费者将从起始位置读取分区的记录
      auto-offset-reset: earliest
      # 是否自动提交偏移量,默认值是true,为了避免出现重复数据和数据丢失,可以把它设置为false,然后手动提交偏移量
      enable-auto-commit: false
      # 键的反序列化方式
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      # 值的反序列化方式
      value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
    listener:
      # 在侦听器容器中运行的线程数。
      concurrency: 5
      # listner负责ack,每调用一次,就立即commit
      ack-mode: manual_immediate
      missing-topics-fatal: false

3. 创建消息生产者

@Component
public class KafkaProducer {

    private Logger logger = LoggerFactory.getLogger(KafkaProducer.class);

    @Resource
    private KafkaTemplate<String, Object> kafkaTemplate;

    public static final String TOPIC_TEST = "Hello-Kafka";

    public static final String TOPIC_GROUP = "test-consumer-group";

    public void send(Object obj) {
        String obj2String = JSON.toJSONString(obj);
        logger.info("准备发送消息为:{}", obj2String);

        // 发送消息
        ListenableFuture<SendResult<String, Object>> future = kafkaTemplate.send(TOPIC_TEST, obj);
        future.addCallback(new ListenableFutureCallback<SendResult<String, Object>>() {
            @Override
            public void onFailure(Throwable throwable) {
                //发送失败的处理
                logger.info(TOPIC_TEST + " - 生产者 发送消息失败:" + throwable.getMessage());
            }

            @Override
            public void onSuccess(SendResult<String, Object> stringObjectSendResult) {
                //成功的处理
                logger.info(TOPIC_TEST + " - 生产者 发送消息成功:" + stringObjectSendResult.toString());
            }
        });
    }

}

4. 创建消息消费者

@Component
public class KafkaConsumer {

    private Logger logger = LoggerFactory.getLogger(KafkaConsumer.class);

    @KafkaListener(topics = KafkaProducer.TOPIC_TEST, groupId = KafkaProducer.TOPIC_GROUP)
    public void topicTest(ConsumerRecord<?, ?> record, Acknowledgment ack, @Header(KafkaHeaders.RECEIVED_TOPIC) String topic) {
        Optional<?> message = Optional.ofNullable(record.value());
        if (message.isPresent()) { // 包含非空值,则执行
            Object msg = message.get();
            logger.info("topic_test 消费了: Topic:" + topic + ",Message:" + msg);
            ack.acknowledge(); // 确认成功消费一个消息
        }
    }

}

5. 消息发送测试

@RunWith(SpringRunner.class)
@SpringBootTest
public class KafkaProducerTest {

    private Logger logger = LoggerFactory.getLogger(KafkaProducerTest.class);

    @Resource
    private KafkaProducer kafkaProducer; // 注意使用自己创建的,看清楚!

    /*
      测试之前需要开启 Kafka 服务
      启动 Zookeeper:bin/zookeeper-server-start.sh -daemon config/zookeeper.properties
      启动 Kafka:bin/kafka-server-start.sh -daemon config/server.properties

      测试结果数据:

      准备发送消息为:"你好,我是Lottery 001"
      Hello-Kafka - 生产者 发送消息成功:SendResult [producerRecord=ProducerRecord(topic=Hello-Kafka, partition=null,
      headers=RecordHeaders(headers = [], isReadOnly = true), key=null, value=你好,我是Lottery 001, timestamp=null),
      recordMetadata=Hello-Kafka-0@47]

      topic_test 消费了: Topic:Hello-Kafka,Message:你好,我是Lottery 001
     */
    @Test
    public void test_send() throws InterruptedException {
        // 循环发送消息
        while (true) {
            kafkaProducer.send("你好,我是Lottery 001");
            Thread.sleep(3500);
        }
    }

}

Redis 是内存数据库,高效使用内存对 Redis 的实现来说非常重要。

看一下,Redis 中针对字符串结构针对内存使用效率做的设计优化。

一、SDS的结构

c语言没有string类型,本质是char[]数组;而且c语言数组创建时必须初始化大小,指定类型后就不能改变,并且字符数组的最后一个元素总是空字符 '\0' 。

以下展示了一个值为 "Redis" 的 C 字符串:

Redis没有直接使用C语言的字符串方式,而是构建了一种简单动态字符串(Simple dynamic string, SDS)的类型,Redis中的字符串底层都是使用SDS结构进行存储,比如包含字符串的键值对底层都是使用SDS结构实现的。

SDS结构定义在sds.h中

struct sdshdr{


    int len;//SDS保存的字符串长度


    int free;//buf数组中未使用字节数量


    char buf[];//字符数组,保存字符串


}

最后一个字节保存了空字符'\0',保留了C字符串的规范,使得SDS结构的字符串,可以重用一部分C函数库的函数。

二、为什么不使用C字符串

主要是因为C字符串有以下缺点:

  • 获取字符串长度时间复杂度为O(N):C字符串获取长度需遍历整个字符串,遇到'\0'空字符为止。
  • 缓冲区溢出:比如在进行字符串追加操作时,如果没有分配足够的内存,就会造成内存溢出。
  • 内存重分配:每次增长或者截短字符串,程序都要对保存C字符串的数组进行内存重分配操作,而内存重分配涉及复杂的算法,并可能需要执行系统调用,所以它通常比较耗时。
  • 空字符问题:C字符串中间不能保存空格,否则程序遍历是会误认为是字符串的末尾。这一限制导致C字符串只能存储文本数据,不能保存像图片、音视频、压缩文件等二进制数据。

三、怎样解决C字符串问题

1、SDS通过len属性记录了SDS长度,所以获取长度的时间复杂度为O(1),即strlen命令的时间复杂度是O(1)。

2、SDS空间分配策略避免了缓冲区溢出:当对SDS进行修改时,会先检查SDS空间是否满足修改,不满足会自动扩展到所需大小,然后才执行修改。

3、较少修改字符串时内存重分配次数:SDS中的free记录buf字节数组中未使用的字节。

redis通过free属性实现空间预分配、惰性空间释放两种优化策略。

  • 空间预分配:当对SDS进行增长操作时,程序不仅会分配修改所必须得空间,还会为SDS分配额外的未使用空间。通过预分配策略,减少了连续执行字符串增长操作时内存重分配次数。
  • 惰性空间释放:当对SDS进行截短操作时,程序并不会立即回收缩短后多出来的字节所占用的内存,而是使用free属性记录多出来的字节数,以供将来使用。如果将来要对这个SDS进行增长操作,未使用空间可能就派上用场,并且增长操作也不一定会执行内存重分配。

SDS结构中的buf字节数组,是二进制安全的,不仅可以保存字符,也可以保存二进制数据。

SDS保留了C字符串的惯例,将数据的末尾设置为空字符'\0',SDS中之所以保留这一规范是可以重用C字符串函数库的一部分函数,例如追加字符串。

四、对字符串的进一步优化

Redis string的三种编码:

  • int 存储8个字节的长整型(long,2^63-1 )
  • embstr, embstr格式的SDS (Simple Dynamic String)
  • raw, raw格式的SDS,存储大于44个字节的长字符串

int类型就是指的是数字,那么raw、embstr都代表的是字符串有什么异同吗,下面我们分析下。

图中展示了两者的区别,可以看到embstr将redisObject和SDS保存在连续的64字节空间内,这样可以只需要一次内存分配,而对于raw来说,SDS和redisObject分离,需要两次内存分配,而且占用更多的内存空间。

可以看到embstr在3.2+中使用了叫sdshdr8的结构,在该结构下,元数据只需要3个字节,而Redis需要8个字节,所以总共64个字节,减去redisObject(16字节),再减去SDS的原信息,最后的实际内容就变成了44字节和39字节。

当字符串小于等于 44 字节时,Redis 就使用了嵌入式字符串的创建方法,以此减少内存分配和内存碎片。

下面这张图展示了 createEmbeddedStringObject 创建嵌入式字符串的过程:

总之,只要记住,Redis 会通过设计实现一块连续的内存空间,把 redisObject 结构体和 SDS 结构体紧凑地放置在一起。

这样一来,对于不超过 44 字节的字符串来说,就可以避免内存碎片和两次内存分配的开销了。