wenmo8 发布的文章

.NET Conf上的介绍

在今年的.NET Conf上Steve Sanderson带来了题为“AI Building Blocks - A new, unified AI layer”的演讲。该演讲的主要内容如下:

“大多数.NET应用程序可以通过AI功能变得更加强大和高效,例如语义搜索、自动分类、摘要生成、翻译、数据提取,甚至是基于聊天的助手。但直到现在,.NET本身还没有统一的AI概念表示标准,因此开发者需要组合使用许多不相关的API。Microsoft.Extensions.AI解决了这个问题,提供了一组新的AI服务标准API,包括在本地工作站上运行或作为托管服务的大型语言模型(LLMs),并集成了文本嵌入、向量存储等功能。在本次演讲中,我们将展示这些新的标准抽象如何让你组合多个服务,并且这些服务可以随着时间的推移轻松替换和更改,以及如何在更高级的场景中接入内部机制。通过本次演讲,你将能够开始在自己的应用程序中实验新的AI功能。”

youtube地址:
https://www.youtube.com/watch?v=qcp6ufe_XYo&list=PLdo4fOcmZ0oXeSG8BgCVru3zQtw_K4ANY&index=3

Steve Sanderson介绍了以下几种应用场景:

image-20241120094958717

Microsoft.Extensions.AI介绍

2024年10月8日,Luis Quintanilla在.NET Blog上发布了题为“Introducing Microsoft.Extensions.AI Preview – Unified AI Building Blocks for .NET”的文章介绍了Microsoft.Extensions.AI Preview。

文章地址:
https://devblogs.microsoft.com/dotnet/introducing-microsoft-extensions-ai-preview/

“Microsoft.Extensions.AI 是一组由 .NET 生态系统中的开发者(包括 Semantic Kernel 团队)共同开发的核心 .NET 库。这些库提供了一层统一的 C# 抽象层,用于与 AI 服务进行交互,例如小型和大型语言模型(SLM 和 LLM)、嵌入内容以及中间件。”

img

“目前,我们的重点是创建抽象概念,这些抽象概念可以由各种服务实现,并且都遵循相同的核心理念。我们不打算发布针对任何特定服务提供商的API。我们的目标是在.NET生态系统中充当一个统一的层,使开发者能够选择他们喜欢的框架和库,同时确保在整个生态系统中的无缝集成和协作。”

Microsoft.Extensions.AI的优势

Microsoft.Extensions.AI 提供了一个统一的 API 抽象,用于 AI 服务,类似于我们在日志记录和依赖注入(DI)抽象方面的成功。我们的目标是提供标准的实现,用于缓存、遥测、工具调用和其他常见任务,这些实现可以与任何提供商兼容。

核心优势有以下几点:

统一API:为将AI服务集成到.NET应用程序提供了一致的API和约定。
灵活性:允许.NET库作者使用AI服务而无需绑定特定提供商,使其适应任何提供商。
易用性:使.NET开发人员能够使用相同的底层抽象尝试不同的包,在整个应用程序中保持单一API。
组件化:简化了添加新功能的过程,并促进了应用程序的组件化和测试。

Microsoft.Extensions.AI简单实践

使用Microsoft.Extensions.AI可以看Nuget包的介绍。

地址:
https://www.nuget.org/packages/Microsoft.Extensions.AI.Abstractions/9.0.0-preview.9.24556.5

先简单的以OpenAI为例,然后考虑到在国内使用OpenAI不便,再介绍一下如何接入兼容OpenAI格式的大语言模型提供商。

简单的对话:

string OPENAI_API_KEY = "sk-sssss...";

IChatClient client =
 new OpenAIClient(OPENAI_API_KEY)
.AsChatClient(modelId: "gpt-4o-mini");

var response = await client.CompleteAsync("你是谁?");

Console.WriteLine(response.Message);

效果:

image-20241120101114704

我比较关心的是Function Calling的功能,来简单尝试一下:

string OPENAI_API_KEY = "sk-sssss...";

[Description("Get the current time")]
 string GetCurrentTime() => DateTime.Now.ToString();

 IChatClient client = new ChatClientBuilder()
     .UseFunctionInvocation()
     .Use(new OpenAIClient(OPENAI_API_KEY).AsChatClient(modelId: "gpt-4o-mini"));

 var response = client.CompleteStreamingAsync(
     "现在几点了?",
     new() { Tools = [AIFunctionFactory.Create(GetCurrentTime)] });

 await foreach (var update in response)
 {
     Console.Write(update);
 }

效果:

image-20241120101404123

成功获取到了当前的时间。

由于在国内使用OpenAI不方便,而且国内也有很多大模型提供商都是兼容OpenAI格式的,因此现在以国内的模型提供商为例,进行说明。

