一個非侵入的Go事務管理庫——工作原理

在上一篇文章“一個非侵入的Go事務管理庫——如何使用”中,我講述了如何使用事務庫。有些讀者可能讀過”清晰架構(Clean Architecture)的Go微服務: 事物管理” ,其中描述了事務管理系統的舊版本。那篇文章和本文之間會有一些重疊。因為大多數人可能還沒有讀過那篇文章或者即使讀了也忘記了它的內容。因此為了照顧多數讀者,本文還是從頭開始(假設你沒有讀過前文)。如果你讀過,那你可以直接跳過熟悉的部分。

好的事務庫對於使用它的應用程序是透明的。在Go的“sql”庫中,有兩種類型的數據庫鏈接,“sql.DB”和“sql.Tx”。當你不需要事務支持時,使用“sql.DB”;否則使用“sql.Tx”。為了讓這兩種不同場景共享相同的持久層代碼,我們需要對數據庫鏈接進行一個封裝來同時支持這兩種場景。我從”db transaction in golang” 里得到了這個想法。

數據庫層的接口

數據庫層是事務管理庫中處理數據庫訪問的最低層。應用程序不需要修改該層,只有事務管理庫需要這樣做。

數據庫訪問封裝

下面是可同時支持事務和非事務操作的共享數據庫訪問接口, 它在“gdbc.go”中定義。

// SqlGdbc (SQL Go database connection) is a wrapper for SQL database handler ( can be *sql.DB or *sql.Tx)
// It should be able to work with all SQL data that follows SQL standard.
type SqlGdbc interface {
	Exec(query string, args ...interface{}) (sql.Result, error)
	Prepare(query string) (*sql.Stmt, error)
	Query(query string, args ...interface{}) (*sql.Rows, error)
	QueryRow(query string, args ...interface{}) *sql.Row
	// If need transaction support, add this interface
	Transactioner
}

// Transactioner is the transaction interface for database handler
// It should only be applicable to SQL database
type Transactioner interface {
	// Rollback a transaction
	Rollback() error
	// Commit a transaction
	Commit() error
	// TxEnd commits a transaction if no errors, otherwise rollback
	// txFunc is the operations wrapped in a transaction
	TxEnd(txFunc func() error) error

}

它有兩部分。一個是數據庫接口,它包含了常規的數據庫操作,如查詢表、更新表記錄。另一個事務接口,它包含里支持事務所需要的函數,如“提交”和“回滾”。“SqlGdbc”接口是兩者的結合。該接口將用於連接數據庫。

數據庫訪問接口的實現

下面是數據庫訪問接口的代碼實現。它在“sqlConnWrapper.go”文件中。它定義了兩個結構體,“SqlDBTx”是對“sql.DB”的封裝,將被非事務函數使用。“SqlConnTx”是對“sql.Tx”的封裝,將被事務函數使用。

// SqlDBTx is the concrete implementation of sqlGdbc by using *sql.DB
type SqlDBTx struct {
	DB *sql.DB
}

// SqlConnTx is the concrete implementation of sqlGdbc by using *sql.Tx
type SqlConnTx struct {
	DB *sql.Tx
}

func (sdt *SqlDBTx) Exec(query string, args ...interface{}) (sql.Result, error) {
	return sdt.DB.Exec(query, args...)
}

func (sdt *SqlDBTx) Prepare(query string) (*sql.Stmt, error) {
	return sdt.DB.Prepare(query)
}

func (sdt *SqlDBTx) Query(query string, args ...interface{}) (*sql.Rows, error) {
	return sdt.DB.Query(query, args...)
}

func (sdt *SqlDBTx) QueryRow(query string, args ...interface{}) *sql.Row {
	return sdt.DB.QueryRow(query, args...)
}

func (sdb *SqlConnTx) Exec(query string, args ...interface{}) (sql.Result, error) {
	return sdb.DB.Exec(query, args...)
}

func (sdb *SqlConnTx) Prepare(query string) (*sql.Stmt, error) {
	return sdb.DB.Prepare(query)
}

func (sdb *SqlConnTx) Query(query string, args ...interface{}) (*sql.Rows, error) {
	return sdb.DB.Query(query, args...)
}

func (sdb *SqlConnTx) QueryRow(query string, args ...interface{}) *sql.Row {
	return sdb.DB.QueryRow(query, args...)
}

事務接口的實現

下面是“Transactioner”接口的代碼實現,它在文件 “txConn.go”中。我從”database/sql Tx — detecting Commit or Rollback”中得到這個想法。

因為“SqlDBTx”不支持事務,所以它的所有函數都返回“nil”。

// DB doesn't rollback, do nothing here
func (cdt *SqlDBTx) Rollback() error {
	return nil
}

//DB doesnt commit, do nothing here
func (cdt *SqlDBTx) Commit() error {
	return nil
}

// DB doesnt rollback, do nothing here
func (cdt *SqlDBTx) TxEnd(txFunc func() error) error {
	return nil
}

func (sct *SqlConnTx) TxEnd(txFunc func() error) error {
	var err error
	tx := sct.DB

	defer func() {
		if p := recover(); p != nil {
			log.Println("found p and rollback:", p)
			tx.Rollback()
			panic(p) // re-throw panic after Rollback
		} else if err != nil {
			log.Println("found error and rollback:", err)
			tx.Rollback() // err is non-nil; don't change it
		} else {
			log.Println("commit:")
			err = tx.Commit() // if Commit returns error update err with commit err
		}
	}()
	err = txFunc()
	return err
}

func (sct *SqlConnTx) Rollback() error {
	return sct.DB.Rollback()
}

func (sct *SqlConnTx) Commit() error {
	return sct.DB.Commit()
}

持久層的接口

在數據庫層之上是持久層,應用程序使用持久層來訪問數據庫表中的記錄。你需要定義一個函數在本層中實現對事務的支持。下面是持久層的事務接口,它位於“txDataService.go”文件中。

// TxDataInterface represents operations needed for transaction support.
type TxDataInterface interface {
	// EnableTx is called at the end of a transaction and based on whether there is an error, it commits or rollback the
	// transaction.
	// txFunc is the business function wrapped in a transaction
	EnableTx(txFunc func() error) error
}

以下是它的實現代碼。它只是調用下層數據庫中的函數“TxEnd()”,該函數已在數據庫層實現。下面的代碼不是事務庫的代碼(它是本文中惟一的不是事務庫中的代碼),你需要在應用程序中實現它。

func (uds *UserDataSql) EnableTx(txFunc func() error) error {
	return uds.DB.TxEnd(txFunc)
}

獲取數據庫鏈接的代碼

除了我們上面描述的調用接口之外,在應用程序中你還需要先獲得數據庫鏈接。事務庫中有兩個函數可以完成這個任務。

返回”SqlGdbc”接口的函數

函數”Build()”(在”factory.go”中)將返回”SqlGdbc”接口。根據傳入的參數,它講返回滿足”SqlGdbc”接口的結構,如果需要事務支持就是“SqlConnTx”,不需要就是“SqlDBTx”。如果你不需要在應用程序中直接使用數據庫鏈接,那麼調用它是最好的。

// Build returns the SqlGdbc interface. This is the interface that you can use directly in your persistence layer
// If you don't need to cache sql.DB connection, you can call this function because you won't be able to get the sql.DB
// in SqlGdbc interface (if you need to do it, call BuildSqlDB()
func Build(dsc *config.DatabaseConfig) (gdbc.SqlGdbc, error) {
	db, err := sql.Open(dsc.DriverName, dsc.DataSourceName)
	if err != nil {
		return nil, errors.Wrap(err, "")
	}
	// check the connection
	err = db.Ping()
	if err != nil {
		return nil, errors.Wrap(err, "")
	}
	dt, err := buildGdbc(db, dsc)
	if err != nil {
		return nil, err
	}
	return dt, nil
}

func buildGdbc(sdb *sql.DB,dsc *config.DatabaseConfig) (gdbc.SqlGdbc, error){
	var sdt gdbc.SqlGdbc
	if dsc.Tx {
		tx, err := sdb.Begin()
		if err != nil {
			return nil, err
		}
		sdt = &gdbc.SqlConnTx{DB: tx}
		log.Println("buildGdbc(), create TX:")
	} else {
		sdt = &gdbc.SqlDBTx{sdb}
		log.Println("buildGdbc(), create DB:")
	}
	return sdt, nil
}

返回數據庫鏈接的函數

函數”BuildSqlDB()”(在”factory.go”中)將返回”sql.DB”。它會忽略傳入的事務標識參數。應用程序在調用這個函數獲得數據庫鏈接后,還需要根據事務標識自己生成“SqlConnTx”或“SqlDBTx”。如果你需要在應用程序里緩存”sql.DB”,那麼你必須調用這個函數。

// BuildSqlDB returns the sql.DB. The calling function need to generate corresponding gdbc.SqlGdbc struct based on
// sql.DB in order to use it in your persistence layer
// If you need to cache sql.DB connection, you need to call this function
func BuildSqlDB(dsc *config.DatabaseConfig) (*sql.DB, error) {
	db, err := sql.Open(dsc.DriverName, dsc.DataSourceName)
	if err != nil {
		return nil, errors.Wrap(err, "")
	}
	// check the connection
	err = db.Ping()
	if err != nil {
		return nil, errors.Wrap(err, "")
	}
	return db, nil

}

局限性

首先,它只支持SQL數據庫的事務。如果你有一個NoSql數據庫,那麼它不支持(大多數NoSql數據庫不支持事務)。

其次,如果你的事務跨越數據庫(例如在不同的微服務之間),那麼它將無法工作。常用的做法是使用“Saga Pattern”。你可以為事務中的每個操作編寫一個補償操作,並在回滾階段逐個執行補償操作。在應用程序中添加“Saga”解決方案並不困難。你可能會問,為什麼不把“Saga”加到事務庫中呢? 這是一個有趣的問題。我覺得還是單獨為“Saga”建一個庫比較合適。

第三,它不支持嵌套事務(Nested Transaction),因此你需要手動確保在代碼中沒有嵌套事務。如果代碼庫不是太複雜,這很容易做到。如果你有一個非常複雜的代碼庫,其中有很多事務和非事務代碼混在一起,那麼你需要一個支持嵌套事務的解決方案。我沒有花時間研究如何添加嵌套事務,但它應該有一定的工作量。如果你對此感興趣,可以從”database/sql: nested transaction or save point support”開始。到目前為止,對於大多數場景,當前的解決方案可能是在代價不大的情況下的最佳方案。

如何擴展庫的功能

“SqlGdbc”接口沒有列出“sql”包中的所有函數,只列出我的應用程序中需要的函數。你可以輕鬆地擴展該接口以包含其他函數。

例如,如果需要將全鏈路跟蹤(詳情請見”Go微服務全鏈路跟蹤詳解”)擴展到數據庫中,則可能需要在上下文中傳遞到數據庫函數中。“sql”庫已經支持具有上下文的數據庫函數。你只需要找到它們並將它們添加到”SqlGdbc”接口中,然後在”sqlConnWrapper “中實現它們。然後在持久層中,需要使用上下文作為參數調用函數。

源碼:

完整源碼: “jfeng45/gtransaction”

索引:

1 “一個非侵入的Go事務管理庫——如何使用”

2 “清晰架構(Clean Architecture)的Go微服務: 事物管理”

3 “db transaction in golang”

4 “database/sql Tx — detecting Commit or Rollback”

5 “Applying the Saga Pattern – GOTO Conference”

6 “database/sql: nested transaction or save point support”

7 “Go微服務全鏈路跟蹤詳解”

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】

※USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

※台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※幫你省時又省力,新北清潔一流服務好口碑

※回頭車貨運收費標準

.Net Core微服務入門全紀錄(六)——EventBus-事件總線

前言

上一篇【.Net Core微服務入門全紀錄(五)——Ocelot-API網關(下)】中已經完成了Ocelot + Consul的搭建,這一篇簡單說一下EventBus。

EventBus-事件總線

  • 首先,什麼是事件總線呢?

貼一段引用:

事件總線是對觀察者(發布-訂閱)模式的一種實現。它是一種集中式事件處理機制,允許不同的組件之間進行彼此通信而又不需要相互依賴,達到一種解耦的目的。

如果沒有接觸過EventBus,可能不太好理解。其實EventBus在客戶端開發中應用非常廣泛(android,ios,web前端等),用於多個組件(或者界面)之間的相互通信,懂的人都懂。。。

  • 那麼,我們為什麼要用EventBus呢?

就拿當前的項目舉例,我們有一個訂單服務,一個產品服務。客戶端有一個下單功能,當用戶下單時,調用訂單服務的下單接口,那麼下單接口需要調用產品服務的減庫存接口,這涉及到服務與服務之間的調用。那麼服務之間又怎麼調用呢?直接RESTAPI?或者效率更高的gRPC?可能這兩者各有各的使用場景,但是他們都存在一個服務之間的耦合問題,或者難以做到異步調用。

試想一下:假設我們下單時調用訂單服務,訂單服務需要調用產品服務,產品服務又要調用物流服務,物流服務再去調用xx服務 等等。。。如果每個服務處理時間需要2s,不使用異步的話,那這種體驗可想而知。

如果使用EventBus的話,那麼訂單服務只需要向EventBus發一個“下單事件”就可以了。產品服務會訂閱“下單事件”,當產品服務收到下單事件時,自己去減庫存就好了。這樣就避免了兩個服務之間直接調用的耦合性,並且真正做到了異步調用。

既然涉及到多個服務之間的異步調用,那麼就不得不提分佈式事務。分佈式事務並不是微服務獨有的問題,而是所有的分佈式系統都會存在的問題。
關於分佈式事務,可以查一下“CAP原則”和“BASE理論”了解更多。當今的分佈式系統更多的會追求事務的最終一致性。

下面使用國人開發的優秀項目“CAP”,來演示一下EventBus的基本使用。之所以使用“CAP”是因為它既能解決分佈式系統的最終一致性,同時又是一個EventBus,它具備EventBus的所有功能!
作者介紹:https://www.cnblogs.com/savorboard/p/cap.html

CAP使用

  • 環境準備

在Docker中準備一下需要的環境,首先是數據庫,數據庫我使用PostgreSQL,用別的也行。CAP支持:SqlServer,MySql,PostgreSql,MongoDB。
關於在Docker中運行PostgreSQL可以看我的另一篇博客:https://www.cnblogs.com/xhznl/p/13155054.html

然後是MQ,這裏我使用RabbitMQ,Kafka也可以。
Docker運行RabbitMQ:

docker pull rabbitmq:management
docker run -d -p 15672:15672 -p 5672:5672 --name rabbitmq rabbitmq:management

默認用戶:guest,密碼:guest

環境準備就完成了,Docker就是這麼方便。。。

  • 代碼修改:

為了模擬以上業務,需要修改大量代碼,下面代碼如有遺漏的直接去github找。

NuGet安裝:

Microsoft.EntityFrameworkCore
Microsoft.EntityFrameworkCore.Tools
Npgsql.EntityFrameworkCore.PostgreSQL

CAP相關:

DotNetCore.CAP
DotNetCore.CAP.RabbitMQ
DotNetCore.CAP.PostgreSql

Order.API/Controllers/OrdersController.cs增加下單接口:

[Route("[controller]")]
[ApiController]
public class OrdersController : ControllerBase
{
    private readonly ILogger<OrdersController> _logger;
    private readonly IConfiguration _configuration;
    private readonly ICapPublisher _capBus;
    private readonly OrderContext _context;

    public OrdersController(ILogger<OrdersController> logger, IConfiguration configuration, ICapPublisher capPublisher, OrderContext context)
    {
        _logger = logger;
        _configuration = configuration;
        _capBus = capPublisher;
        _context = context;
    }

    [HttpGet]
    public IActionResult Get()
    {
        string result = $"【訂單服務】{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}——" +
            $"{Request.HttpContext.Connection.LocalIpAddress}:{_configuration["ConsulSetting:ServicePort"]}";
        return Ok(result);
    }

    /// <summary>
    /// 下單 發布下單事件
    /// </summary>
    /// <param name="order"></param>
    /// <returns></returns>
    [Route("Create")]
    [HttpPost]
    public async Task<IActionResult> CreateOrder(Models.Order order)
    {
        using (var trans = _context.Database.BeginTransaction(_capBus, autoCommit: true))
        {
            //業務代碼
            order.CreateTime = DateTime.Now;
            _context.Orders.Add(order);

            var r = await _context.SaveChangesAsync() > 0;

            if (r)
            {
                //發布下單事件
                await _capBus.PublishAsync("order.services.createorder", new CreateOrderMessageDto() { Count = order.Count, ProductID = order.ProductID });
                return Ok();
            }
            return BadRequest();
        }

    }

}

Order.API/MessageDto/CreateOrderMessageDto.cs:

/// <summary>
/// 下單事件消息
/// </summary>
public class CreateOrderMessageDto
{
    /// <summary>
    /// 產品ID
    /// </summary>
    public int ProductID { get; set; }

    /// <summary>
    /// 購買數量
    /// </summary>
    public int Count { get; set; }
}

Order.API/Models/Order.cs訂單實體類:

public class Order
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int ID { get; set; }

    /// <summary>
    /// 下單時間
    /// </summary>
    [Required]
    public DateTime CreateTime { get; set; }

    /// <summary>
    /// 產品ID
    /// </summary>
    [Required]
    public int ProductID { get; set; }

    /// <summary>
    /// 購買數量
    /// </summary>
    [Required]
    public int Count { get; set; }
}

Order.API/Models/OrderContext.cs數據庫Context:

public class OrderContext : DbContext
{
    public OrderContext(DbContextOptions<OrderContext> options)
       : base(options)
    {

    }

    public DbSet<Order> Orders { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {

    }
}

Order.API/appsettings.json增加數據庫連接字符串:

"ConnectionStrings": {
  "OrderContext": "User ID=postgres;Password=pg123456;Host=host.docker.internal;Port=5432;Database=Order;Pooling=true;"
}

Order.API/Startup.cs修改ConfigureServices方法,添加Cap配置:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    services.AddDbContext<OrderContext>(opt => opt.UseNpgsql(Configuration.GetConnectionString("OrderContext")));

    //CAP
    services.AddCap(x =>
    {
        x.UseEntityFramework<OrderContext>();

        x.UseRabbitMQ("host.docker.internal");
    });
}

以上是訂單服務的修改。

Product.API/Controllers/ProductsController.cs增加減庫存接口:

[Route("[controller]")]
[ApiController]
public class ProductsController : ControllerBase
{
    private readonly ILogger<ProductsController> _logger;
    private readonly IConfiguration _configuration;
    private readonly ICapPublisher _capBus;
    private readonly ProductContext _context;

    public ProductsController(ILogger<ProductsController> logger, IConfiguration configuration, ICapPublisher capPublisher, ProductContext context)
    {
        _logger = logger;
        _configuration = configuration;
        _capBus = capPublisher;
        _context = context;
    }

    [HttpGet]
    public IActionResult Get()
    {
        string result = $"【產品服務】{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}——" +
            $"{Request.HttpContext.Connection.LocalIpAddress}:{_configuration["ConsulSetting:ServicePort"]}";
        return Ok(result);
    }

    /// <summary>
    /// 減庫存 訂閱下單事件
    /// </summary>
    /// <param name="message"></param>
    /// <returns></returns>
    [NonAction]
    [CapSubscribe("order.services.createorder")]
    public async Task ReduceStock(CreateOrderMessageDto message)
    {
        //業務代碼
        var product = await _context.Products.FirstOrDefaultAsync(p => p.ID == message.ProductID);
        product.Stock -= message.Count;

        await _context.SaveChangesAsync();
    }

}

Product.API/MessageDto/CreateOrderMessageDto.cs:

/// <summary>
/// 下單事件消息
/// </summary>
public class CreateOrderMessageDto
{
    /// <summary>
    /// 產品ID
    /// </summary>
    public int ProductID { get; set; }

    /// <summary>
    /// 購買數量
    /// </summary>
    public int Count { get; set; }
}

Product.API/Models/Product.cs產品實體類:

public class Product
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int ID { get; set; }

    /// <summary>
    /// 產品名稱
    /// </summary>
    [Required]
    [Column(TypeName = "VARCHAR(16)")]
    public string Name { get; set; }

    /// <summary>
    /// 庫存
    /// </summary>
    [Required]
    public int Stock { get; set; }
}

Product.API/Models/ProductContext.cs數據庫Context:

public class ProductContext : DbContext
{
    public ProductContext(DbContextOptions<ProductContext> options)
       : base(options)
    {

    }

    public DbSet<Product> Products { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
        
        //初始化種子數據
        modelBuilder.Entity<Product>().HasData(new Product
        {
            ID = 1,
            Name = "產品1",
            Stock = 100
        },
        new Product
        {
            ID = 2,
            Name = "產品2",
            Stock = 100
        });
    }
}

Product.API/appsettings.json增加數據庫連接字符串:

"ConnectionStrings": {
  "ProductContext": "User ID=postgres;Password=pg123456;Host=host.docker.internal;Port=5432;Database=Product;Pooling=true;"
}

Product.API/Startup.cs修改ConfigureServices方法,添加Cap配置:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    services.AddDbContext<ProductContext>(opt => opt.UseNpgsql(Configuration.GetConnectionString("ProductContext")));

    //CAP
    services.AddCap(x =>
    {
        x.UseEntityFramework<ProductContext>();

        x.UseRabbitMQ("host.docker.internal");
    });
}

以上是產品服務的修改。

訂單服務和產品服務的修改到此就完成了,看着修改很多,其實功能很簡單。就是各自增加了自己的數據庫表,然後訂單服務增加了下單接口,下單接口會發出“下單事件”。產品服務增加了減庫存接口,減庫存接口會訂閱“下單事件”。然後客戶端調用下單接口下單時,產品服務會減去相應的庫存,功能就這麼簡單。

關於EF數據庫遷移之類的基本使用就不介紹了。使用Docker重新構建鏡像,運行訂單服務,產品服務:

docker build -t orderapi:1.1 -f ./Order.API/Dockerfile .
docker run -d -p 9060:80 --name orderservice orderapi:1.1 --ConsulSetting:ServicePort="9060"
docker run -d -p 9061:80 --name orderservice1 orderapi:1.1 --ConsulSetting:ServicePort="9061"
docker run -d -p 9062:80 --name orderservice2 orderapi:1.1 --ConsulSetting:ServicePort="9062"

docker build -t productapi:1.1 -f ./Product.API/Dockerfile .
docker run -d -p 9050:80 --name productservice productapi:1.1 --ConsulSetting:ServicePort="9050"
docker run -d -p 9051:80 --name productservice1 productapi:1.1 --ConsulSetting:ServicePort="9051"
docker run -d -p 9052:80 --name productservice2 productapi:1.1 --ConsulSetting:ServicePort="9052"

最後 Ocelot.APIGateway/ocelot.json 增加一條路由配置:

好了,進行到這裏,整個環境就有點複雜了。確保我們的PostgreSQL,RabbitMQ,Consul,Gateway,服務實例都正常運行。

服務實例運行成功后,數據庫應該是這樣的:

產品表種子數據:

cap.published表和cap.received表是由CAP自動生成的,它內部是使用本地消息表+MQ來實現異步確保。

運行測試

這次使用Postman作為客戶端調用下單接口(9070是之前的Ocelot網關端口):

訂單庫published表:

訂單庫order表:

產品庫received表:

產品庫product表:

再試一下:

OK,完成。雖然功能很簡單,但是我們實現了服務的解耦,異步調用,和最終一致性。

總結

注意,上面的例子純粹是為了說明EventBus的使用,實際中的下單流程絕對不會這麼做的!希望大家不要較真。。。

