(譯)在ASP.NET Web API中使用Redis

本文將介紹如何在ASP.NET Web API應用程序中使用Redis做爲數據存儲。利用ServiceStack.Redis庫以及它強類型的Redis 客戶端,實現如何建模和存儲一對多的關係,以及使用Autofac的Web API依賴注入的功能,實現將資源庫(respositories)注入到控制器(controllers)中。html

客戶端(Client)類庫

寫做本文時有兩個流行和活躍的C#版Redis客戶端類庫:git

在作出選擇以前,建議兩個都試用下,再決定哪一個API和功能更適合你的項目。 BookSleeve包含非阻塞(異步)API,提供線程安全的鏈接對象,而ServiceStack的實現提供JSON序列化功能、相似於客戶端工廠的鏈接池以及使用約定簡化POCO(Plain Old CLR Object)對象的持久化。github

本文將使用ServiceStack.Redis,可是記住BookSleeve已經在大型的Web應用中使用且證實性能表現優良。web

nutshell中的Redis

你讀到本文時頗有可能對Redis有所瞭解了。若是你是一個對ASP.NET Web API 集成感興趣的經驗豐富的Redis用戶,那麼你能夠安全的跳到下一部分。redis

爲了有效使用Redis且避免潛在的陷阱,你得了解她是如何工做的以及它同關係型數據庫的區別。強烈建議閱讀一本書或者關於它的在線材料。算法

簡言之,Redis是一個支持持久化的鍵值對內存數據庫。*基於內存(in-memory)*和 *鍵值對(key-value)*聽起來很像一個內存緩存-實際上你能夠將Redis看作是一個專業的並且更加先進的內存緩存。shell

相比於傳統數據庫,它的主要優點是它直接在高速運轉的內存中存儲和獲取數據-這意味着它實際上速度很快。數據庫

Redis是簡單且專業的-不一樣於關係型數據庫,它不提供任何相似於表結構的抽象以及關係能力。可是,它提供五個基本數據類型,以及處理這些類型(存儲值)的特定操做。這也是爲何它有時會被當作一個 數據結構服務器(data structure server) 的緣由:api

  • 字符串(strings)-最基本且最原子的類型,用於儲存任何數據(整數,序列化POCO對象等等)
  • 列表(lists)-根據插入的順序排序後的字符串列表。
  • 集合(sets)-字符串的邏輯集合。
  • 哈希表(hashes)-字符串類型的key和字符串類型的value之間的映射。
  • 有序集合(sorted sets)-相似於集合,可是每一個元素都對應一個用於排序的得分。

專用的命令集有:緩存

  • 字符串-SET, INCR, APPEND, INCRBY, STRLEN, SETBIT,
  • 列表- LPUSH, LPOP, LTRIM, LINSERT,
  • 集合-SADD, SDIFF, SINTER, SUNION, etc.

但願這可讓你對Redis有個基本的瞭解。

爲何要使用Redis

Redis在應用程序可否起做用以及做用多大,取決於應用程序的體系結構,數據量,數據複雜度和有經驗的負載。若運用得當,Redis將會帶來重大的性能提高並且將爲大規模應用程序提供強有力的支持。

下面是我認爲的一些使用場景:

  • 做爲一個主數據庫,
  • 做爲衆多數據庫中的一個,例如存儲小數據量可是頻繁訪問的數據,
  • 做爲一個高性能的領域模型只讀視圖,
  • 做爲一個緩存。

值得注意的是Redis工做在內存中,因此第一個場景是很極端的,只有在你的數據量很小或者擁有足夠多的RAM的狀況下才可行。

因爲本文主要關注ASP.NET Web API 集成,而不是架構方面,所以選擇這種場景進行介紹。

Redis在ASP.NET Web API應用程序中的應用

本文將以空ASP.NET Web API應用程序爲起點,使用2個第三方類庫:

  • ServiceStack.Redis-C# Redis客戶端,
  • Autofac-集成Web API的依賴注入容器.

顯然也須要一個處於運行狀態的Redis服務實例。若是沒有Redis服務,那麼你能夠下載MS Tech提供的Windows 移植版本。請注意該移植版本還不是正式的產品(爲此你須要使用其中一個官方包),但在開發場景中是有效的。

模型

針對本例,考慮如下需求:

  • API應該提供存儲客戶、獲取客戶詳情以及獲取系統中的全部客戶列表的功能。
  • 客戶端能夠訂購由多個商品組成的訂單。
  • API應該提供一個最暢銷的N個商品的列表

下面是咱們設計的模型:

public class Customer
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public IList<Guid> Orders { get; set; }
    public Address Address { get; set; }
}

Properly defining your data model will help you use Redis in an efficient way. Redis stores values as byte blobs internally and *ServiceStack.Redis* will serialize the whole object graph for us. Thus it is important that we define aggregate boundaries. As you can see Address is a *value object* and will be persisted and retrieved as a part of Customer *aggregate*, while *Orders* property is a list of ids.

