LRU Cache在柚子加速器中的实践

Published on 4/9/2019 8:21:24 AM

Backgrounds

一个软件系统由多部分组成:Web Service、前端、数据库等,在一个大规模的软件系统中,我们需要考虑在某一部分发生故障时,对整体业务的影响是多大。比如在数据库无法连接时,我们的柚子加速器系统会有哪些部分受到影响导致用户无法使用?

LRU是Least Recently Used的缩写,LRU Cache是一个容积固定,在溢出时将历史中最古老的元素置换。

Onboard

我们希望在数据库不可用时,已经付费的用户仍可正常登录使用,而在数据库故障时,我们可以接受无法注册新用户,因此用户相关的信息、游戏列表、线路列表等信息是需要被Cache的,而这个LRU Cache的容积应与日活量成正比。

由于上述需求,开发了Pomelo.Framework.LRUCache这个库,目前还没有发布到NuGet,敬请期待。

首先向项目中添加Pomelo.Framework.LRUCache的引用,并在Startup中使用AddLRUCache()自动添加程序集中所有实现Pomelo.Framework.LRUCache.ICache<,>接口的缓存对象。

services.AddLRUCache();

以柚子加速器业务中用户和Session为例,建立了如下两个Cache,在使用Pomelo.Framework.LRUCache时,只需继承LRUCacheBase类即可,该LRU Cache可以实现O(1)的时间复杂度读写缓存,并使用O(n)的空间复杂度。

UserCache.cs

using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Pomelo.Framework.LRUCache;
using Pomelo.Booster.Management.Models;

namespace Pomelo.Booster.Management.WebApi.Cache
{
    public class UserCache : LRUCacheBase<string, User>
    {
        private BoosterContext db;

        public UserCache(BoosterContext db)
        {
            this.db = db;
            this.Capacity = 100;
        }

        public override Task<User> GetModelFromSourceAsync(string key)
        {
            return db.Users.SingleAsync(x => x.Username == key);
        }
    }
}

GetModelFromSourceAsync是一个LRUCacheBase类的虚方法,重写该方法可以使用该类下的Refresh相关方法,来刷新缓存,也可在缓存中无指定key对应的对象时自动获取对象。不重新该方法则无法使用上述功能,需要手动调用Put方法向LRU Cache中添加缓存对象。

SessionCache.cs

using System;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Pomelo.Framework.LRUCache;
using Pomelo.Booster.Management.Models;

namespace Pomelo.Booster.Management.WebApi.Cache
{
    public class WebSessionCache : LRUCacheBase<Guid, WebSession>
    {
        private BoosterContext db;

        public WebSessionCache(BoosterContext db)
        {
            this.db = db;
            this.Capacity = 400;
        }

        public override Task<WebSession> GetModelFromSourceAsync(Guid key)
        {
            return db.WebSessions.SingleAsync(x => x.Id == key);
        }
    }
}

在获取用户时,直接通过Cache对象进行获取:

public User CurrentUser
{
    get
    {
        var cache = HttpContext.RequestServices.GetRequiredService<ICache<string, User>>();
        if (_currentUser == null && User.Identity.IsAuthenticated)
        {
            _currentUser = cache.RefreshAndGet(User.Identity.Name.ToLower());
        }

        return _currentUser;
    }
    internal set
    {
        this._currentUser = value;
    }
}

使用RefreshAndGet则先行尝试刷新缓存,如果刷新失败则将缓存中的内容返回,此举是为了保障多个服务无状态运行,例如在A服务中用户更改了密码,B服务先尝试刷新缓存的对象,保持数据的一致性,而如果在数据库故障时,返回缓存过的对象可以增强系统的可用性。而在游戏图标缓存中,则只使用Get方法,保证每次都从缓存中读取游戏的图标,因为图标不会经常变,就算变了读到老图标也不会对用户体验造成过大的影响。

为了方便监控各个缓存的使用情况,我还在Controller里写了一个方法来获取各个缓存的使用情况:

public IActionResult Get()
{
    var ret = new List<GetSystemCacheResponseV2>();

    var types = AppDomain.CurrentDomain
        .GetAssemblies()
        .SelectMany(x => x.GetTypes())
        .Where(x => x.IsClass && !x.IsAbstract)
        .Where(x => x.GetInterface(typeof(ICache<,>).FullName) != null)
        .ToList();

    foreach (var x in types)
    {
        var inter = x.GetInterface(typeof(ICache<,>).FullName);
        var serviceType = typeof(ICache<,>).MakeGenericType(inter.GetGenericArguments());
        var cachedProperty = serviceType.GetProperty("Cached");
        var cache = HttpContext.RequestServices.GetService(serviceType);
        if (cache != null)
        {
            var cached = (IEnumerable<object>)cachedProperty.GetValue(cache);
            ret.Add(new GetSystemCacheResponseV2
            {
                Name = x.Name,
                Cached = cached.Count()
            });
        }
    }

    return ApiResult(ret);
}

返回结果如下:

{
    "code": 200,
    "data": [
        {
            "name": "GameIconCache",
            "cached": 33
        },
        {
            "name": "UserCache",
            "cached": 6
        },
        {
            "name": "WebSessionCache",
            "cached": 22
        }
    ],
    "msg": null
}

此外,你还可以通过修改Cache容器的Capacity属性来调整缓存容积。

Share to:

Comments

使用微信扫码