可能有人會說如果下單成功,但是庫存不足導致減庫存失敗了怎麼辦,是不是要回滾訂單表的數據?如果產生這種想法,說明還沒有真正理解最終一致性的思想。首先下單前肯定會檢查一下庫存數量,既然允許下單那麼必然是庫存充足的。這裏的事務是指:訂單保存到數據庫,和下單事件保存到cap.published表(保存到cap.published表理論上就能夠發送到MQ)這兩件事情,要麼一同成功,要麼一同失敗。如果這個事務成功,那麼就可以認為這個業務流程是成功的,至於產品服務的減庫存是否成功那就是產品服務的事情了(理論上也應該是成功的,因為消息已經確保發到了MQ,產品服務必然會收到消息),CAP也提供了失敗重試,和失敗回調機制。

如果非要數據回滾也是能實現的,CAP的ICapPublisher.Publish方法提供一個callbackName參數,當減庫存時,可以觸發這個回調。其本質也是通過發布訂閱完成,這是不推薦的做法,就不詳細說了,有興趣自己研究一下。
另外,CAP無法保證消息不重複,實際使用中需要自己考慮一下消息的重複過濾和冪等性。

這一篇內容有點多,不知道有沒有表達清楚,有問題歡迎評論交流,如有不對之處還望大家指出。

下一篇計劃寫一下授權認證相關的內容。

代碼放在:https://github.com/xiajingren/NetCoreMicroserviceDemo

未完待續…

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

※南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

面試官:十問泛型,你能扛住嗎?

問題一:為什麼需要泛型?

答:

使用泛型機制編寫的代碼要比那些雜亂的使用Object變量,然後再進行強制類型轉換的代碼具有更好的安全性和可讀性,也就是說使用泛型機制編寫的代碼可以被很多不同類型的對象所重用。

問題二:從ArrayList的角度說一下為什麼要用泛型?

答:

在Java增加泛型機制之前就已經有一個ArrayList類,這個ArrayList類的泛型概念是使用繼承來實現的。

public class ArrayList {
    private Object[] elementData;
    public Object get(int i) {....}
    public void add(Object o) {....}
}

這個類存在兩個問題:

  1. 當獲取一個值的時候必須進行強制類型轉換
  2. 沒有錯誤檢查,可以向數組中添加任何類的對象
ArrayList files = new ArrayList();
files.add(new File(""));
String filename = (String)files.get(0);

對於這個調用,編譯和運行都不會出錯,但是當我們在其他地方使用get方法獲取剛剛存入的這個File對象強轉為String類型的時候就會產生一個錯誤。

泛型對於這種問題的解決方案是提供一個類型參數。

ArrayList<String> files = new ArrayList<>();

這樣可以使代碼具有更好的可讀性,我們一看就知道這個數據列表中包含的是String對象。 編譯器也可以很好地利用這個信息,當我們調用get的時候,不需要再使用強制類型轉換,編譯器就知道返回值類型為String,而不是Object:

String filename = files.get(0);

編譯器還知道ArrayList<String>中add方法中有一個類型為String的參數。這將比使用Object類型的參數安全一些,現在編譯器可以檢查,避免插入錯誤類型的對象:

files.add(new File(""));

這樣的代碼是無法通過編譯的,出現編譯錯誤比類在運行時出現類的強制類型轉換異常要好得多。

問題三:說說泛型類吧

一個泛型類就是具有一個或多個類型變量的類,對於這個類來說,我們只關注泛型,而不會為數據存儲的細節煩惱。

public class Couple<T> {
   private T one;
   private T two;
}

Singer類引入了一個類型變量T,用尖括號括起來,並放在類名的後面。泛型類可以有多個類型變量:

public class Couple<T, U> {...}

類定義中的類型變量是指定方法的返回類型以及域和局部變量的類型

//域
private T one;
//返回類型
public T getOne() { return one; }
//局部變量
public void setOne(T newValue) { one = newValue; }

使用具體的類型代替類型變量就可以實例化泛型類型:

Couple<Rapper>

泛型類可以看成是普通類的工廠,打個比方:我用泛型造了一個模型,具體填充什麼樣的材質,由使用者去做決定。

問題四: 說說泛型方法的定義和使用

答:

泛型方法可以定義在普通類中,也可以定義在泛型類中,類型變量是放在修飾符的後面,返回類型的前面。

我們來看一個泛型方法的實例:

class ArrayUtil {

    public static <T> T getMiddle(T...a){
        return a[a.length / 2];
    }
}

當調用一個泛型方法時,在方法名前的尖括號中放入具體的類型:

String middle = ArrayUtil.<String>getMiddle("a","b","c");

在這種情況下,方法調用中可以省略<String>類型參數,編譯器會使用類型推斷來推斷出所調用的方法,也就是說可以這麼寫:

String middle = ArrayAlg.getMiddle("a","b","c");

問題五:E V T K ? 這些是什麼

答:

  • E——Element 表示元素 特性是一種枚舉
  • T——Type 類,是指Java類型
  • K—— Key 鍵
  • V——Value 值
  • ?——在使用中表示不確定類型

問題六:了解過類型變量的限定嗎?

答:

一個類型變量或通配符可以有多個限定,例如:

<T extends Serializable & Cloneable>

單個類型變量的多個限定類型使用&分隔,而,用來分隔多個類型變量。

<T extends Serializable,Cloneable>

在類型變量的繼承中,可以根據需要擁有多個接口超類型,但是限定中至多有一個類。如果用一個類作為限定,它必定是限定列表中的第一個。

類型變量的限定是為了限制泛型的行為,指定了只有實現了特定接口的類才可以作為類型變量去實例化一個類。

問題七:泛型與繼承你知道多少?

答:

首先,我們來看一個類和它的子類,比如 Singer 和 Rapper。但是Couple<Rapper>卻並不是Couple<Singer>的一個子類。

無論S和T有什麼聯繫,Couple<S>與Couple<T>沒有什麼聯繫。

這裏需要注意泛型和Java數組之間的區別,可以將一個Rapper[]數組賦給一個類型為Singer[]的變量:

Rapper[] rappers = ...;
Singer[] singer = rappers;

然而,數組帶有特別的保護,如果試圖將一個超類存儲到一個子類數組中,虛擬機會拋出ArrayStoreException異常。

問題八:聊聊通配符吧

答:

通配符類型中,允許類型參數變化。比如,通配符類型:

Couple<? extends Singer>

表示任何泛型類型,它的類型參數是Singer的子類,如Couple<Rapper>,但不會是Couple<Dancer>。

假如現在我們需要編寫一個方法去打印一些東西:

public static void printCps(Couple<Rapper> cps) {
      Rapper one = cp.getOne();
      Rapper two = cp.getTwo();
      System.out.println(one.getName() + " & " + two.getName() + " are cps.");
}

正如前面所講到的,不能將Couple<Rapper>傳遞給這個方法,這一點很受限制。解決的方案很簡單,使用通配符類型:

public static void printCps(Couple< ? extends Singer> cps) 

Couple<Rapper>是Couple< ? extends Singer>的子類型。

我們接下來來考慮另外一個問題,使用通配符會通過Couple< ? extends Singer>的引用破壞Couple<Rapper>嗎?

Couple<Rapper> rapper = new Couple<>(rapper1, rapper2);
Couple<? extends Singer> singer = rapper;
player.setOne(reader);

這樣可能會引起破壞,但是當我們調用setOne的時候,如果調用的不是Singer的子類Rapper類的對象,而是其他Singer子類的對象,就會出錯。 我們來看一下Couple<? extends Singer>的方法:

? extends Singer getOne();
void setOne(? extends Singer);

這樣就會看的很明顯,因為如果我們去調用setOne()方法,編譯器之可以知道是某個Singer的子類型,而不能確定具體是什麼類型,它拒絕傳遞任何特定的類型,因為 ? 不能用來匹配。 但是使用getOne就不存在這個問題,因為我們無需care它獲取到的類型是什麼,但一定是Singer的子類。

通配符限定與類型變量限定非常相似,但是通配符類型還有一個附加的能力,即可以指定一個超類型限定:

? super Rapper

這個通配符限製為Rapper的所有父類,為什麼要這麼做呢?帶有超類型限定的通配符的行為與子類型限定的通配符行為完全相反,可以為方法提供參數,但是卻不能獲取具體的值,即訪問器是不安全的,而更改器方法是安全的:

編譯器無法知道setOne方法的具體類型,因此調用這個方法時不能接收類型為Singer或Object的參數。只能傳遞Rapper類型的對象,或者某個子類型(Reader)對象。而且,如果調用getOne,不能保證返回對象的類型。

總結一下:

帶有超類型限定的通配符可以向泛型對象寫入,帶有子類型限定的通配符可以從泛型對象讀取。

問題九:泛型在虛擬機中是什麼樣呢?

答:

  1. 虛擬機沒有泛型類型對象,所有的對象都屬於普通類。 無論何時定義一個泛型類型,都自動提供了一個相應的原始類型。原始類型的名字就是刪去類型參數后的泛型類型名。擦除類型變量,並替換成限定類型(沒有限定的變量用Object)。這樣做的目的是為了讓非泛型的Java程序在後續支持泛型的 jvm 上還可以運行(向後兼容)

  2. 當程序調用泛型方法時,如果擦除返回類型,編譯器插入強制類型轉換。

Couple<Singer> cps = ...;
Singer one = cp.getOne();

擦除cp.getOne的返回類型后將返回Object類型。編譯器自動插入Singer的強制類型轉換。也就是說,編譯器把這個方法調用編譯為兩條虛擬機指令:

對原始方法cp.getOne的調用 將返回的Object類型強制轉換為Singer類型。

  1. 當存取一個公有泛型域時也要插入強制類型轉換。
//我們寫的代碼
Singer one = cps.one;
//編譯器做的事情
Singer one = (Singer)cps.one;

問題十:關於泛型擦除,你知道多少?

答:

類型擦除會出現在泛型方法中,程序員通常認為下述的泛型方法

public static <T extends Comparable> T min(T[] a)

是一個完整的方法族,而擦除類型之後,只剩下一個方法:

public static Comparable min(Comparable[] a)

這個時候類型參數T已經被擦除了,只留下了限定類型Comparable。

但是方法的擦除會帶來一些問題:

class Coupling extends Couple<People> {
    public void setTwo(People people) {
            super.setTwo(people);
    }
}

擦除后:

class Coupling extends Couple {
    public void setTwo(People People) {...}
}

這時,問題出現了,存在另一個從Couple類繼承的setTwo方法,即:

public void setTwo(Object two)

這顯然是一個不同的方法,因為它有一個不同類型的參數(Object),而不是People。

Coupling coupling = new Coupling(...);
Couple<People> cp = interval;
cp.setTwo(people);

這裏,希望對setTwo的調用具有多態性,並調用最合適的那個方法。由於cp引用Coupling對象,所以應該調用Coupling.setTwo。問題在於類型擦除與多態發生了衝突。要解決這個問題,就需要編譯器在Coupling類中生成一個橋方法:

public void setTwo(Object second) {
    setTwo((People)second);
}

變量cp已經聲明為類型Couple<LocalDate>,並且這個類型只有一個簡單的方法叫setTwo,即setTwo(Object)。虛擬機用cp引用的對象調用這個方法。這個對象是Coupling類型的,所以會調用Coupling.setTwo(Object)方法。這個方法是合成的橋方法。它會調用Coupling.setTwo(Date),這也正是我們所期望的結果。

所以,我們要記住關於Java泛型轉換的幾個點:

  1. 虛擬機中沒有泛型,只有普通的類和方法
  2. 所有的類型參數都用它們的限定類型替換
  3. 橋方法被合成來保持多態
  4. 為保持類型安全性,必要時插入強制類型轉換

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

※網頁設計最專業,超強功能平台可客製化

再看rabbitmq的交換器和隊列的關係

最近又要用到rabbitmq,業務上要求服務器只發一次消息,需要多個客戶端都去單獨消費。但我們知道rabbitmq的機制里,每個隊列里的消息只能消費一次,所以客戶端要單獨消費信息,就必須得每個客戶端單獨監聽一個queue。所以我最終想實現的是服務端只聲明exchange,客戶端來創建queue和綁定exchange。但是在看各種rabbitmq博文和討論的時候,我覺得對exchange的模式和queue間的關係講的都不是很清楚。所以我決定自己驗證一下

