【.Net7】-性能优化-.Net7+WebApi+EFCore+SqlServer读写分离封装
这篇博客描述的是运行环境是.Net 7下使用WebApi,ORM框架使用EF Core的DbFirst模式,再配合上SqlServer的1主,2从3个数据库,完成的读写分离封装。
一.先准备3个数据库,1主,2从
我先准备了3个数据库,分别是:SchoolDB(作为主库,到时候只负责写)、SchoolDB_Read_1(作为从库1,到时候只负责读)、SchoolDB_Read_2(作为从库2,到时候只负责读),里面都有张学生表。
二.EFCore DbFirst模式生成实体和DbContext
根据数据库生成实体,工具=>NuGet包管理器=>程序包管理器控制台(项目设置为启动项)
生成命令:
三.封装前的其他类准备
3.1.数据库连接配置
namespace MengLin.Shopping.SchoolDB.DbFirst.ConfigureOptions { /// <summary> /// 数据库连接配置 /// </summary> public class ConnectionStringOptions { /// <summary> /// 写链接-主库Mast /// </summary> public string WriteConnection { get; set; } /// <summary> /// 读链接-从库Salve /// </summary> public List<string> ReadConnectionList { get; set; } } }
3.2.appsettings.json映射ConnectionStringOptions数据库连接配置的json文件
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*", "ConnectionStringOptions": { "WriteConnection": "Server=meng\\MSSQLSERVERML;Database=SchoolDB;uid=sa;pwd=123456abc;Trusted_Connection=True;TrustServerCertificate=true", "ReadConnectionList": [ "Server=meng\\MSSQLSERVERML;Database=SchoolDB_Read_1;uid=sa;pwd=123456abc;Trusted_Connection=True;TrustServerCertificate=true", "Server=meng\\MSSQLSERVERML;Database=SchoolDB_Read_2;uid=sa;pwd=123456abc;Trusted_Connection=True;TrustServerCertificate=true" ] } }
3.3.操作数据库是读还是写的枚举
namespace MengLin.Shopping.SchoolDB.DbFirst.Enum { public enum WriteAndReadEnum { //主库操作 Write, //从库操作 Read } }
四.定义接口-IBaseService
定义一些常用、共性操作,比如:增删改查,为了简化代码,我这里只是定义了Insert添加、Where分页查询、Commit提交三个方法。
/// <summary> /// 基本操作接口 /// </summary> public interface IBaseService { /// <summary> /// 添加数据 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="t"></param> /// <returns></returns> T Insert<T>(T t) where T : class;
/// <summary> /// 根据表达式目录树进行分页查找数据 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="expression">表达式目录树</param> /// <param name="writeAndReadEnum">默认从库操作</param> /// <returns></returns> (IQueryable<T>, int totalCount) Where<T>(Expression<Func<T, bool>> expression,int pageIndex,int pageSize, WriteAndReadEnum writeAndReadEnum = WriteAndReadEnum.Read) where T : class; /// <summary> /// 保存提交 /// </summary> void Commit(); }
五.定义基本操作实现类-BaseService
继承IBaseService接口,实现接口里面的Insert添加、Where分页查询、Commit提交这三个方法。
/// <summary> /// 基本操作实现 /// </summary> public class BaseService : IBaseService, IDisposable { /// <summary> /// 数据访问工厂 /// </summary> private DBContextFactory _dbContextFactory = null; /// <summary> /// 构造函数注入DbContext工厂 /// </summary> /// <param name="dbContext"></param> public BaseService(DBContextFactory dbContextFactory) { _dbContextFactory = dbContextFactory; } /// <summary> /// 插入数据 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="key">主键</param> /// <returns></returns> public T Insert<T>(T t) where T : class {
//只能对主库增加 _dbContext = _dbContextFactory.GetSetupDbContext(WriteAndReadEnum.Write); _dbContext.Set<T>().Add(t); return t; }
/// <summary> /// 保存提交 /// </summary> public void Commit() { _dbContext.SaveChanges(); } /// <summary> /// 释放资源 /// </summary> public void Dispose() { if (_dbContext != null) { _dbContext.Dispose(); } }
/// <summary> /// 根据表达式目录树进行分页查找数据 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="expression">表达式目录树</param> /// <param name="writeAndReadEnum">默认从库操作</param> /// <returns></returns> public (IQueryable<T>,int totalCount) Where<T>(Expression<Func<T, bool>> expression, int pageIndex, int pageSize, WriteAndReadEnum writeAndReadEnum = WriteAndReadEnum.Read) where T : class { //选择其中一个从库进行查询 _dbContext = _dbContextFactory.GetSetupDbContext(writeAndReadEnum); return (_dbContext.Set<T>().Where(expression).Skip((pageIndex-1) * pageSize).Take(pageSize), _dbContext.Set<T>().Where(expression).Count()); } }
六.定义学生服务类-IStudentService、StudentService
学生服务,除了基本的增删改查,还要有自己的行为,比如:打游戏,学习。
/// <summary> /// 学生接口 /// </summary> public interface IStudentService:IBaseService { /// <summary> /// 学习 /// </summary> public void Study(); /// <summary> /// 玩游戏 /// </summary> public void PalyGame(); }
/// <summary> /// 学生服务 /// </summary> public class StudentService:BaseService, IStudentService { public StudentService(DBContextFactory dbContextFactory) : base(dbContextFactory) { } /// <summary> /// 学习 /// </summary> public void Study() { Console.WriteLine("我要学习了!"); } /// <summary> /// 玩游戏 /// </summary> public void PalyGame() { Console.WriteLine("我要玩游戏了!"); } }
七.最重要的来了,DBContext工厂类-DBContextFactory
.Net 7框架中动不动来个工厂,比如:DefaultServiceProviderFactory(IOC容器工厂,造容器的)、DefaultControllerFactory(控制器工厂,造控制器的),我也借鉴.Net 7框架的思想,我来个DBContextFactory,造DBContext的,其实也不算造DBContext,只是指定DBContext的数据库连接字符串。
在DBContextFactory中完成了对DBContext连接数据库字符串的指定。
namespace MengLin.Shopping.SchoolDB.DbFirst.Factory { /// <summary> /// DBContext制造工厂 /// </summary> public class DBContextFactory { /// <summary> /// DbContext数据库上下文 /// </summary> private readonly DbContext _dbContext = null; /// <summary> /// 读/写数据库连接字符串配置 /// </summary> private readonly ConnectionStringOptions _connectionStringOptions = null; /// <summary> /// 构造函数注入DbContext实例 /// 构造函数Option注入读/写数据库连接字符串配置 /// </summary> /// <param name="dbContext"></param> public DBContextFactory(DbContext dbContext, IOptionsSnapshot<ConnectionStringOptions> connectionStringOptions) { _dbContext = dbContext; _connectionStringOptions = connectionStringOptions.Value; } /// <summary> /// 得到已经重新设置过数据库连接的DbContext /// </summary> /// <param name="writeAndReadEnum">标记读或写</param> /// <returns></returns> public DbContext GetSetupDbContext(WriteAndReadEnum writeAndReadEnum) { //设置读数据库连接字符串 if (writeAndReadEnum is WriteAndReadEnum.Read) { SetReadConnectionString(); } else if(writeAndReadEnum is WriteAndReadEnum.Write)//设置写数据库连接字符串 { SetWriteConnectionString(); } return _dbContext; } /// <summary> /// 设置写数据库连接字符串 /// </summary> private void SetWriteConnectionString() { //从注入的Options配置中获取写的数据库链接字符串 string writeConnectionString = _connectionStringOptions.WriteConnection; if (_dbContext is SchoolDBContext) { var schoolDBContext = (SchoolDBContext)_dbContext; schoolDBContext.SetWriteOrReadConnectionString(writeConnectionString); } } private static int seed = 0;//种子 /// <summary> /// 设置读数据库连接字符串 /// </summary> private void SetReadConnectionString() {//随机策略--取得读的数据库链接字符串 //int connectionStringCount = _connectionStringsOptions.ReadConnectionList.Count; //int index = new Random().Next(0, connectionStringCount); //string readConnectionString = _connectionStringsOptions.ReadConnectionList[index]; //均衡策略---第1次index为0,第2次index为1,第3次index为0 // 第4次index为1,第5次index为0,第6次index为1 // 第7次index为0,第8次index为1,第9次index为0 // 0 % 2 = 0 1 % 2 = 1 2 % 2 = 0 // 3 % 2 = 1 4 % 2 = 0 5 % 2 = 1 // 6 % 2 = 0 7 % 2 = 1 8 % 2 = 0 int connectionStringCount = _connectionStringOptions.ReadConnectionList.Count; int index = seed++ % connectionStringCount; string readConnectionString = _connectionStringOptions.ReadConnectionList[index];//索引不是0就是1 if (_dbContext is SchoolDBContext) { var schoolDBContext = (SchoolDBContext)_dbContext; schoolDBContext.SetWriteOrReadConnectionString(readConnectionString); } } } }
八.SchoolDBContext
设置它自己的访问数据库的连接字符串。
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { if (!optionsBuilder.IsConfigured) { //这里写死了 //optionsBuilder.UseSqlServer("Server=meng\\MSSQLSERVERML;Database=SchoolDB;uid=sa;pwd=123456abc;Trusted_Connection=True;TrustServerCertificate=true"); //动态使用数据库链接字符串 optionsBuilder.UseSqlServer(connectionString); } } //数据库链接字符串 private string connectionString = string.Empty; /// <summary> /// 设置读或者写的数据库链接字符串 /// </summary> /// <param name="connString">链接字符串</param> public void SetWriteOrReadConnectionString(string connString) { connectionString = connString; }
九.程序入口Program中注册数据库上下文类(SchoolDBContext)、数据库上下文工厂类(DBContextFactory)、学生服务类(StudentService)
public class Program { public static void Main(string[] args) { var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddControllers(); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); #region 注册服务,以及数据库上下文工厂 { //注册数据库上下文类 builder.Services.AddScoped<DbContext, SchoolDBContext>(); //注注册数据库上下文工厂类-目的:为了修改数据库上下文类的数据库连接字符串 builder.Services.AddScoped<DBContextFactory, DBContextFactory>(); //注册学生服务 builder.Services.AddScoped<IStudentService, StudentService>(); } #endregion #region 注册配置 { //注册配置实例到哪个TOptions builder.Services.Configure<ConnectionStringOptions>(builder.Configuration.GetSection("ConnectionStringOptions")); } #endregion //添加跨越策略 builder.Services.AddCors(options => options.AddPolicy("any", policy => { //设定允许跨域的来源,有多个可以用','隔开 policy.WithOrigins("http://localhost:8080", "http://localhost:8080") .AllowAnyHeader()//允许任何标头 .AllowAnyMethod()//允许任何方法访问 .AllowCredentials();//允许凭据的策略 })); builder.Services.AddSwaggerGen(s => { s.SwaggerDoc("V1", new OpenApiInfo() { Title = "通用后台系统", Version = "Version-01", Description = "通用后台系统" }); var currentDirectory = AppContext.BaseDirectory; s.IncludeXmlComments($"{currentDirectory}/MengLin.DotNet7.WebAPI.xml"); }); var app = builder.Build(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(s => { s.SwaggerEndpoint("/swagger/V1/swagger.json", "test1"); }); } //使用跨越策略 app.UseCors("any"); app.UseHttpsRedirection(); app.UseAuthorization(); app.MapControllers(); app.Run(); } }
十.访问学生的控制器
在StudentService服务去做查询的时候,内部设置了下SchoolDBContext连接的字符串为读库的连接字符串。
namespace MengLin.DotNet7.WebAPI.Controllers { /// <summary> /// 学生 /// </summary> [Route("api/[controller]")] [ApiController] public class StudentsController : ControllerBase { /// <summary> /// 获取学生列表 /// </summary> /// <returns></returns> [HttpGet] [Route("{pageindex}/{pagesize}")] public RespResult Get(IStudentService studentService) { //从请求中获取路由数据 RouteValueDictionary dicRouteValue = HttpContext.Request.RouteValues; //url中的页索引和页大小 int.TryParse(dicRouteValue["pageindex"]?.ToString(),out int pageIndex); int.TryParse(dicRouteValue["pageSize"]?.ToString(), out int pageSize); var respResult = new RespResult<IQueryable<Student>>(); //查询满足条件的学生,且带分页 (IQueryable<Student> studentList,int totalCount) = studentService.Where<Student>(c => c.Sex == "女", pageIndex, pageSize); respResult.data = studentList;//结果集 respResult.TotalCount = totalCount;//记录总条数 return respResult; } } }
最后附上一张项目图