我以硅基流动为例,上面还有一些额度。

简单对话:

 OpenAIClientOptions openAIClientOptions = new OpenAIClientOptions();
 openAIClientOptions.Endpoint = new Uri("https://api.siliconflow.cn/v1");

 // SiliconCloud API Key
 string mySiliconCloudAPIKey = "sk-lll...";


 OpenAIClient client = new OpenAIClient(new ApiKeyCredential(mySiliconCloudAPIKey),  openAIClientOptions);
 IChatClient chatClient = client.AsChatClient("Qwen/Qwen2.5-72B-Instruct-128K");
 var response = await chatClient.CompleteAsync("你是谁?");
 Console.WriteLine(response.Message);

效果:

image-20241120101803488

函数调用:

 OpenAIClientOptions openAIClientOptions = new OpenAIClientOptions();
 openAIClientOptions.Endpoint = new Uri("https://api.siliconflow.cn/v1");

 // SiliconCloud API Key
 string mySiliconCloudAPIKey = "sk-lll...";

  [Description("Get the current time")]
  string GetCurrentTime() => DateTime.Now.ToString();

  IChatClient client = new ChatClientBuilder()
      .UseFunctionInvocation()
      .Use(new OpenAIClient(new ApiKeyCredential(mySiliconCloudAPIKey), openAIClientOptions).AsChatClient("Qwen/Qwen2.5-72B-Instruct-128K"));

  var response = await client.CompleteAsync(
      "现在几点了?",
      new() { Tools = [AIFunctionFactory.Create(GetCurrentTime)] });

  Console.Write(response);

image-20241120102258535

也成功进行函数调用,获取到了当前的时间。

会发现其实和SemanticKernel很像,Steve Sanderson也坦言这些是从SemanticKernel“毕业”的东西,更多用例可由读者自行探索。

Spring Data Redis 为我们提供了下面的Serializer:GenericToStringSerializer、Jackson2JsonRedisSerializer、JacksonJsonRedisSerializer、JdkSerializationRedisSerializer、OxmSerializer、StringRedisSerializer。
image

序列化方式对比:

  • JdkSerializationRedisSerializer: 使用JDK提供的序列化功能。 优点是反序列化时不需要提供类型信息(class),但缺点是需要实现Serializable接口,还有序列化后的结果非常庞大,是JSON格式的5倍左右,这样就会消耗redis服务器的大量内存。
  • Jackson2JsonRedisSerializer: 使用Jackson库将对象序列化为JSON字符串。优点是速度快,序列化后的字符串短小精悍,不需要实现Serializable接口。但缺点也非常致命,那就是此类的构造函数中有一个类型参数,必须提供要序列化对象的类型信息(.class对象)。 通过查看源代码,发现其只在反序列化过程中用到了类型信息。

使用 FastJson2 来做。重写一些序列化器,并实现RedisSerializer接口。源码如下:

<dependency>
    <groupId>com.alibaba.fastjson2</groupId>
    <artifactId>fastjson2</artifactId>
    <version>2.0.50</version>
</dependency>

Fastjson2 序列化

FastJson2JsonRedisSerializer

package com.vipsoft.base.util;

import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONReader;
import com.alibaba.fastjson2.JSONWriter;
import com.alibaba.fastjson2.filter.Filter;

import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;

import java.nio.charset.Charset;

/**
 * Redis使用FastJson序列化
 * 
 * @author ruoyi
 */
public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
{
    /**
     * 自动识别json对象白名单配置(仅允许解析的包名,范围越小越安全)
     */
    public static final String[] JSON_WHITELIST_STR = { "org.springframework", "com.vipsoft" };

    public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");

    static final Filter AUTO_TYPE_FILTER = JSONReader.autoTypeFilter(JSON_WHITELIST_STR);

    private Class<T> clazz;

    public FastJson2JsonRedisSerializer(Class<T> clazz)
    {
        super();
        this.clazz = clazz;
    }

    @Override
    public byte[] serialize(T t) throws SerializationException
    {
        if (t == null)
        {
            return new byte[0];
        }
        return JSON.toJSONString(t, JSONWriter.Feature.WriteClassName).getBytes(DEFAULT_CHARSET);
    }

    @Override
    public T deserialize(byte[] bytes) throws SerializationException
    {
        if (bytes == null || bytes.length <= 0)
        {
            return null;
        }
        String str = new String(bytes, DEFAULT_CHARSET);
        return (T)JSON.parseObject(str, clazz, AUTO_TYPE_FILTER);
    }
}

Redis.config

package com.vipsoft.base.config;