fanout模式和direct模式

本文主要驗證fanout模式和direct模式下以上猜想是否可行。fanout模式就是大名鼎鼎的廣播模式了,只要queue綁定了fanout的交換器,就可以直接的收到消息,無需routingkey的參与。而direct模式就是通過routing key直接發送到綁定了同樣routing key的隊列中。那麼,在這兩種exchange的模式下,是否都可以實現服務端僅創建exchange,客戶端創建queue並綁定exchange呢?

Direct模式驗證

我們先把交換器、routingkey、隊列的名稱定義好:

  1. 交換器為directTest
  2. routingkey為direct_routing_key
  3. 隊列測試3個,首先測試Direct_test_queue_1,再行測試Direct_test_queue_2,再行測試Direct_test_queue_3

代碼使用spring boot框架快速搭建。我們先規劃好需要幾個類來完成這個事情:

  1. 針對生產者,需要RabbitmqConfig,用來配置exchange的
  2. 針對生產者,需要DirectRabbitSender,用來實現Direct模式的消息發送
  3. 針對消費者,需要DirectConsumerOne,來測試第一個隊列Direct_test_queue_1生成和消息接收
  4. 針對消費者,需要DirectConsumerTwo,來測試第二個隊列Direct_test_queue_2生成和消息接收
  5. 針對消費者,需要DirectConsumerThree,來測試第三個隊列Direct_test_queue_3生成和消息接收
  6. 我們還需要一個測試類RabbitmqApplicationTests,用於測試消息的發送和接收

rabbitmq先配置一個DirectExchange

@Bean
DirectExchange directExchange(){
    return new DirectExchange("directTest", true, false);
}

我們可以看到Direct交換器的名稱定義為了directTest,這時候還未綁定任何的隊列。啟動程序,若我們的設想沒錯,則rabbitmq中應該已經生成了directTest的exchange。

Bingo!directTest交換器成功創建。接下來,我們去編寫DirectRabbitSender的代碼

@Component
public class DirectRabbitSender{

    @Autowired
    private RabbitTemplate rabbitTemplate;

    private final String EXCHANGE_NAME = "directTest";
    private final String ROUTING_KEY = "direct_routing_key";

    public void send(Object message) {
        rabbitTemplate.convertAndSend(EXCHANGE_NAME, ROUTING_KEY, message);
    }

}

我們可以看到代碼中,通過rabbitTemplate發送消息到了交換器為directTest,routingkey為direct_routing_key的地方。但這時候我們沒有任何隊列了,自然接不到消息。現在我們去編寫第一個消費者DirectConsumerOne來接受消息。

@Component
@RabbitListener(bindings = @QueueBinding(
        value = @Queue(value = "Direct_test_queue_1", durable = "true"),
        exchange = @Exchange(value = "directTest"),
        key = "direct_routing_key"
))
public class DirectConsumerOne {

    @RabbitHandler
    private void onMessage(String message){
        System.out.println("監聽隊列Direct_test_queue_1接到消息" + message);
    }

}

通過代碼可以看到,我們通過@QueueBinding把Direct_test_queue_1隊列綁定到了directTest和direct_routing_key上。Direct_test_queue_1並沒有在rabbitmq創建,這並沒有關係。一般來說,@RabbitListener會自動去創建隊列。啟動程序,我們去看一下rabbitmq里隊列是不是創建了。

Bingo!再次驗證成功。我們去看看綁定關係是不是正確。這時候Direct_test_queue_1應該綁定到了名為directTest的交換器,而綁定的routingkey為direct_routing_key

biubiubiu!綁定關係完全正確。到了這裏,我們進行最後一步,寫了單元測試去發送消息,查看控制台中消費者是否成功收到消息。RabbitmqApplicationTests的代碼如下:

@SpringBootTest
class RabbitmqApplicationTests {

    @Autowired
    private DirectRabbitSender directRabbitSender;

    @Test
    void contextLoads() {
    }

    @Test
    public void directSendTest(){
        directRabbitSender.send("direct-sender");
        directRabbitSender.send("direct-sender_test");
    }

}

啟動測試類,然後去查看控制台。

沒錯,這就是我們想要達到的效果!基本可以宣布Direct模式驗證成功。服務端生成exchange,客戶端去生成隊列綁定的方式在direct模式下完全可行。為了保險起見,再驗證一下生成多個消費者綁定到同一個隊列是否可行。

DirectConsumerTwo代碼如下:

@Component
@RabbitListener(bindings = @QueueBinding(
        value = @Queue(value = "Direct_test_queue_2", durable = "true"),
        exchange = @Exchange(value = "directTest"),
        key = "direct_routing_key"
))
public class DirectConsumerTwo {

    @RabbitHandler
    private void onMessage(String message){
        System.out.println("監聽隊列Direct_test_queue_2接到消息" + message);
    }

}

DirectConsumerThree代碼如下:

@Component
@RabbitListener(bindings = @QueueBinding(
        value = @Queue(value = "Direct_test_queue_3", durable = "true"),
        exchange = @Exchange(value = "directTest"),
        key = "direct_routing_key"
))
public class DirectConsumerThree {

    @RabbitHandler
    private void onMessage(String message){
        System.out.println("監聽隊列Direct_test_queue_3接到消息" + message);
    }

}

啟動測試類,我們去看兩個地方:

  1. rabbitmq是否創建了客戶端綁定的三個隊列Direct_test_queue_1、Direct_test_queue_2、Direct_test_queue_3
  2. 消費者應該各自收到2條消息(Test中發送了兩條,參看上面 RabbitmqApplicationTests 的代碼)。那3個隊列,控制台中應該打印了6條消息。

hohohoho!創建成功,並且綁定關係我看了也全都正確。我們去看控制台

6條!沒有任何毛病,至此,可以宣布Direct模式下,完全支持我們最初的想法:服務端生成exchange,客戶端去生成隊列綁定的方式在direct模式下完全可行。

fanout模式驗證

接下來我們驗證一下fanout的方式,基本操作流程和Direct模式一致。代碼的結構也差不多:

  1. 針對生產者,需要RabbitmqConfig,直接在Direct模式下的rabbitmqConfig里直接添加Fanout的交換器配置
  2. 針對生產者,需要FanoutRabbitSender,用來實現Fanout模式的消息發送
  3. 針對消費者,需要FanoutConsumerOne,來測試第一個隊列Fanout_test_queue_1生成和消息接收
  4. 針對消費者,需要FanoutConsumerTwo,來測試第二個隊列Fanout_test_queue_2生成和消息接收
  5. 針對消費者,需要FanoutConsumerThree,來測試第三個隊列Fanout_test_queue_3生成和消息接收
  6. 測試類RabbitmqApplicationTests也直接復用Direact模式下測試的類

我就不多BB,直接上代碼了。

RabbitmqConfig代碼如下

@Configuration
public class RabbitmqConfig {

    @Bean
    DirectExchange directExchange(){
        return new DirectExchange("directTest", true, false);
    }

    @Bean
    FanoutExchange fanoutExchange(){
        return new FanoutExchange("fanoutTest", true, false);
    }

}

FanoutRabbitSender的代碼如下,此處和direct模式的區別是Fanout中沒有routingkey,所以代碼里也沒定義routingkey:

@Component
public class FanoutRabbitSender{

    @Autowired
    private RabbitTemplate rabbitTemplate;

    private final String EXCHANGE_NAME = "fanoutTest";

    public void send(Object message) {
        rabbitTemplate.convertAndSend(EXCHANGE_NAME, null, message);
    }

}

我們到這裏先啟動程序試試,看看fanoutTest的交換器在沒有綁定隊列的情況下是否生成了。

棒棒棒!和我們想的一樣,那接下來去寫完所有的消費者,這裏和Direct模式最重要的區別是@Exchange中必須要指定type為fanout。direct模式的代碼里沒指定是因為@Exchange的type默認值就是direct。我直接上代碼了:

/**
 * 監聽器主動去聲明queue=fanout_test_queue_1,並綁定到fanoutTest交換器
 */
@Component
@RabbitListener(bindings = @QueueBinding(
        value = @Queue(value = "fanout_test_queue_1", durable = "true"),
        exchange = @Exchange(value = "fanoutTest", type = ExchangeTypes.FANOUT)
))
public class FanoutConsumerOne {

    @RabbitHandler
    private void onMessage(String message){
        System.out.println("監聽隊列fanout_test_queue_1接到消息" + message);
    }

}

@Component
@RabbitListener(bindings = @QueueBinding(
        value = @Queue(value = "fanout_test_queue_2", durable = "true"),
        exchange = @Exchange(value = "fanoutTest", type = ExchangeTypes.FANOUT)
))
public class FanoutConsumerTwo {

    @RabbitHandler
    private void onMessage(String message){
        System.out.println("監聽隊列fanout_test_queue_2接到消息" + message);
    }

}

@Component
@RabbitListener(bindings = @QueueBinding(
        value = @Queue(value = "fanout_test_queue_3", durable = "true"),
        exchange = @Exchange(value = "fanoutTest", type = ExchangeTypes.FANOUT)
))
public class FanoutConsumerThree {

    @RabbitHandler
    private void onMessage(String message){
        System.out.println("監聽隊列fanout_test_queue_3接到消息" + message);
    }

}

接着去測試類RabbitmqApplicationTests中加上fanout的發送測試,然後註釋掉direct的單元測試,以便一會造成干擾

@SpringBootTest
class RabbitmqApplicationTests {

    @Autowired
    private DirectRabbitSender directRabbitSender;

    @Autowired
    private FanoutRabbitSender fanoutRabbitSender;

    @Test
    void contextLoads() {
    }

//    @Test
//    public void directSendTest(){
//        directRabbitSender.send("direct-sender");
//        directRabbitSender.send("direct-sender_test");
//    }

    @Test
    public void fanoutSendTest(){
        fanoutRabbitSender.send("fanout-sender_1");
        fanoutRabbitSender.send("fanout-sender_2");
    }

}

代碼都完成了,現在我們啟動測試類,看看控制台是否正常收到了消息

看圖看圖,fanout模式下也完全認證成功!!!那我們可以宣布,文章開頭的猜想完全可以實現。

總結

服務端只聲明exchange,客戶端來創建queue和綁定exchange的方式完全可行。並且在Direct和Fanout模式下都可行。

那我們可以推測在Header模式的交換器和Topic模式的交換器下應該也大差不差。具體各位可自行驗證,基本流程和上面direct和fanout的流程差不多。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※南投搬家公司費用需注意的眉眉角角,別等搬了再說!

※新北清潔公司,居家、辦公、裝潢細清專業服務

※教你寫出一流的銷售文案?

MongoDB設計方法及技巧

MongoDB是一種流行的數據庫,可以在不受任何錶格schema模式的約束下工作。數據以類似JSON的格式存儲,並且可以包含不同類型的數據結構。例如,在同一集合collection 中,我們可以擁有以下兩個文檔document:

{
    id: '4',
    name: 'Mark',
    age: '21',
    addresses : [
        { street: '123 Church St', city: 'Miami', cc: 'USA' },
        { street: '123 Mary Av', city: 'Los Angeles', cc: 'USA' }
    ]
}

{
    id: '15',
    name: 'Robin',
    department: 'New Business',
    example: 'robin@example.com'
}

為了能夠充分利用MongoDB的優勢,您必須了解並遵循一些基本的數據庫設計原則。在講解設計方法之前,我們必須首先了解MongoDB存儲數據的結構。

一、 數據如何存儲在MongoDB中

與傳統的RDBMS關係型數據庫不同,MongoDB並沒有表Table,行row和列column的概念。它將數據存儲在集合collections,文檔documents和字段fields中。下圖說明了與RDBMS類比的結構之間的關係:

二、數據庫設計技巧和竅門

2.1.規範化存儲與非規範化存儲

因為MongoDB使用文檔來存儲數據,所以理解“規範化存儲“”和“非規範化存儲”的概念非常重要。