public class Order
{
    public Guid Id { get; set; }
    public Guid UserId { get; set; }
    public IList<OrderLine> Lines { get; set; }
}

public class OrderLine
{
    public string Item { get; set; }
    public int Quantity { get; set; }
    public decimal TotalAmount { get; set; }
}

public class Address
{
    public string Line1 { get; set; }
    public string Line2 { get; set; }
    public string City { get; set; }
}

如今定義資源庫接口:

public interface ICustomerRepository
{
    IList<Customer> GetAll();
    Customer Get(Guid id);
    Customer Store(Customer customer);
}

public interface IOrderRepository
{
    IList<Order> GetCustomerOrders(Guid customerId);
    IList<Order> StoreAll(Customer customer, IList<Order> orders);
    Order Store(Customer customer, Order order);
    IDictionary<string, double> GetBestSellingItems(int count);
}

實現以下:

public class CustomerRepository : ICustomerRepository
{
    private readonly IRedisClient _redisClient;

    public CustomerRepository(IRedisClient redisClient)
    {
        _redisClient = redisClient;
    }

    public IList<Customer> GetAll()
    {
        using (var typedClient = _redisClient.GetTypedClient<Customer>())
        {
            return typedClient.GetAll();
        }
    }

    public Customer Get(Guid id)
    {
        using (var typedClient = _redisClient.GetTypedClient<Customer>())
        {
            return typedClient.GetById(id);
        }
    }

    public Customer Store(Customer customer)
    {
        using (var typedClient = _redisClient.GetTypedClient<Customer>())
        {
            if (customer.Id == default(Guid))
            {
                customer.Id = Guid.NewGuid();
            }
            return typedClient.Store(customer);
        }
    }
}

public class OrderRepository : IOrderRepository
{
    private readonly IRedisClient _redisClient;

    public OrderRepository(IRedisClient redisClient)
    {
        _redisClient = redisClient;
    }

    public IList<Order> GetCustomerOrders(Guid customerId)
    {
        using (var orderClient = _redisClient.GetTypedClient<Order>())
        {
            var orderIds = _redisClient.GetAllItemsFromSet(RedisKeys
                        .GetCustomerOrdersReferenceKey(customerId));
            IList<Order> orders = orderClient.GetByIds(orderIds);
            return orders;
        }
    }

    public IList<Order> StoreAll(Customer customer, IList<Order> orders)
    {
        foreach (var order in orders)
        {
            if (order.Id == default(Guid))
            {
                order.Id = Guid.NewGuid();
            }
            order.CustomerId = customer.Id;
            if (!customer.Orders.Contains(order.Id))
            {
                customer.Orders.Add(order.Id);
            }

            order.Lines.ForEach(l=>_redisClient
                .IncrementItemInSortedSet(RedisKeys.BestSellingItems,
                                                                 (string) l.Item, (long) l.Quantity));
        }
        var orderIds = orders.Select(o => o.Id.ToString()).ToList();
        using (var transaction = _redisClient.CreateTransaction())
        {
            transaction.QueueCommand(c => c.Store(customer));
            transaction.QueueCommand(c => c.StoreAll(orders));
            transaction.QueueCommand(c => c.AddRangeToSet(RedisKeys
                .GetCustomerOrdersReferenceKey(customer.Id),
                orderIds));
            transaction.Commit();
        }

        return orders;
    }

    public Order Store(Customer customer, Order order)
    {
        IList<Order> result = StoreAll(customer, new List<Order>() { order });
        return result.FirstOrDefault();
    }

    public IDictionary<string, double> GetBestSellingItems(int count)
    {
        return _redisClient
            .GetRangeWithScoresFromSortedSetDesc(RedisKeys.BestSellingItems, 
            0, count - 1);
    }
}

能夠看到資源庫(respositories)實現了專有的操做。能夠利用Redis有序集類型有效存儲和獲取最暢銷商品列表。

值得注意的是咱們如何實現Customer-*Orders的關係。咱們在一個專屬集合中存儲了客戶的訂單(它們的Id),所以不須要取出整個Customer實體就可以快速獲取到它們。

客戶端和鏈接生命週期管理

咱們面臨的其中一個挑戰是鏈接/客戶端生命週期管理。正如你已經知道的那樣,Web API提供一個可擴展的依賴注入機制,它能夠用來對每一個請求注入和處理依賴。若是不打算從零開始本身編寫 IDependencyResolver 的實現(固然這也是一種選擇),咱們可使用.NET DI庫好比Ninject, StructureMap, Unity, Windsor 或者 Autofac。最後一個DI庫是我我的喜歡的,並且很好的集成了Web API,這也是爲何我在本例中使用它的緣故。