import com.cuwor.base.util.FastJson2JsonRedisSerializer;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@EnableCaching   //开启缓存功能,作用于缓存配置类上或者作用于springboot启动类上
@Configuration
public class RedisConfig {


    /**
     * 创建一个RedisTemplate实例,用于操作Redis数据库。
     * 其中,redisTemplate是一个泛型为<String, Object>的模板对象,可以存储键值对数据;
     * @param factory   factory是一个Redis连接工厂对象,用于建立与Redis服务器的连接
     * @return
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {

        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);

        FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);

        // 使用StringRedisSerializer来序列化和反序列化redis的key值
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(serializer);

        // Hash的key也采用StringRedisSerializer的序列化方式
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(serializer);

        template.afterPropertiesSet();
        return template;
    }
}

RedisUtil.java

package com.vipsoft.base.util;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;

import java.util.*;
import java.util.concurrent.TimeUnit;

@Component
public class RedisUtil {

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 判断key是否存在
     *
     * @param key 键
     * @return true 存在 false不存在
     */
    public boolean hasKey(String key) {
        return redisTemplate.hasKey(key);
    }


    /**
     * 指定缓存失效时间
     *
     * @param key  键
     * @param time 时间(秒)
     */
    public boolean expire(String key, long time) {
        try {
            redisTemplate.expire(key, time, TimeUnit.MINUTES);
            return true;
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return false;
    }


    /**
     * 获取 Keys
     *
     * @param key 键
     */
    public Set<String> keys(String key) {
        Set<String> result = new HashSet();
        try {
            result = redisTemplate.keys(key);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return result;
    }


    /**
     * 删除缓存
     */
    public boolean del(String key) {
        try {
            redisTemplate.delete(key);
            return true;
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return false;
    }

    /**
     * 删除缓存
     */
    public boolean del(Collection<String> keys) {
        try {
            redisTemplate.delete(keys);
            return true;
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return false;
    }

    //region String

    public <T> T get(String key) {
        ValueOperations<String, T> operation = redisTemplate.opsForValue();
        return operation.get(key);
    }

    public <T> boolean set(String key, T value) {
        try {

            redisTemplate.opsForValue().set(key, value);
            return true;
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return false;
    }

    /**
     * 普通缓存放入并设置时间
     *
     * @param key   键
     * @param value 值
     * @param time  时间(分种) time要大于0 如果time小于等于0 将设置无限期
     * @return true成功 false 失败
     */
    public boolean set(String key, Object value, long time) {
        try {
            redisTemplate.opsForValue().set(key, value, time, TimeUnit.MINUTES);
            return true;
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return false;
    }

    //endregion

    //region Hash

    public <T> T hget(String key, String hashKey) {
        HashOperations<String, String, T> hash = redisTemplate.opsForHash();
        return hash.get(key, hashKey);
    }

    public <T> Map<String, T> hmget(String key) {
        return redisTemplate.opsForHash().entries(key);
    }


    /**
     * 向一张hash表中放入数据,如果不存在将创建
     *
     * @param key     键
     * @param hashKey 项
     * @param value   值
     * @return true 成功 false失败
     */
    public boolean hset(String key, String hashKey, Object value) {
        redisTemplate.opsForHash().put(key, hashKey, value);
        return true;
    }

    /**
     * 向一张hash表中放入数据,如果不存在将创建
     *
     * @param key     键
     * @param hashKey 项
     * @param value   值
     * @param time    时间(分钟) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
     * @return true 成功 false失败
     */
    public boolean hset(String key, String hashKey, Object value, long time) {
        redisTemplate.opsForHash().put(key, hashKey, value);
        if (time > 0) {
            redisTemplate.expire(key, time, TimeUnit.MINUTES);
        }
        return true;
    }

    /**
     * HashSet
     *
     * @param key 键
     * @param map 对应多个键值
     * @return true 成功 false 失败
     */
    public boolean hmset(String key, Map<String, Object> map) {
        redisTemplate.opsForHash().putAll(key, map);
        return true;
    }

    /**
     * HashSet 并设置时间
     *
     * @param key  键
     * @param map  对应多个键值
     * @param time 时间(秒)
     * @return true成功 false失败
     */
    public boolean hmset(String key, Map<String, Object> map, long time) {
        redisTemplate.opsForHash().putAll(key, map);
        if (time > 0) {
            redisTemplate.expire(key, time, TimeUnit.MINUTES);
        }
        return true;
    }


    /**
     * 删除hash表中的值
     *
     * @param key     键 不能为null
     * @param hashKey 项 可以使多个 不能为null
     */
    public void hdel(String key, Object... hashKey) {
        redisTemplate.opsForHash().delete(key, hashKey);
    }

    /**
     * 判断hash表中是否有该项的值
     *
     * @param key     键 不能为null
     * @param hashKey 项 不能为null
     * @return true 存在 false不存在
     */
    public boolean hHasKey(String key, String hashKey) {
        return redisTemplate.opsForHash().hasKey(key, hashKey);
    }


    /**
     * hash递增 如果不存在,就会创建一个 并把新增后的值返回
     *
     * @param key  键
     * @param hashKey 项
     * @param by   要增加几(大于0)
     * @return
     */
    public double hincr(String key, String hashKey, double by) {
        return redisTemplate.opsForHash().increment(key, hashKey, by);
    }

    /**
     * hash递减
     *
     * @param key  键
     * @param hashKey 项
     * @param by   要减少记(小于0)
     * @return
     */
    public double hdecr(String key, String hashKey, double by) {
        return redisTemplate.opsForHash().increment(key, hashKey, -by);
    }


    //endregion

    //region List

    /**
	 * 获取list缓存的内容
	 * @param key 键
	 * @param start 开始
	 * @param end 结束 0 到 -1代表所有值
	 * @return
	 */
	public <T> List<T> lrange(String key, long start, long end) {
		return redisTemplate.opsForList().range(key, start, end);
	}

	/**
	 * 获取list缓存的长度
	 * @param key 键
	 * @return
	 */
	public long llen(String key) {
		return redisTemplate.opsForList().size(key);
	}

	/**
	 * 通过索引 获取list中的值
	 * @param key 键
	 * @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
	 * @return
	 */
	public Object lindex(String key, long index) {
		return redisTemplate.opsForList().index(key, index);
	}

	/**
	 * 将一个或多个值插入到列表的尾部(最右边)。
	 * @param key 键
	 * @param value 值
	 * @return
	 */
	public boolean lrpush(String key, Object value) {
		redisTemplate.opsForList().rightPush(key, value);
		return true;
	}

	/**
	 * 将一个或多个值插入到列表的尾部(最右边)。
	 * @param key 键
	 * @param value 值
	 * @param time 时间(秒)
	 * @return
	 */
	public boolean lrpush(String key, Object value, long time) {
		redisTemplate.opsForList().rightPush(key, value);
		if (time > 0) {
			expire(key, time);
		}
		return true;
	}

	/**
	 * 将一个或多个值插入到列表的尾部(最右边)。
	 * @param key 键
	 * @param value 值
	 * @return
	 */
	public boolean lrpush(String key, List<Object> value) {
		redisTemplate.opsForList().rightPushAll(key, value);
		return true;
	}

	/**
	 * 将list放入缓存
	 * @param key 键
	 * @param value 值
	 * @param time 时间(秒)
	 * @return
	 */
	public boolean lrpush(String key, List<Object> value, long time) {
		redisTemplate.opsForList().rightPushAll(key, value);
		if (time > 0) {
			expire(key, time);
		}
		return true;
	}

	/**
	 * 通过索引来设置元素的值
	 * @param key 键
	 * @param index 索引
	 * @param value 值
	 * @return /
	 */
	public boolean lset(String key, long index, Object value) {
		redisTemplate.opsForList().set(key, index, value);
		return true;
	}

	/**
	 * 删除指定key集合中值等于value的元素(count=0, 删除所有值等于value的元素; count>0, 从头部开始删除第一个值等于value的元素; count<0, 从尾部开始删除第一个值等于value的元素)
	 * @param key 键
	 * @param count  count>0, 从头部开始删除第一个值等于value的元素; count<0, 从尾部开始删除第一个值等于value的元素
	 * @param value 值
	 * @return 移除的个数
	 */
	public long lremove(String key, long count, Object value) {
		return redisTemplate.opsForList().remove(key, count, value);
	}

    //endregion
}


fastjson和fastjson2的区别

FastJSON是一种广泛使用的JSON解析库,其高性能和简单易用的特点受到开发者的喜爱。然而,随着应用场景的复杂化和安全要求的提高,FastJSON逐渐暴露出一些问题。为了解决这些问题并进一步提升性能和安全性,阿里巴巴推出了FastJSON2。以下是FastJSON和FastJSON2的详细对比:
一、性能
FastJSON:
以其快速高效著称,可以快速地将Java对象转换为JSON字符串,也可以将JSON字符串转换为Java对象。
FastJSON2:
在性能上进行了多方面的优化,通过优化序列化算法和数据结构,提高了序列化的效率,减少了不必要的对象创建和复制操作,从而降低了CPU和内存的开销。同时,对解析器进行了重构,使其能够更快速地处理大规模JSON数据。根据官方提供的性能测试数据,FastJSON2的性能远超其他流行的JSON库,如Jackson、Gson和org.json。

二、安全性
FastJSON:
在安全性方面存在一些漏洞,特别是其AutoType功能的安全性问题较为突出。该功能在序列化的JSON字符串中带上类型信息,在反序列化时不需要传入类型,实现自动类型识别,但这也为恶意攻击提供了可能。
FastJSON2:
在安全性方面做了显著改进,针对之前版本中暴露的一些安全漏洞进行了修复。为了防止反序列化漏洞,FastJSON2引入了更严格的黑名单机制,默认禁止了某些危险类型的反序列化操作。同时,增强了类型检测机制,可以更好地防止恶意数据的注入,确保数据解析的安全性。此外,FastJSON2的AutoType功能必须显式打开才能使用,且没有任何白名单,也不包括任何Exception类的白名单,这进一步提高了其安全性。
三、功能特性
FastJSON:
支持复杂对象的处理,包括嵌套对象、集合、Map等;支持注解来指定Java对象与JSON字符串之间的映射关系;允许用户自定义实现对象的序列化和反序列化过程;提供了流式的API,可以在处理大量JSON数据时提供更好的性能。
FastJSON2:
在FastJSON的基础上增加了一系列新的功能特性。例如,对JSONPath的支持更加全面和高效,允许开发者使用JSONPath表达式方便地访问和操作JSON数据;改进了对日期和时间的处理,提供了更多的配置选项和更好的性能,支持多种日期格式和时区处理;提供了对JSON Schema的支持,增强了数据验证的能力;新增加对二进制格式JSONB的支持,进一步提升了数据处理的效率和灵活性。
四、兼容性
FastJSON:
作为早期版本的JSON处理库,已经被广泛应用于各种Java项目中。
FastJSON2:
在保持与FastJSON兼容性的同时,也进行了一些必要的改进和优化。例如,FastJSON2支持最新的JDK特性,包括JDK 11和JDK 17等;同时提供了对Kotlin语言的优化支持,使得Kotlin开发者能够更加便捷地使用FastJSON2。此外,FastJSON2还支持跨平台兼容性,包括Android 8+等平台。
五、易用性
FastJSON:
提供了简单明了的API,使用起来非常方便。
FastJSON2:
对部分API进行了调整和优化,使其更加直观和易用。同时提供了更详细和友好的错误提示信息,帮助开发者更快地定位和解决问题。
综上所述,FastJSON2在性能、安全性、功能特性、兼容性和易用性等方面相比FastJSON都有显著的提升。对于需要高性能JSON解析和序列化的应用场景,特别是在安全性要求较高的情况下,FastJSON2是一个更好的选择。

公司SaaS系统有个给客户的员工发放金币,最后计算金币老是流水和总额对不上,以前负责这块的人做过修改还是不对,后来这负责人离职,接手大数据的事情后,该客户真在用金币这块业务,而且财务用这个结算对账,2023年底客户逼急了,要彻底解决这个问题:

和负责这块的产品经理沟通这块内容,说这个金币计算有历史原因导致流水和总额不对,不确定是谁不对,程序也有bug,但现在要重新计算对上,因不懂这块业务,产品经理最后确定原则:

1,每条金币日志流水中会保留当前的金币总数,用这个2022年12月31日的总额为准,把不对的总额改成当前的流水的总额

2,在按当天总额+后面的流水重新计算得到每天的期末和期初(金币余额)

持续多少年的问题,一直没解决,一听就头疼,尽接这样的难题,这里面的坑也不小:

后来查询金币报表的计算逻辑,同时按上面的思路做发现,不能按2的来做,因为到2022年12月31日批量计算后,和报表的总额有部分还是对不上。后来找了产品经理,他说他只是提供一下解决思路,具体怎么做自己想办法了!

整体解决方法如下:

1,SQL查出截止到2022年12月31日的最新的一个金币记录数(2023-01-01前)

2,对比金币数据,不一致的改成上面总额一致

3,通过计算金币流水的存储过程,重新计算从2023-01-01到2023-12-25的每天的金币明细

4,对比金币在2023-12-25的流水总额,不一致的查看不一致原因

5,发现有几十个账号的金币还是不一致, 期初是一致的,通过计算流水到2023-12-25就不一样,不得不一个账号查原因再计算

其中发现有程序bug:2个金币明细表的数据有同一条重复的数据,修改其中的一条金币数为0,还有金币数小于0的计算。

通过这样的笨办法,一个一个看数据,再修正数据到一致,修的眼睛都看花了,程序的bug导致提交后,有2条是一样的数据,让研发同学去改程序。

没想到2周后,客户发现又有账号的金币又对不上,自己手工重新计算后,对比发现就只有1个账号不对,修正好,再加上程序的修正,到现在2024年11月18日,客户再也没说金币不对的问题。

总结:

想想为何这么多年的问题一直没有彻底解决,是真的很难?现在看来,就是数据细心核对修正和程序bug修改,就OK,
真是事上无难事,只怕有心人

Hivgor脚本文件

在构建的生命周期中Hvigor使用两个脚本文件来完成插件、任务以及生命周期hook的注册:

  • hvigorconfig.ts
    :此文件在整个项目中只有根目录下存在一份,不是构建必须的文件并且默认不存在,如有需要可自行创建,此文件被解析执行的时间较早,可用于在Hvigor生命周期刚开始时操作某些数据。
  • hvigorfile.ts
    :此文件在每个node下都有一份,是构建的必须文件,在此文件中可以注册插件、任务以及生命周期hook等操作。

任务与任务依赖图

Hvigor是基于任务对您的项目进行自动化构建的,任务(Task)是Hvigor构建过程中的基本工作单元,它定义了构建项目时需要执行的具体工作。任务可以完成多种操作,比如源码编译任务,打包任务或签名任务等。每一种任务的执行逻辑由插件(plugin)提供,插件可以是由hvigor-ohos-plugin提供的默认任务逻辑,也可由您个性化定制。

需要注意的一点是,任务是存在依赖关系的,Hvigor在执行任何任务之前会构建任务依赖图,所有任务会形成一个有向无环图(DAG),如下示例图,任务之间的依赖关系用箭头进行表示:
img1
hvigor插件(hvigor-ohos-plugin)和hvigorfile.ts文件中的构建脚本都将通过任务依赖机制对任务依赖图做出影响。

hvigor-ohos-plugin

hvigor-ohos-plugin是默认的构建插件,为任务(Task)的完成提供业务逻辑支持,比如为Hvigor提供Hap、Har和Hsp打包服务等任务,每一种任务的具体执行逻辑由本模块中不同的插件来提供。

Hvigor与hvigor-ohos-plugin的关系

概述部分提到了Hvigor提供任务注册编排以及配置管理等任务管理机制,它负责控制任务的执行流程,但是并不包含每一个任务的具体业务逻辑,具体逻辑是由hvigor-ohos-plugin提供的。

Hvigor和hvigor-ohos-plugin的关系可以通过下图来说明,Hvigor接受任务的注册并编排任务执行顺序,并按照顺序依次调用hvigor-ohos-plugin中的插件来执行任务。如果您定制了自己的任务逻辑插件并将其注册,hvigor-ohos-plugin也会调用您的个性化插件来完成编译构建流程。

在Hvigor执行构建的过程中,hvigor-ohos-plugin会向Hvigor进行任务的注册,Hvigor会根据构建的任务执行有向图依次调用对应的插件来执行相应任务,在完成编译、打包、签名等一系列任务后,Hvigor也就正式完成了构建。
img2

Hvigor生命周期

生命周期展示了Hvigor编译构建系统如何进行一次完整的编译构建流程。Hvigor的编译构建过程有三个不同的阶段,分为初始化、配置和执行,Hvigor会按顺序运行这些阶段。

  • 初始化
    :此阶段主要目的为初始化项目的编译参数,构造出项目结构的树形数据模型,每个node为一个HvigorNode对象。
  • 配置
    :此阶段开始时,所有的node都已经加载完毕,但每个node中还没有加载插件(plugin)、任务(task)和DAG图,此阶段的主要目的就是加载出这些内容。
  • 执行
    :任务之间的依赖关系决定了任务执行顺序,任务可以并行执行

生命周期及hook点

在Hvigor的生命周期中,以下多个hook点可供您使用,便于您在对应的时机调用某些逻辑。在下图中所有绿色标记的线框为可以使用的hook点
img3

Moebius(莫比斯)
介绍

Moebius数据库多活集群是格瑞趋势为SQL Server数据库研发的能够同时满足可用性、数据安全、容灾、读写分离、负载均衡的一站式多活集群。
集群的名字取自
Moebius环,寓意无限扩展。

Moebius
采用
“share nothing”
架构,每个节点的
SQL Server
服务独立安装,使用每个服务器自己存储介质内的数据库文件。不基于共享存储设备,也不基于磁盘镜像等功能,通过
SQL Server
的日志同步技术实现各节点中数据的一致性。
在主节点写入数据时会产生日志,
Moebius
捕获并传输日志到其他节点,并通过
REDO
技术把日志转换成数据。因此每个节点的
SQL Server
服务都是启动的,数据都是



的。
Moebius
有实时和准实时两种同步方式,不同的节点可以使用不同的同步方式。

Moebius
通过

网络心跳



仲裁机制

实现故障监控,当侦测到某节点发生故障并经过仲裁后,将此节点剥离出集群,如果故障节点是主节点,则会进行自动故障转移,重新选择健康的节点作为主节点。节点故障恢复后会自动从主节点同步差异数据,同步完成后加入到集群中。

Moebius
的调度引擎支持连接级和
SQL
语句级两种调度方式,通过规则的配置,在不改动或者少改动应用程序的前提下,透明的实现读写分离、负载均衡。

Moebius集群的
架构


Moebius集群采用无共享磁盘架构
Moebius集群由一组数据库服务器组成,每个服务器上安装相同的数据库,集群支持无共享磁盘架构,各机器可以不连接一个共享设备,数据可以存储在每个机器自己的存储介质中。

无共享磁盘架构,使得存储不再是单点,系统可用性提高,同时还可以充分利用集群中每个机器的CPU、I/O等硬件来实现集群的高性能。
无需价格高昂的共享磁盘柜,只要使用2台服务器即可轻松构筑低成本的集群。

Moebius集群架构的分类


依据数据是否分区,Moebius集群架构分为标准架构和高级架构:
标准架构
:每个节点中具有完全相同的数据,每个节点都拥有数据全集。
高级架构
:每个节点中数据是不同的,每个节点只拥有数据全集的一部分。
Moebius For SQL Server标准架构
Moebius集群是一组相互独立的服务器,通过相互协作形成一个统一的整体。集群中多个节点相互连接,这样冗余的硬件架构不但可以避免单点故障而且提供了杰出的故障恢复能力。一旦发生系统失败,Moebius集群对用户保证最高的可用性,保障关键是业务数据不丢失。

Moebius 集群标准架构
一个集群数据库可以看作是一个被多个应用实例访问的单一数据库。在Moebius集群中,每个SQL Server实例在各自的服务器上运行。随着应用的增加,当需要添加额外的资源时,可以在不停机的情况下很容易地增加节点。
标准架构中间件工作原理
中间件驻留在每个机器的数据库中,监测数据库内数据的变化,并将变化的数据同步到其它数据库中。数据同步完成后客户端才会得到响应,同步过程是并发完成的,因此同步到多个数据库和同步到一个数据库的时间基本相等;另外同步过程是在事务环境下完成的,保证了多份数据的数据一致性。

正因为中间件宿主在数据库中,所以中间件不但能知道数据的变化,而且知道引起数据变化的SQL语句,根据SQL语句的类型智能地采取不同的数据同步策略以保证数据同步成本的最小化:
1. 数据条数很少,数据内容也不大,则直接同步数据。
2. 数据条数很少,但是里面包含大数据类型,比如文本,二进制数据等, 则先对数据进行压缩然后再
同步,从而减少网络带宽的占用和传输所用的时间。
3. 数据条数很多,此时中间件会获取造成数据变化的SQL语句, 然后对SQL语句进行解析,分析其
执行计划和执行成本,并选择是同步数据还是同步SQL语句到其他的数据库中。在对表结构进行调整
或者批量更改数据的时候,这种同步策略非常有用。
Moebius For SQL Server高级架构
在高级架构中,采用数据分区技术,依据某种规则把数据分散到多个数据库中。
数据为什么分区?
1.当数据量很大的时候,即使服务器在没有任何压力的情况下,某些复杂的查询操作都会非常缓慢,影
响最终用户的体验。
2.在大数据量下对数据库的装载与导出,备份与恢复,结构的调整,索引的调整等都会让数据库停止服
务或者高负荷运转很长时间,影响数据库的可用性和易管理性。
3.面对这样的应用环境,仅仅依靠提升服务器的硬件配置是起不到作用的,比较好的办法是通过数据分
区,把数据分成更小的部分来提高数据库的可用性和易管理性。
4.分区把各部分数据放到不同的机器中,每次查询可以由多个机器上的CPU、I/O来共同负载,通过
各节点并行处理数据来提高性能。

系统结构
Moebius For SQL Server高级架构
在结构上分访问层数据库和数据层数据库两部分。
访问层:
访问层数据库只有原来数据库的结构没有数据,处理提交上来的
SQL语句并调度执行。访问层数
据库可以由多个机器来负载均衡。
数据层:
数据层数据库就是原来的数据库,但是可以有多个冗余对查询进行负载均衡,以提高整个系统
的性能,Moebius For SQL Server保证多个数据库的一致性;数据层数据库不暴露给用户和业务程序,用户和业务程序面对的是访问层数据库。

通过访问层和数据层构建出一个网格集群来实现集群的高可用性和负载均衡,访问层和数据层的数据库是可以扩展的。(每列中各节点的数据是相同的,每行构成数据的全集;图中数据数据层设计为5×2矩阵,在实际应用中要依据业务的特点来划分)。
如何分区?
Moebius For SQL Server支持两种分区方式:Hash分区和线性分区。
Hash分区
:是将表按某一字段的值均匀地分布到若干个指定分区中的一种分区方法。
优点:
每个分区内分配的数据比较平均,承载的压力也就比较平均,机器能够得到充分的利用。
缺点:
不易扩展,如果扩展新的分区会涉及到数据的重新分配,因此在设计的时候要提前规划好。
Moebius For SQL Server支持把多个分区数据放在一个机器上然后再根据压力逐个的拆到新机器中去,这样既可以保证了分区的规划又不浪费机器,实现了线性扩展。
线性分区
:即范围分区,将表按某一字段的取值范围进行分区,如按时间,每个月的数据在一个分区中。
优点
:扩展性能比较好,因为数据的增长是有一定规律的。
缺点:
每个分区内数据的压力不是很平均,大部分业务都存在这种现象,越老的数据被访问的频率越低,从而导致各机器面临的压力也不同,因此使机器的利用率不高。
Moebius For SQL Server支持把多个分区数据放在一个机器上,所以可以通过新老分区的交替使用来提高机器的利用率。
分区操作在管理工具中很容易配置,首先设置分区,接下来给每个表选择分区并设置分区字段
.
这样中间件在解析、处理SQL语句的时候就会根据配置把数据分配到相应分区所在的机器中去或者从相应分区中读取数据。和其他一些集群不同的是Moebius For SQL Server的分区是经过抽象的,是完全透明的。

高级架构中间件工作原理
1.中间件解析到查询的SQL语句后,首先分析该语句要查找的表,根据所要查找表的分区配置和SQL语句的WHERE条件计算出要从一个分区中还是多个分区中去取数据,取完数据后在访问层合并后再返回给应用程序。这里要重点说明的是中间件通过分析SQL语句,能够对分区范围进行动态缩小或者放大。SELECT * FROM dbo.UserInfo WHERE UserID = 1,因为UserID是分区字段,所以中间件只会从一个分区中查找;SELECT * FROM dbo.UserInfo WHERE UserID IN(3, 4)则会从两个分区中查找;SELECT * FROM dbo.UserInfo WHERE Username = ‘wangzhongtao’,没有使用分区字段作为查询的条件,中间件就会从每个分区列数据库进行查找。对于多个分区列数据进行查询的操作是并行的从而保证总体响应时间的最小化。

2.中间件解析到更新的SQL语句后,首先分析要更新的表,根据要更新表的分区配置和更新语句的SQL语法来计算出要更新一个或者多个分区中的数据。例如INSERT dbo.UserInfo(UserID, Username) VALUES(1, ‘wangzhongtao’),中间件会解析到UserID = 1,然后根据表的分区配置把数据插入到第一个分区中去。中间件解析到一个更新的SQL语句后,会同时更新同一列中的数据库。第一:更新操作是并行的,整个操作的响应时间和更新一个数据库的响应时间基本相同;第二:整个操作是在事务的环境下完成的,保证了多个数据库中数据是一致的,实现了真正的冗余。
3.中间件解析到一个更新数据库结构的DDL语句,会把该语句同步到其他访问层数据库和所有的数据层数据库中。这样用户就像在使用一个数据库去维护表、索引、存储过程等等,大大降低了用户的管理成本,也降低了出错的机率。这是Moebius For SQL Server 的亮点。

常见问题

虚拟化
\超融合平台下还有做Moebius的必要吗?

虚拟化或者超融合虽然能够保障可用性和数据安全,
但本质上提供的是一个单数据库服务器。如果有在
SQL Server层面的容灾或者读写分离、负载均衡的需求,就是有必要的。

Moebius兼容虚拟化\超融合平台吗?

兼容,
Moebius的节点可以是物理机,也可以是虚拟机,或者是两者的组合。

做了
Moebius后还有必要做存储双活吗?

如果存储设备只是为了数据库服务器提供的,那么是没有必要再做双活的,因为
Moebius的数据就是多份的,既能保障数据安全,又可以随时验证数据的一致性。可以让每个Moebius节点各接一个独立的存储设备,这样既节省了双活模块的成本,又提升了磁盘空间的利用率。

北京格瑞趋势科技有限公司是聚焦于数据服务的高新技术企业,成立于2008年,创始团队及核心技术人员来自微软和雅虎。微软数据平台高级合作伙伴,卫宁健康数据平台战略合作伙伴。通过产品+服务双轮驱动的业务模式,14年间累计服务4000+客户,覆盖互联网、市政、交通、电信、医疗、教育、电力、制造业等各个领域。