規範化存儲:-規範化意味着將數據存儲到多個集合collections中,並在它們之間設計關聯關係。數據保存之後,更新數據比較容易。但是在讀取數據的時候,規範化存儲的缺點就顯現出來。如果要從多個集合collections查找數據,則必須執行多個查詢,從而使讀取數據的速度變慢。 (比如:將網頁標題、作者、內容分別存儲到不同的collections中)

非規範化存儲:-這種方式將若干對象數據,以嵌套的方式存儲到單個文檔中。它在讀取數據的時候表現更好,但在寫入時會變慢。這種存儲數據的方式還將佔用更多空間。 (比如:將網頁標題、作者、內容分別存儲到同一個collection中)

所以在兩種存儲數據方式之間進行選擇之前,先評估一下你的應用數據庫的使用方式。

  • 如果您有一個不需要頻繁更新的數據,更新的即時一致性不是很重要,但是在讀取時需要良好的性能,那麼非規範化可能是明智的選擇。(比如:我們博客的博文,作者一旦保存之後,幾乎就不在進行頻繁的修改,但是面臨着讀者頻繁的讀取閱讀操作)

  • 如果數據庫中的文檔數據需要不斷的更新,並且您希望在寫入時具有良好的性能,那麼您可能需要考慮規範化存儲。(比如:需要頻繁修改數據的業務類系統)

2.2. 一對多關係

與RDBMS相比,在MongoDB中對“一對多”關係建模需要進行更細粒度的設計。許多初學者陷入將文檔數組嵌入父文檔中的陷阱。正如我們在上文中介紹的,知道何時進行規範化存儲或非規範化存儲是非常重要的。因此設計者需要考慮關係的基數是“一個對少數幾個”還是“一個對多個”?每種關係將具有不同的建模方法。

例如:下面“一個對少數幾個”的建模示例。最好的建模方法是在父文檔(persopn)中嵌入幾個(address):

> db.person.findOne()
{
  name: 'Mark Kornfield',
  ssn: '1223-234-75554',
  addresses : [
     { street: '123 Church St', city: 'Miami', cc: 'USA' },
     { street: '123 Mary Av', city: 'Los Angeles', cc: 'USA' }
  ]
}

在“一個對多個”示例中,我們將考慮設計兩個集合,即產品products集合和零件parts集合。每個零件都有一個“ ObjectID”,該“ ObjectID”將出現在產品集合的引用中。這樣的設計可以讓讀寫性能更高效。