ServiceStack.Redis 擁有 IRedisClient工廠,即 客戶端管理器(client managers):

  • BasicRedisClientManager-支持負載均衡的客戶端工廠,
  • PooledRedisClientManager-支持負載均衡和鏈接池的客戶端工廠-實際工做中頗有用。
  • ShardedRedisClientManager-利用一致性哈希算法提供客戶端鏈接分片(sharding)。

因爲這些類庫是線程安全(thread-safe)的,所以能夠在全部的請求中使用一個工廠實例。

public class ApiApplication : System.Web.HttpApplication { 
    public IRedisClientsManager ClientsManager; 
    private const string RedisUri = "localhost";

    protected void Application_Start()
    {
        ClientsManager = new PooledRedisClientManager(RedisUri);

        AreaRegistration.RegisterAllAreas();

        WebApiConfig.Register(GlobalConfiguration.Configuration);
        ConfigureDependencyResolver(GlobalConfiguration.Configuration);

        FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
        RouteConfig.RegisterRoutes(RouteTable.Routes);
    }

    private void ConfigureDependencyResolver(HttpConfiguration configuration)
    {
        var builder = new ContainerBuilder();
        builder.RegisterApiControllers(Assembly.GetExecutingAssembly())
            .PropertiesAutowired();

        builder.RegisterType<CustomerRepository>()
            .As<ICustomerRepository>()
            .PropertiesAutowired()
            .InstancePerApiRequest();

        builder.RegisterType<OrderRepository>()
            .As<IOrderRepository>()
            .PropertiesAutowired()
            .InstancePerApiRequest();

        builder.Register<IRedisClient>(c => ClientsManager.GetClient())
            .InstancePerApiRequest();

        configuration.DependencyResolver
            = new AutofacWebApiDependencyResolver(builder.Build());
    }

    protected void Application_OnEnd()
    {
        ClientsManager.Dispose();
    }
}

咱們使用池鏈接管理器做爲IRedisClientsManager的實現。每當一個請求觸發時,就會得到一個新的客戶端實例,而且注入到資源庫中並且在請求結束時進行處理。

控制器

既然有了資源庫,那麼咱們來實現控制器-一個用於添加和獲取顧客另外一個用於管理訂單。

public class CustomersController : ApiController
{
    public ICustomerRepository CustomerRepository { get; set; }

    public IOrderRepository OrderRepository { get; set; }

    public IQueryable<Customer> GetAll()
    {
        return CustomerRepository.GetAll().AsQueryable();
    }

    public Customer Get(Guid id)
    {
        var customer = CustomerRepository.Get(id);
        if (customer == null)
        {
            throw new HttpResponseException(HttpStatusCode.NotFound);
        }

        return customer;
    }

    public HttpResponseMessage Post([FromBody] Customer customer)
    {
        var result = CustomerRepository.Store(customer);
        return Request.CreateResponse(HttpStatusCode.Created, result);
    }

    public HttpResponseMessage Put(Guid id, [FromBody] Customer customer)
    {
        var existingEntity = CustomerRepository.Get(id);
        if (existingEntity == null)
        {
            throw new HttpResponseException(HttpStatusCode.NotFound);
        }
        customer.Id = id;
        CustomerRepository.Store(customer);
        return Request.CreateResponse(HttpStatusCode.NoContent);
    }
}

public class OrdersController : ApiController
{
    public IOrderRepository OrderRepository { get; set; }
    public ICustomerRepository CustomerRepository { get; set; }

    public HttpResponseMessage Post([FromBody] Order order)
    {
        var customer = CustomerRepository.Get(order.CustomerId);
        var result = OrderRepository.Store(customer, order);
        return Request.CreateResponse(HttpStatusCode.Created, result);
    }

    [ActionName("top")]
    [HttpGet]
    public IDictionary<string, double> GetBestSellingItems(int count)
    {
        return OrderRepository.GetBestSellingItems(count);
    }

    [ActionName("customer")]
    [HttpGet]
    public IList<Order> GetCustomerOrders(Guid id)
    {
        return OrderRepository.GetCustomerOrders(id);
    }
}

這就是本文的全部內容,本文使用Redis做爲數據存儲並且依賴將自動鏈接。

源代碼託管在Bitbucket.

原文連接:http://www.piotrwalat.net/using-redis-with-asp-net-web-api/?utm_source=tuicool 本文同步在個人我的博客.


廣告時間

這兩年最重要的感悟就是,無論工做怎樣,都要好好對本身,特別是本身的身體;女朋友很體諒我成天對着電腦工做,讓我能天天堅持吃一個蘋果;最近還開了一家網店,淘寶店鋪名稱叫「碭山水果經營店8」,她家有個蘋果園,每一年10月份是蘋果的收穫季節,剛摘下的蘋果新鮮可口,又甜又脆,並且價格實惠;若是你們讀到這裏,但願你們前往淘寶店鋪看看,同時也但願你們能捧場^-^,在此先謝謝各位童鞋啦.

淘寶店鋪二維碼 enter image description here

相關文章
相關標籤/搜索