> db.parts.findOne()
{
    _id : ObjectID('AAAA'),
    partno : '1224-dsdf-2215',
    name : 'bearing',
    price: 2.63

> db.products.findOne()
{
    name : 'car',
    manufacturer : 'Ford',
    catalog_number: 2234,
    parts : [     // array of references to Part documents
        ObjectID('AAAA'),    // reference to the bearing above
        ObjectID('F17C'),    // reference to a different Part
        ObjectID('D2AA'),
        // etc
]

2.3.設計模式可視化

儘管MongoDB是schemaless“無模式的”,但仍然存在將集合collections可視化為圖表的方法。能夠查看設計圖,將對您理解和設計MongoDB的方式上產生重大影響。

DbSchema是可以很好地完成可視化設計工作的一個工具。如下圖所示,它將通過讀取集合和文檔來推導架構。此外,您只需單擊就可以修改圖中的對象。在DbSchema中,您還可以為MongoDB創建外鍵,當然僅在本地創建,只用於設計目的。

2.4.智能索引

為了保持數據庫的良好性能,有必要建立智能索引,這將簡化寫入和讀取操作。知道MongoDB的索引優勢和局限性非常重要,MongoDB保留用於排序操作的內存限製為32MB。如果你不使用索引,則排序時數據庫將被迫將所有排序文檔hold在內存裏面,如果達到32M的限制,則數據庫將返回錯誤或空集。

結論

對MongoDB的透徹理解與對數據庫想要實現的目標的清晰了解是良好數據庫設計的秘訣。

歡迎關注我的博客,裏面有很多精品合集

  • 本文轉載註明出處(必須帶連接,不能只轉文字):字母哥博客。

覺得對您有幫助的話,幫我點贊、分享!您的支持是我不竭的創作動力! 。另外,筆者最近一段時間輸出了如下的精品內容,期待您的關注。

  • 《手摸手教你學Spring Boot2.0》
  • 《Spring Security-JWT-OAuth2一本通》
  • 《實戰前後端分離RBAC權限管理系統》
  • 《實戰SpringCloud微服務從青銅到王者》
  • 《VUE深入淺出系列》

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※想知道最厲害的網頁設計公司"嚨底家"!

※幫你省時又省力,新北清潔一流服務好口碑

※別再煩惱如何寫文案,掌握八大原則!

※產品缺大量曝光嗎?你需要的是一流包裝設計!

Swagger之外的選擇

今天給大家安利一款接口文檔生成器——JApiDocs。

swagger想必大家都用過吧,非常方便,功能也十分強大。如果要說swaager有什麼缺點,想必就是註解寫起來比較麻煩。如果我說有一款不用寫註解,就可以生成文檔的工具,你心動了嗎?他就是我們今天的主角——JApiDocs。

下面我們一起來看看如何使用!

一、添加依賴

<dependency>
  <groupId>io.github.yedaxia</groupId>
  <artifactId>japidocs</artifactId>
  <version>1.3</version>
</dependency>

二、配置生成參數

我們新建一個項目,然後隨便寫一個main方法,增加生成文檔的配置,然後運行main方法。

DocsConfig config = new DocsConfig();
config.setProjectPath("F:\\Java旅途\\japi-docs"); // 項目根目錄
config.setProjectName("japi-docs"); // 項目名稱
config.setApiVersion("V1.0");       // 聲明該API的版本
config.setDocsPath("F:\\test"); // 生成API 文檔所在目錄
config.setAutoGenerate(Boolean.TRUE);  // 配置自動生成
Docs.buildHtmlDocs(config); // 執行生成文檔

三、編碼規範

由於JApiDocs是通過解析Java源碼來實現的,因此如果要想實現想要的文檔,還是需要遵循一定的規範。

3.1 類註釋、方法註釋和屬性註釋

如果我們想生成類的註釋,我們可以直接在類上加註釋,也可以通過加@description來生成。

/**
 * 用戶接口類
 */
@RequestMapping("/api/user")
@RestController
public class UserController {}

/**
 * @author Java旅途
 * @Description 用戶接口類
 * @Date 2020-06-15 21:46
 */
@RequestMapping("/api/user")
@RestController
public class UserController {}

如果我們想生成方法的註釋,只能直接加註釋,不能通過加@description來生成。

/**
 * 查詢用戶
 * @param age 年齡
 * @return R<User>
*/
@GetMapping("/list")
public R<User> list(@RequestParam int age){

    User user = new User("Java旅途", 18);
    return R.ok(user);
}

JApiDocs可以自動生成實體類,關於實體類屬性的註釋有三種方式,生成的效果都是一樣的,如下:

/**
 * 用戶名稱
 */
private String name;
/**
 * 用戶年齡
 */
private int age;
// 用戶名稱
private String name;
// 用戶年齡
private int age;
private String name;// 用戶名稱
private int age;// 用戶年齡

他除了支持咱們常用的model外,還支持IOS的model生成效果如下:

​

3.2 請求參數

如果提交的表單是 application/x-www-form-urlencoded 類型的key/value格式,則我們通過@param註解來獲取參數,在參數後面添加註釋,示例如下:

/**
  * @param age 年齡
  */
@GetMapping("/list")
public R<User> list(@RequestParam int age){
    User user = new User("Java旅途", 18);
    return R.ok(user);
}

生成的文檔效果如下:

請求參數

參數名 類型 必須 描述
age int 否 年齡

如果提交的表單是 application/json 類型的json數據格式,如下:

/**
  * @param user
  * @return
  */
@PostMapping("/add")
public R<User> add(@RequestBody User user){
    return R.ok(user);
}

生成的文檔效果如下:

請求參數

{
  "name": "string //用戶名稱",
  "age": "int //用戶年齡"
}

3.3 響應結果

我們知道,如果Controller聲明了@RestController,SpringBoot會把返回的對象直接序列成Json數據格式返回給前端。 JApiDocs也利用了這一特性來解析接口返回的結果,但由於JApiDocs是靜態解析源碼的,因此你要明確指出返回對象的類型信息,JApiDocs支持繼承、泛型、循環嵌套等複雜的類解析。

因此我們不需要再寫註釋,它會根據我們的返回結果進行解析,效果如下:

返回結果:

{
  "code": "int",
  "msg": "string",
  "data": {
    "name": "string //用戶名稱",
    "age": "int //用戶年齡"
  }
}

最終,我們生成的接口文檔,如下:

四、高級配置

4.1 @ApiDoc

如果你不希望把所有的接口都導出,我們可以在配置中設置config.setAutoGenerate(Boolean.FALSE);然後再想要生成的接口上添加@ApiDoc。

@ApiDoc有以下三個屬性:

  • result: 這個可以直接聲明返回的對象類型,如果你聲明了,將會覆蓋SpringBoot的返回對象
  • url: 請求URL,擴展字段,用於支持非SpringBoot項目
  • method: 請求方法,擴展字段,用於支持非SpringBoot項目
@ApiDoc(result = User.class, url = "/api/user/view", method = "post")

4.2 @Ignore

如果你不想導出對象裏面的某個字段,可以給這個字段加上@Ignore註解,這樣JApiDocs導出文檔的時候就會自動忽略掉了。

public class User {
    @Ignore
    private int age;
}

五、總結

JApiDocs就介紹到這裏了,優勢劣勢大家很容易就看出來了。幾乎不需要註釋即可生成接口文檔,僅有的幾個註釋我們也可以通過ide來自動生成。但是JApiDocs不具備swagger在線調試功能。如果有一天JApiDocs支持在線調試后,那時候肯定會有一大波追隨者,畢竟寫代碼的誰喜歡寫多餘的註解!~

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※新北清潔公司,居家、辦公、裝潢細清專業服務

※別再煩惱如何寫文案,掌握八大原則!

※教你寫出一流的銷售文案?

※超省錢租車方案

※FB行銷專家,教你從零開始的技巧

我們是如何做go語言系統測試覆蓋率收集的?

工程效能領域,測試覆蓋率度量總是繞不開的話題,我們也不例外。在七牛雲,我們主要使用go語言構建雲服務,在考慮系統測試覆蓋率時,最早也是通過圍繞原生go test -c -cover的能力來構建。這個方案,筆者還曾在 MTSC2018大會上有過專項分享。其實我們當時已經做了不少自動化工作,能夠針對很多類型的代碼庫,自動插樁服務,自動生成TestMain()等方法,但隨着接入項目越來越多,以及後面使用場景的不斷複雜化,我們發現這套還是有其先天局限,會讓後面越來越難受:

  • 程序必須關閉才能收集覆蓋率。如果將這套系統僅定位在收集覆蓋率數據上,這個痛點倒也能忍受。但是如果想進一步做精準測試等方向,就很受局限。
  • 因為不想污染被測代碼庫,我們採取了自動化的方式,在編譯階段給每個服務生成類似main_test.go文件。但這種方式,其最難受的地方在於flag的處理,要知道go test命令本身會調用flag.Parse方法,所以這裏需要自動化的修改源碼,保證被測程序的flag定義,要先於go test調用flag.Parse之前。但是,隨着程序自己使用flag姿勢的複雜化,我們發現越來越難有通用方案來處理這些flag,有點難受。
  • 受限於go test -c命令的先天缺陷,它會給被測程序注入一些測試專屬的flag,比如-test.coverprofile, -test.timeout等等。這個是最難受的,因為它會破壞被測程序的啟動姿勢。我們知道系統測試面對是完整被測集群,如果你需要專門維護一套測試集群來做覆蓋率收集時,就會顯得非常浪費。好鋼就應該用在刀刃上,在七牛雲,我們倡導極客文化,追求用工程師思維解決重複問題。而作為業務效率部門,我們自己更應該走在前列。

也是因為以上的種種考量,我們內部一直在優化這一套系統,到今天這一版,我們已從架構和實現原理上完成了顛覆,能夠做到無損插樁,運行時分析覆蓋率,當屬非常優雅。

Goc – A Comprehensive Coverage Testing System for The Go Programming Language

一圖勝千言:

使用goc run .的姿勢直接運行被測程序,就能在運行時,通過goc profile命令方便的得到覆蓋率結果。是不是很神奇?是不是很優雅?

這個系統就是goc, 設計上希望完全兼容go命令行工具核心命令(go buld/install/run)。使用體驗上,也希望向go命令行工具靠攏:

以下是goc 1.0版本支持的功能:

系統測試覆蓋率收集方案

有了goc,我們再來看如何收集go語言系統測試覆蓋率。整體比較簡單,大體只需要三步:

  • 首先通過goc server命令部署一個服務註冊中心,它將會作為樞紐服務跟所有的被測服務通信。

  • 使用goc build --center="<server>" 命令編譯被測程序。goc不會破壞被測程序的啟動方式,所以你可以直接將編譯出的二進制發布到集成測試環境。

  • 環境部署好之後,就可以做執行任意的系統測試。而在測試期間,可以在任何時間,通過goc profile --center="<server>"拿到當前被測集群的覆蓋率結果。
    是不是很優雅?

goc 核心原理及未來

goc在設計上,拋棄老的go test -c -cover模式,而是直接與go tool cover工具交互,避免因go test命令引入的一系列弊端。goc同樣沒有選擇自己做插樁,也是考慮go語言的兼容性,以及性能問題,畢竟go tool cover工具,原生採用結構體來定義counter收集器,每個文件都有單獨的結構體,性能相對比較可靠。goc旨在做go語言領域綜合性的覆蓋率工具以及精準測試系統,其還有很長的路要走:

  • 基於PR的單測/集測/系統覆蓋率增量分析
  • 精準測試方向,有一定的產品化設計體驗,方便研發與測試日常使用
  • 擁抱各種CICD系統

當前goc已經開源了,歡迎感興趣的同學,前往代碼倉庫查看詳情並Star支持。當然,我們更歡迎有志之士,能夠參与貢獻,和我們一起構建這個有意思的系統。

最後,父親節快樂!

Contact me ?

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※新北清潔公司,居家、辦公、裝潢細清專業服務

※別再煩惱如何寫文案,掌握八大原則!

※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※超省錢租車方案

※教你寫出一流的銷售文案?

※網頁設計最專業,超強功能平台可客製化

C# 9.0 新特性之只讀屬性和記錄

閱讀本文大概需要 2 分鐘。

大家好,這是 C# 9.0 新特性系列的第 4 篇文章。

熟悉函數式編程的童鞋一定對“只讀”這個詞不陌生。為了保證代碼塊自身的“純潔”,函數式編程是不能隨便“弄髒”外來事物(參數、變量等)的,所以“只讀”對函數式編程非常重要。

為了豐富 C# 對函數式編程支持,較新的 C# 版本引入了一些很有用的新特性。比如 C# 8 中就對 struct 類型的方法增加了 readonly 修飾符支持,被 readonly 修飾的方法是不能修改該方法所在類的屬性的。舉個例子:

public struct FooValue
{
    private int A { get; set; }
    public readonly int IncreaseA()
    {
        A = A + 1; // 報錯
        return A;
    }
}

而 C# 9 又進一步增加了對“只讀”的支持,此次增加了 init-only 屬性和 record 相關特性,下面一一介紹。

Init-only 屬性

我們知道類的屬性有 set 和 get 兩種訪問器,現在 C# 9 增加一種屬性訪問器:init。init 是 set 訪問器的變體,它的作用是使屬性只能在對象初始化的時候對其賦值,之後該屬性就是只讀的,因此叫 init-only 屬性。使用方式如下:

public class Foo
{
    public string PropA { get; init; }
    public string PropB { get; init; }
}

賦值操作:

var foo = new Foo {  PropA = "A", PropB = "B" };
foo.PropA = "AA"; // 報錯,PropA 此時是只讀的!

由於 init 是在初始化階段賦值,所以它可以在類內部修改 readonly 修飾的字段。比如:

public class Foo
{
    private readonly string propA;
    private readonly string propB;

    public string PropA
    {
        get => propA;
        init => propA = (value ?? throw new ArgumentNullException(nameof(propA)));
    }
    public string PropA
    {
        get => propB;
        init => propB = (value ?? throw new ArgumentNullException(nameof(propB)));
    }
}

如果你知道在構造函數中可以對只讀字段/屬性賦值就自然也理解這一點。

記錄 (Record)

做過財務系統的人都知道交易記錄一旦入賬是不能修改的,如果錄入錯誤,就要新錄入一筆負的記錄把之前的紅衝掉,再錄入正確的記錄。應對類似這種只讀記錄的場景,C# 9 引入了 Record(記錄,下文均使用中文的“記錄”)的概念,它用來支持整個對象的只讀特性(即實例化後為只讀)。使用方式如下:

public data class Foo
{
    public string PropA { get; init; }
    public string PropB { get; init; }
}

這裏用了一個 data 關鍵字,表示該類的對象只是純粹的記錄值,它不是可修改的狀態(在函數式編程中,所有的數據修改都是狀態在發生變化)。

上面的太麻煩了,可以這樣簡寫:

public data class Foo
{
    string PropA;
    string PropB;
}

默認屬性都是 public 的,如果實在要改為 private,可以在屬性定義前面加上 private 修飾符。

定位記錄 (Positional Record)

有時候為了初始化更方便,可以定義構造函數來給屬性賦值,初始化時只需要把屬性值按順序傳給構造函數即可,這個操作稱為定位構造(Positional Construction)。同樣,也可以使用解構函數(Deconstructor)來實現屬性的解構,即按照解構函數的參數順序從對象中提取屬性的值,被稱為定位解構(Positional Deconstructor)。實現了定位構造或定位解構的記錄稱為定位記錄(Positional Record)。下面是一個定位記錄的實現:

public data class Foo
{
    string PropA;
    string PropB;
    public Foo(string propA, string propB)
      => (PropA, PropB) = (propA, propB);
    public void Deconstruct(out string propA, out string propB)
      =>  (propA, propB) = (PropA, PropB);
}

這個寫法太麻煩了,可以直接簡寫為:

public data class Foo(string PropA, string PropB);

這樣簡短一句代碼,其內部默認實現了 init-only 自動屬性,且同時為所有屬性定義了構造函數和解構函數。

使用示例:

var foo = new Foo("AA", "BB");  // 構造定位
var (a, b) = foo;               // 解構定位

可以想象,記錄的大部分使用場景,以上簡寫的寫法能滿足需求。若有特殊場景,就不能簡單,需要進行自定義修改其默認行為。

with 表達式

當處理不可變數據時,若要生成不同的狀態,一個常見的場景是在一條舊記錄基礎上拷貝一條新的記錄。比如我們要修改 Foo 對象的 PropA 屬性,我們就要拷貝該對象生成一個新的對象。這個操作在函數式編程中被稱為“非破壞性修改 (non-destructive mutation)”。為了支持記錄這個操作,C# 9 引入了 with 表達式,它可以很方便在一條原有記錄基礎上創建一條新記錄。示例:

var other = foo with { PropA = "AA" };

with 表達式內部其實是通過一個默認的 protected 構造函數來實現的,大致如下:

protected Foo(Foo original)
{
    // 拷貝 original 的所有字段
}

如果默認實現的字段拷貝不符合你的需求,你也可以手動實現這個構造函數。

今天就分享到這裏,敬請期待一下篇關於 C# 9 新特性的文章!

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※教你寫出一流的銷售文案?

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※回頭車貨運收費標準

※別再煩惱如何寫文案,掌握八大原則!

※超省錢租車方案

※產品缺大量曝光嗎?你需要的是一流包裝設計!

HTML躬行記(1)——SVG

  <svg>是矢量圖的根元素,通過xmlns屬性聲明命名空間,從而告訴用戶代理標記名稱屬於哪個XML方言。在下面的示例中,為<svg>元素聲明了寬度和高度(默認以像素為單位),其子元素<title>可作為提示,在<desc>中可聲明一段描述性純文本,這兩個元素都不會在頁面中呈現。而<rect>是一個矩形,將被繪製到頁面中。

<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100">
  <title>SVG example</title>
  <desc>SVG contains a rectangle</desc>
  <rect width="50" height="50" fill="#F60" />
</svg>

  SVG作為一種圖像格式,可以包含在<img>元素內(如下所示)或CSS樣式中(例如background-image屬性)。

<img src="demo.svg" />

一、坐標系統

1)視口

  在SVG中,有一張無限大的畫布,而畫布區域被稱為視口(viewport)。通過<svg>元素的width和height兩個屬性可定義視口的尺寸,視口的原點在其左上角。

  而在視口中,還包含一個坐標系統,由viewBox屬性控制。它能包含四個數值(以逗號或空格分隔),分別是用戶坐標系統的最小橫坐標(x軸)和縱坐標(y軸),以及寬和高。

  下面的兩個<svg>元素,第一個採用了默認的坐標系統,第二個聲明了新的坐標系統,並且寬高比相同,如圖1所示,第二個矩形縮小了。

<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100">
  <rect width="50" height="50" fill="#F60" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100" viewBox="0 0 300 200">
  <rect width="50" height="50" fill="#F60" />
</svg>

圖 1

  接下來修改兩個最小坐標(如圖2所示),第一個<svg>元素聲明的最小坐標為(20,20),雖然矩形的坐標是(0,0),但是比最小坐標要小,所以就會往左上角偏移;第二個<svg>元素聲明了負數坐標,與前一個正好相反;在第三個<svg>元素中,修改了矩形的坐標,正好在左上角。

<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100" viewBox="20 20 300 200">
  <rect width="50" height="50" fill="#F60" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100" viewBox="-20 -20 300 200">
  <rect width="50" height="50" fill="#F60" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100" viewBox="20 20 300 200">
  <rect width="50" height="50" fill="#F60" x="20" y="20" />
</svg>

圖 2

2)preserveAspectRatio屬性

  當viewBox屬性中聲明的尺寸與視圖的寬高比不一致時,可以使用preserveAspectRatio屬性指定圖形的縮放比例和對齊方式,語法如下,默認值是“xMidYMid meet”。

preserveAspectRatio: <align> [<meetOrSlice>]

  其中<align>的屬性值由兩個軸(x和y)以及三個位置(min、mid和max)組合而成,如表1所列。

表 1

值 說明
xMin viewport與viewBox的左側對齊
xMid viewport與viewBox的x軸中點對齊
xMax viewport與viewBox的右側對齊
YMin viewport與viewBox的頂部對齊
YMid viewport與viewBox的y軸中點對齊
YMax viewport與viewBox的底部對齊

  在下面的示例中,由於三個<svg>元素寬高比是3:2,而viewBox的寬高比是3:1,因此矩形會等比縮小。對它們分別應用xMinYMin、xMidYMid和xMinYMax,效果如圖3所示。

<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100" viewBox="0 0 300 100" preserveAspectRatio="xMinYMin">
  <rect width="50" height="50" fill="#F60" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100" viewBox="0 0 300 100" preserveAspectRatio="xMidYMid">
  <rect width="50" height="50" fill="#F60" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100" viewBox="0 0 300 100" preserveAspectRatio="xMinYMax">
  <rect width="50" height="50" fill="#F60" />
</svg>

圖 3

  注意,因為寬度正好適配,所以對於x軸的對齊方式,效果都是相同的。

  <meetOrSlice>可控製圖形的適配或裁剪,可選的值如表2所列。

表 2

值 說明
meet 保留圖形的寬高比,並且縮放viewBox以適應viewport
slice 保留圖形的寬高比,並且放大viewBox以覆蓋viewport

  在下面的示例中,兩個矩形的高度比視口要大,對它們分別應用meet和slice,效果如圖4所示,第二個矩形將整個視口給填滿了。

<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100" viewBox="0 0 300 100" preserveAspectRatio="xMinYMin meet">
  <rect width="150" height="150" fill="#F60" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100" viewBox="0 0 300 100" preserveAspectRatio="xMinYMin slice">
  <rect width="150" height="150" fill="#F60" />
</svg>

圖 4

二、形狀

  SVG的基本形狀包括線段(line)、矩形(rect)、圓形(circle)、橢圓(ellipse)、多邊形(polygon)和折線(polyline)。

1)<line>

  線段元素需要指定起止點的坐標,如下所示,效果如圖5所示。

<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100" viewBox="0 0 300 200">
  <line x1="20" y1="100"  x2="100" y2="20" stroke="black" stroke-width="2"/>
</svg>

圖 5

  其中stroke和stroke-width是筆畫屬性,可指定筆畫的顏色和寬度,相關屬性如表3所列,部分屬性的效果如圖6所示。

表 3

值 說明
stroke 筆畫顏色
stroke-dasharray 筆畫的外觀(實線、點線或虛線),由一系列逗號分隔的数字組成,表示長度和空隙長度
stroke-dashoffset 相對繪圖起點的偏移值
stroke-linecap 描邊的展現形狀
stroke-linejoin 路徑轉角處的形狀
stroke-miterlimit 斜接長度與線寬的最大比例
stroke-opacity 筆畫不透明度,取值範圍0~1,其中0表示透明
stroke-width 筆畫寬度

圖 6

2)<rect>

  除了直角矩形之外,還可以聲明圓角矩形,如下所示,效果如圖7所示。

<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100" viewBox="0 0 300 200">
  <rect x="10" y="10" width="100" height="100" rx="15" ry="15" fill="#F60"/>
</svg>

圖 7

3)<circle>和<ellipse>

  利用圓形,可非常便捷的畫出圓環,效果如圖8所示。將<circle>元素的stroke-dasharray聲明為圓的周長(2πR),例如半徑為50,周長就是314。如果為stroke-dashoffset屬性定義一個值,就能得到圓環缺失一段的效果,從而就能模擬出圓環型的進度條了。

<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100" viewBox="0 0 300 200">
  <circle cx="100" cy="100" r="50" stroke="black" stroke-width="2" 
    stroke-dasharray="314" stroke-dashoffset="40" fill="transparent" />
</svg>

圖 8

  橢圓和圓形類似,只是需要聲明兩個方向的半徑,如圖9所示。

<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100" viewBox="0 0 300 200">
  <ellipse cx="100" cy="100" rx="100" ry="50" />
</svg>

圖 9

4)<polygon>

  <polygon>可畫出任意形狀的封閉圖形,例如平行四邊形、梯形等。在points屬性中,可聲明各個點的坐標,每組坐標用逗號隔開,下面繪製了一個五角星,如圖10所示。

<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100" viewBox="0 0 300 200">
  <polygon points="100,0 160,180 10,60 190,60 40,180" fill="#F60"/>
</svg>

圖 10

5)<polyline>

  折線不需要閉合,與<polygon>元素類似,也是由points屬性繪製,如下所示,得到的折線如圖11所示。

<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100" viewBox="0 0 300 200">
  <polyline points="20,100 40,60 70,80 100,20" fill="none" stroke="black" />
</svg>

圖 11

6)<path>

  上述基本形狀都可以由<path>元素來繪製,並且通過<path>元素還可繪製出不規則的圖形,例如心形,如下所示。

<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100" viewBox="0 0 300 200">
  <path d="M 10,30
           A 20,20 0,0,1 50,30
           A 20,20 0,0,1 90,30
           Q 90,60 50,90
           Q 10,60 10,30 z" />
</svg>

  其中d屬性可聲明一系列路徑,包含多個指令,如表4所列。

表 4

指令 效果
M、m 移動到指定的坐標
L、l 繪製一條直線
H、h 繪製一條水平線
V、v 繪製一條垂直線
Z、z 關閉路徑
Q、q 繪製一條二次貝塞爾曲線
T、t 繪製一條平滑的二次貝塞爾曲線
C、c 繪製一條三次貝塞爾曲線
S、s 繪製一條平滑的三次貝塞爾曲線
A、a 繪製弧形曲線

三、文檔結構

1)內部樣式

  SVG允許在其內部嵌入<style>元素,如下所示,其中CDATA區段中的文本會被解析器忽略,但不影響渲染。

<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100" viewBox="0 0 300 200">
  <style>
    <![CDATA[
    line {
      stroke: black;
      stroke-width: 2;
    }
    ]]>
  </style>
  <line x1="20" y1="100" x2="100" y2="20" />
</svg>

2)<g>

  <g>元素相當於一個容器,能將其所有的子元素組合在一起。應用於<g>元素中的屬性會被其子元素所繼承,如下所示,兩個圓的筆畫顏色都是綠色,而寬度都是10,如圖12所示。

<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100" viewBox="0 0 300 200">
  <g id="ring" fill="white" stroke="green" stroke-width="10">
    <circle cx="80" cy="80" r="50" />
    <circle cx="120" cy="120" r="50" />
  </g>
</svg>

圖 12

3)<use>

  <use>元素可引用其它圖形,以及<g>元素,類似於複製黏貼的功能。在下面的示例中,通過<use>元素的xlink:href屬性引用了id為ring的<g>元素,並且將<use>元素上聲明的屬性傳給了<circle>元素。

<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100" viewBox="0 0 300 200">
  <g id="ring">
    <circle cx="80" cy="80" r="50" />
    <circle cx="120" cy="120" r="50" />
  </g>
  <use x="100" y="0" fill="white" stroke="green" stroke-width="10" xlink:href="#ring" />
</svg>

  注意,當給<use>元素定義坐標后,<circle>元素會與其相加計算出最終的坐標,如圖13所示。

圖 13

4)<defs>

  可將需要復用的圖形抽象到<defs>元素中,在其內部定義的圖形不會直接呈現到頁面中。在上面那個示例的基礎上,將<g>元素整個放置到<defs>中,效果如圖14所示,沒有渲染<g>元素中的圓。

<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100" viewBox="0 0 300 200">
  <defs>
    <g id="ring">
      <circle cx="80" cy="80" r="50" />
      <circle cx="120" cy="120" r="50" />
    </g>
  </defs>
  <use x="100" y="0" fill="white" stroke="green" stroke-width="10" xlink:href="#ring" />
</svg>

圖 14

5)<symbol>

  <symbol>提供了另一種組合圖形的方式,但與<g>元素不同,它的圖形不會呈現,並且它可以聲明viewBox和preserveAspectRatio兩個屬性,如下所示。

<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100">
  <symbol id="ring" viewBox="0 0 300 200">
    <circle cx="80" cy="80" r="50" />
    <circle cx="120" cy="120" r="50" />
  </symbol>
  <use fill="white" stroke="green" stroke-width="10" xlink:href="#ring" />
</svg>

6)<image>

  <use>元素可以引用SVG文件的某個部分,而<image>元素可以引用整個SVG文件或柵格圖像(如下所示),並且能控制引用文件的尺寸和preserveAspectRatio屬性,效果如圖15所示。

<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100" viewBox="0 0 300 200">
  <image xlink:href="img/avatar.png" width="150" />
</svg>

圖 15

四、變形、圖案和漸變

1)變形

  transform屬性定義了一系列圖形元素變形的規則,包括位移(translate)、縮放(scale)、旋轉(rotate)和傾斜(skew)。注意,與CSS3中的transform屬性不同,SVG中的transform屬性作用的對象是坐標系統,而不是元素本身。

  translate()函數會接收兩個參數,沿x軸和y軸位移坐標系統,如圖16所示。

<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100" viewBox="0 0 300 200">
  <rect width="50" height="50" fill="#F60" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100" viewBox="0 0 300 200">
  <rect width="50" height="50" fill="#F60" transform="translate(50, 30)" />
</svg>

圖 16

  scale()函數也會接收兩個參數(這點與CSS3中的scale()不同),沿x軸和y軸縮放坐標系統,如圖17所示。

<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100" viewBox="0 0 300 200">
  <rect width="50" height="50" fill="#F60" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100" viewBox="0 0 300 200">
  <rect width="50" height="50" fill="#F60" transform="scale(2, 3)" />
</svg>

圖 17

  rotate()函數可接收三個參數,第一個是旋轉角度,另外兩個是原點坐標,也就是坐標系統按照該原點旋轉,如圖18所示,第三個矩形指定了原點。

<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100" viewBox="0 0 300 200">
  <rect width="50" height="50" fill="#F60" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100" viewBox="0 0 300 200">
  <rect width="50" height="50" fill="#F60" transform="rotate(30)" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100" viewBox="0 0 300 200">
  <rect width="50" height="50" fill="#F60" transform="rotate(30, 50, 30)" />
</svg>

圖 18

  傾斜分為兩個函數:skewX()和skewY(),分別會沿着x軸和y軸傾斜,如圖19所示。

<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100" viewBox="0 0 300 200">
  <rect width="50" height="50" fill="#F60" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100" viewBox="0 0 300 200">
  <rect width="50" height="50" fill="#F60" transform="skewX(30)" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100" viewBox="0 0 300 200">
  <rect width="50" height="50" fill="#F60" transform="skewY(30)" />
</svg>

圖 19

2)圖案

  要創建一個圖案,就得使用<pattern>元素包裹圖形元素,再利用patternUnits屬性確定平鋪圖案的方式。它有兩個關鍵字可選,默認的objectBoundingBox會讓<pattern>元素的尺寸以百分數或0~1之間的小數表示,參照對象分別是引用<pattern>的圖形元素的寬和高。

  以下面的圖案為例,它的寬和高都是10%,參照的圓形的寬高都是200,計算出的實際值就都是20,效果如圖20所示。

<svg xmlns="http://www.w3.org/2000/svg" width="250" height="200">
  <defs>
    <pattern id="star1" width="10%" height="10%" patternUnits="objectBoundingBox">
      <circle cx="10" cy="10" r="10" />
    </pattern>
  </defs>
  <circle cx="100" cy="100" r="100" fill="url(#star1)" />
</svg>

圖 20

  另一個userSpaceOnUse會讓<pattern>元素的尺寸以絕對值表示,如下所示,因為<pattern>元素的寬高都為25,所以圖案會有空白間隙,得到的效果如圖21所示。

<svg xmlns="http://www.w3.org/2000/svg" width="250" height="200">
  <defs>
    <pattern id="star2" width="25" height="25" patternUnits="userSpaceOnUse">
      <circle cx="10" cy="10" r="10" />
    </pattern>
  </defs>
  <circle cx="100" cy="100" r="100" fill="url(#star2)" />
</svg>

圖 21

  <pattern>元素還包含另一個patternContentUnits屬性,用於處理圖案內部的排列方式,它的兩個關鍵字也是objectBoundingBox和userSpaceOnUse,後者是該屬性的默認值。

  objectBoundingBox會讓<pattern>中的圖形元素以小數定義,如下所示。三個屬性值都是0.1(不能用百分數),參照的仍然是引用<pattern>的圖形元素,計算得到的實際值都是20,效果如圖22所示,每個圖案只显示四分之一個圓。

<svg xmlns="http://www.w3.org/2000/svg" width="250" height="200">
  <defs>
    <pattern id="star3" width="10%" height="10%" patternContentUnits="objectBoundingBox">
      <circle cx=".1" cy=".1" r=".1" />
    </pattern>
  </defs>
  <circle cx="100" cy="100" r="100" fill="url(#star3)" />
</svg>

圖 22

3)漸變

  <linearGradient>元素用於繪製線性漸變,其子元素<stop>可指定某處的色標(即漸變點),如下所示,在起點、中點和止點處聲明了三種顏色,其中stop-opacity用於聲明不透明度(取值範圍0~1),1表示完全不透明,得到的效果如圖23所示。

<svg xmlns="http://www.w3.org/2000/svg" width="250" height="200">
  <defs>
    <linearGradient id="gradient">
      <stop offset="0" stop-color="#F60" />
      <stop offset="50%" stop-color="#CCC" stop-opacity=".5"/>
      <stop offset="100%" stop-color="#FC0" />
    </linearGradient>
  </defs>
  <rect width="200" height="100" fill="url(#gradient)" />
</svg>

圖 23

  如果要改變線性漸變的方向,可通過修改起點和終點的坐標實現,例如沿垂直線漸變,代碼如下,得到的效果如圖24所示。

<svg xmlns="http://www.w3.org/2000/svg" width="250" height="200">
  <defs>
    <linearGradient id="gradientVertical" x1="0" y1="0" x2="0" y2="100%">
      <stop offset="0" stop-color="#F60" />
      <stop offset="50%" stop-color="#CCC" stop-opacity=".5" />
      <stop offset="100%" stop-color="#FC0" />
    </linearGradient>
  </defs>
  <rect width="200" height="100" fill="url(#gradientVertical)" />
</svg>

圖 24

  <linearGradient>元素還有一個spreadMethod屬性,可指定在漸變範圍之外的填充方式,它有三個值可選,如下所列。

  (1)pad:默認值,用起點和終點的顏色填充。

  (2)reflect:按終點到起點,起點到終點的方式重複填充。

  (3)repeat:按起點到終點的方式重複填充。

  下面用一個示例來演示spreadMethod屬性,如圖25所示,從左往右,spreadMethod屬性的值依次是pad、reflect和repeat。

<svg xmlns="http://www.w3.org/2000/svg" width="800" height="200">
  <defs>
    <linearGradient id="spreadMethod" x1="20%" y1="30%" x2="60%" y2="70%">
      <stop offset="0" stop-color="#F60" />
      <stop offset="40%" stop-color="#CCC" />
      <stop offset="80%" stop-color="#FC0" />
    </linearGradient>
    <linearGradient id="gradientPad" href="#spreadMethod" spreadMethod="pad" />
    <linearGradient id="gradientReflect" href="#spreadMethod" spreadMethod="reflect" />
    <linearGradient id="gradientRepeat" href="#spreadMethod" spreadMethod="repeat" />
  </defs>
  <rect width="200" height="100" fill="url(#gradientPad)" />
  <rect width="200" height="100" fill="url(#gradientReflect)" x="210" />
  <rect width="200" height="100" fill="url(#gradientRepeat)" x="420" />
</svg>

圖 25

  另外一種徑向漸變由<radialGradient>元素控制,具體可參考官方文檔。

五、文本

1)<text>

  <text>元素用於處理SVG文件中的文本,在該元素中可修改字體樣式,包括字體名稱、大小、顏色、外觀等。在下面的示例中,聲明了四個<text>元素,併為它們添加了各自的樣式,效果如圖26所示。

<svg xmlns="http://www.w3.org/2000/svg" width="250" height="100">
  <style>
    .small {
      font: italic 13px sans-serif;
    }
    .heavy {
      font: bold 30px sans-serif;
    }
    .Rrrrr {
      font: italic 40px serif;
      fill: red;
    }
  </style>
  <text x="20" y="35" class="small">My</text>
  <text x="40" y="35" class="heavy">name</text>
  <text x="55" y="55" class="small">is</text>
  <text x="65" y="55" class="Rrrrr">Strick!</text>
</svg>

圖 26

2)對齊

  在<text>元素中對齊文本得用text-anchor屬性,它的對齊方式為起點(start)、居中(middle)和終點(end)。在下面的示例中,為了便於觀察這三個關鍵字的行為,畫了一條參考線,如圖27所示。

<svg xmlns="http://www.w3.org/2000/svg" width="120" height="150">
  <path d="M60,15 L60,110 M30,40 L90,40 M30,75 L90,75 M30,110 L90,110" stroke="grey" />
  <text text-anchor="start" x="60" y="40">Start</text>
  <text text-anchor="middle" x="60" y="75">Middle</text>
  <text text-anchor="end" x="60" y="110">End</text>
</svg>

圖 27

  SVG中的文本對齊與CSS中的略有不同,當起點對齊時,第一個字符會貼着參考線;當居中對齊時,文本的中間位置會被參考線貫穿;當終點對齊時,文本的最後一個字符會貼着參考線。

3)<tspan>

  在<text>元素中,通過其子元素<tspan>可調整文本屬性,從而實現一段文本呈現不同的效果,如下所示,這段文本為斜體,而<tspan>元素中的文本被加粗並且賦予了紅色(如圖28所示)。

<svg xmlns="http://www.w3.org/2000/svg" width="200" height="100">
  <style>
    text {
      font: italic 18px serif;
    }
    tspan {
      font: bold 20px sans-serif;
      fill: red;
    }
  </style>
  <text x="10" y="30" class="small">
    My name is
    <tspan>Strick</tspan>!
  </text>
</svg>

圖 28

4)長度
  textLength屬性用於定義文本的長度,lengthAdjust屬性用於設置字符間距和字形,如下所示,默認值spacing只會控制字符間距,而spacingAndGlyphs還能控制字形,如圖29所示。

<svg xmlns="http://www.w3.org/2000/svg" width="200" height="60">
  <text y="20" textLength="90%" lengthAdjust="spacing">Big text length</text>
  <text y="40" textLength="90%" lengthAdjust="spacingAndGlyphs">Big text length</text>
</svg>

圖 29

5)文本路徑

  將文本放置在<textPath>元素中,就可讓文本按任意的路徑排列,而不是以往的水平或垂直排列。在下面的示例中,文本按一條螺旋曲線排列,如圖30所示。

<svg xmlns="http://www.w3.org/2000/svg" width="150" height="120">
  <defs>
    <path id="curve" d="M10,90 Q90,90 90,45 Q90,10 50,10 Q10,10 10,40 Q10,70 45,70 Q70,70 75,50" />
  </defs>
  <text>
    <textPath href="#curve">
      My name is Strick! What is your name?
    </textPath>
  </text>
</svg>

圖 30

六、動畫

1)<animate>

  <animate>是一個動畫元素,它可以包含在圖形元素中,多個<animate>元素可以實現多重動畫。例如將矩形先沿着水平方向,再沿着垂直方向位移,最後在位移結束后切換背景色的動畫,代碼如下所示,效果如圖31。

<svg xmlns="http://www.w3.org/2000/svg" width="200" height="100">
  <rect id="move" width="50" height="50" fill="#F60">
    <animate attributeType="XML" dur="1s" repeatCount="1" attributeName="x"
        from="0" to="50" fill="freeze" begin="1s" id="line" />
    <animate attributeType="XML" dur="1s" repeatCount="1" attributeName="y"
        from="0" to="30" fill="freeze" begin="2s" />
    <animate attributeType="XML" dur="3s" repeatCount="indefinite" attributeName="fill"
        values="#F60;#FC0;#CCC" begin="line.end" />
  </rect>
</svg>

圖 31

  <animate>元素包含多個動畫屬性,此處只使用了其中的幾個,具體說明如下,其中fill=”freeze”是指動畫結束后保持最後的動作。

  (1)attributeName:執行動畫的屬性。

  (2)attributeType:屬性類型,可選值包括XML和CSS。

  (3)from:屬性的起始值。

  (4)to:屬性的結束值。

  (5)dur:動畫持續時間。

  (6)repeatCount:動畫執行次數,可以是有限次的整數,也可以是無限次的indefinite。

  (7)begin:動畫開始時機,可以是秒數,也可以是某個動作,例如上面第一個動畫結束后執行顏色的切換。

  <set>元素是對<animate>元素的補充,可為那些不能過渡的屬性提供動畫,例如在某個時刻显示文本,如下所示,點擊矩形可显示“change color”。

<svg xmlns="http://www.w3.org/2000/svg" width="200" height="100">
  <!--省略矩形代碼-->
  <text x="100" y="20" text-anchor="middle" style="display:none">
    <set attributeName="display" attributeType="CSS" to="block" begin="move.click" dur="1s" fill="freeze" />
    change color
  </text>
</svg>

2)<animateTransform>

  對於transform屬性的動畫,得用<animateTransform>元素完成。下面的示例演示了旋轉矩形(如圖32所示),其中type屬性用於指定變形的動作,可選的值有translate、scale、rotate、skewX和skewY。

<svg xmlns="http://www.w3.org/2000/svg" width="180" height="160">
  <rect id="move" width="50" height="50" fill="#F60" x="80" y="80">
    <animateTransform attributeName="transform" attributeType="XML" type="rotate"
        from="0 80 80" to="360 80 80" dur="5s" repeatCount="indefinite" />
  </rect>
</svg>

圖 32

3)<animateMotion>

  <animateMotion>元素可讓圖形沿着任意路徑運動,無論是直線還是曲線,都能實現。在下面的示例中,橙色矩形會沿着S型曲線來回運動,如圖33所示。

<svg xmlns="http://www.w3.org/2000/svg" width="200" height="100">
  <path fill="none" stroke="#000"
        d="M20,50 C20,-50 180,150 180,50 C180-50 20,150 20,50 z" />
  <rect width="20" height="10" fill="#F60">
    <animateMotion dur="10s" repeatCount="indefinite"
        path="M20,50 C20,-50 180,150 180,50 C180-50 20,150 20,50 z" />
  </rect>
</svg>

圖 33

  如果要讓矩形始終沿着平行於曲線的方向運動,那麼可以將rotate屬性設為auto,如圖34所示。

<svg xmlns="http://www.w3.org/2000/svg" width="200" height="100">
  <path fill="none" stroke="#000"
        d="M20,50 C20,-50 180,150 180,50 C180-50 20,150 20,50 z" />
  <rect width="20" height="10" fill="#F60">
    <animateMotion dur="10s" repeatCount="indefinite" rotate="auto"
        path="M20,50 C20,-50 180,150 180,50 C180-50 20,150 20,50 z" />
  </rect>
</svg>

圖 34

 

【參考資料】
SVG元素參考

[技術分享] 理解 SVG 中的 Viewport 和 ViewBox-拖曳與縮放功能實做(上)

理解SVG viewport,viewBox,preserveAspectRatio縮放

SVG圖像的viewport和viewBox用於設置圖像可見區域的大小

SVG:理解stroke-dasharray和stroke-dashoffset屬性

SVG 研究之路 (16) – Stroke-miterlimit

如何使用SVG圖案

SVG 研究之路 (26) – 有趣的 Patterns

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?

上周熱點回顧(6.15-6.21)

熱點隨筆:

· 因為知道了30+款在線工具,我的工作效率提升500%! (HollisChuang)
· 什麼?一個核同時執行兩個線程? (軒轅之風)
· .Net微服務實戰之DevOps篇 (陳珙)
· .Net 對於PDF生成以及各種轉換的操作 (沉_默)
· 龍芯團隊完成CoreCLR MIPS64移植,已在github開源 (張善友)
· foreach 集合又拋經典異常了,這次一定要刨根問底 (一線碼農)
· 對 JsonConvert 的認識太膚淺了,終於還是遇到了問題 (一線碼農)
· 深入理解 EF Core:EF Core 讀取數據時發生了什麼? (LiamWang)
· 養生吧,程序員! (沉默王二)
· cute-cnblogs 自定義博客園樣式美化二期來啦~ (麋鹿魯喲)
· .Net Core基礎的健康檢查 (她微笑的臉)
· 代碼之間為什麼要加空格? (沉默王二)

熱點新聞:

· 字節跳動程序員期權價值過億 28歲辭職享受人生引熱議 當事人回應
· 阿里P8員工1.6w招私人助理,網友群嘲:你咋不上天呢?
· 我,掉進20萬創業補貼陷阱:最後被告上法庭
· 獨家專訪28歲退休程序員郭宇:1600人加好友,問我“掙了多少錢”
· 北斗導航系統最後一顆衛星就緒:後天發射!
· 有碼變高清!AI一秒還原馬賽克,杜克大學出品
· “零收入”依然淡定,一家基礎軟件公司的冒險與底氣
· 重磅!突破美國封鎖,國產操作系統“天賜”即將誕生,火爆知乎
· 充電器1萬一隻53萬!波士頓動力網紅機器狗開售
· AMD將改寫10年來DIY市場歷史:份額逼近30% 大廠全齊了
· 兩千元的「反 5G 量子設備」,到底是怎樣的智商稅?
· 自主研發 華米發布全球最強大可穿戴人體傳感器

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※新北清潔公司,居家、辦公、裝潢細清專業服務

※推薦評價好的iphone維修中心