我正在使用 Azure Redis 和 StackExchange.Redis 来提供 Twitter 之类的新闻提要。这发生在我身上:
我将代码发布到我的云服务,一切正常,包含 60 个提要项的页面在 300 毫秒内加载,这对我来说太棒了。这可以工作一段时间,比如说几个小时,然后提要页面的加载速度会减慢到 8000 毫秒左右。
奇怪的是,当我在 localhost 上测试相同的代码时,它始终保持在 500 毫秒左右的速度。我连接到同一个 Redis 实例,它在美国,我在欧洲(云服务和 Redis 在同一个 Azure 区域 - 美国东部)。
通过 Azue 门户,我可以看到 Redis Server 负载始终低于 10%,CPU 使用率低于 40%。由于这个和 localhost 上的良好速度加载,我相信 Redis 没有任何问题,显然我在我的代码中做了一些愚蠢的事情,我看不到它,所以这是我的代码:
这是我如何初始化与 Redis 的连接:
public static class AzureRedisFeedsConnectionMultiplexer
{
private static readonly Lazy<ConnectionMultiplexer> LazyConnection =
new Lazy<ConnectionMultiplexer>(
() =>
{
var config = new ConfigurationOptions
{
AbortOnConnectFail = false,
Password = password,
EndPoints = { "myEndpoint" },
ConnectRetry = 5,
ConnectTimeout = 2 * 60 * 1000,
Ssl = true
};
return ConnectionMultiplexer.Connect(config);
});
private static ConnectionMultiplexer Connection
{
get { return LazyConnection.Value; }
}
public static ConnectionMultiplexer GetRedisConnection()
{
return Connection;
}
public static IDatabase GetRedisDatabase()
{
return Connection.GetDatabase();
}
}
这是 API 调用(调用我的 FeedService):
public async Task<List<FeedMobileHelper>> GetFeedForAllUsers(int page, int pageSize, int skip, int take)
{
return
await
_feedService.GetFeedWorkoutsFromRedisAsync(AzureRedisFeedsConnectionMultiplexer.GetRedisDatabase(),
User.Identity.GetUserId(), (page - 1)*pageSize, pageSize - 1);
}
这是 FeedService 中的这个方法:
public async Task<List<FeedMobileHelper>> GetFeedWorkoutsFromRedisAsync(IDatabase redis, string userId, int numOfItemsToSkip, int numOfItemsToTake)
{
var redisFeed = await redis.ListRangeAsync("FeedAllWorkouts", numOfItemsToSkip, numOfItemsToSkip + numOfItemsToTake);
/* WE KNOW FOR SURE TAHT THERE IS 10.000 ELEMENTS IN FEED SOTRED IN REDIS*/
//so if there is request for more (i.e. already gotten (numOfItemsToSkip) + numOfItemsToTake > 10.000 go to SQL
if (numOfItemsToSkip + numOfItemsToTake > 10000)
{
//some items arealready fetched from redis, so we have to skip those
var numOfItemsToGet = numOfItemsToSkip + numOfItemsToTake - 10000;
return await GetFeedWorkoutsMobileAsync(numOfItemsToSkip, numOfItemsToGet, userId);
}
// if length is zero go to SQL
if (redisFeed.Length <= 0)
{
return await GetFeedWorkoutsMobileAsync(numOfItemsToSkip, numOfItemsToSkip, userId);
}
return await CreateRedisFeedViewModelAsync(redisFeed, redis, userId);
}
我想添加到 SQL(即调用方法 GetFeedWorkoutsMobileAsync)永远不会发生,所以 FeedItems 总是从 Redis 获取。在 Redis 中,它们存储为序列化对象列表。
现在这里是 CreateRedisFeedViewModelAsync:
private async Task<List<FeedMobileHelper>> CreateRedisFeedViewModelAsync(RedisValue[] redisFeed, IDatabase redis, string userId)
{
// data to return
var listToReturn = new List<FeedMobileHelper>();
// feed in redis is serialized, we have to deserialize it
var feedDeserialized = new RedisFeedItemWorkout[redisFeed.Length];
// list of all workout ids that are needed for this feed page
var workoutIds = new string[redisFeed.Length];
//list of alll user ids needed for this feed page
var userIds = new string[redisFeed.Length];
var redisFeedService = new RedisFeedService();
using (var databaseContext = new MadBarzDatabaseContext())
{
// go to data from redis, deserialize everything, and allso fetch necessary ids
for (int i = 0; i < redisFeed.Length; i++)
{
var feedItem = JsonConvert.DeserializeObject<RedisFeedItemWorkout>(redisFeed[i]);
feedDeserialized[i] = feedItem;
workoutIds[i] = feedItem.WorkoutId;
userIds[i] = feedItem.UserId;
}
//go to redis and fetch data about users and workouts, idea here is that we don't go to redis seperately for each user, but we fetch data for all users
//and workouts needed to create this feed page
// long live redis pipeling
var redisFeedData = await redisFeedService.GetDataForFeed(redis, userId, workoutIds, userIds);
var workoutsFromRedis = redisFeedData.Workouts;
var usersFromRedis = redisFeedData.Users;
var commentsFromRedis = redisFeedData.Comments;
// in this for loop I just map and connect everything and if needed (i.e. if I don't get data from redis) I go and fetch data from SQL
// that almost never happens
for (int i = 0; i < feedDeserialized.Length; i++)
{
RedisWorkoutItem workout;
bool hasRespected;
string username, fullName, profilePic;
var userRedis = usersFromRedis[i];
var stringWorkout = workoutsFromRedis[i];
var workoutComment = commentsFromRedis[i].HasValue ? commentsFromRedis[i].ToString() : "";
if (userRedis != null)
{
profilePic = userRedis["ProfilePhotoUrl"].HasValue
? userRedis["ProfilePhotoUrl"].ToString()
: "";
fullName = userRedis["FirstName"] + " " + userRedis["LastName"];
username = userRedis["UserName"].HasValue ? userRedis["UserName"].ToString() : "";
}
else
{
var user = databaseContext.Users.Find(feedDeserialized[i].UserId);
profilePic = user.ProfilePhotoUrl;
username = user.UserName;
fullName = user.FirstName + " " + user.LastName;
}
if (stringWorkout.HasValue)
{
workout = JsonConvert.DeserializeObject<RedisWorkoutItem>(stringWorkout);
hasRespected = workout.UsersWhoRespected.Contains(userId);
}
else
{
var workoutGuid = Guid.Parse(feedDeserialized[i].WorkoutId);
var workoutFromDb = await databaseContext.Trenings.FindAsync(workoutGuid);
var routine = await databaseContext.AllRoutineses.FindAsync(workoutFromDb.AllRoutinesId);
workout = new RedisWorkoutItem
{
Name = routine.Name,
Id = workoutFromDb.TreningId.ToString(),
Comment = workoutFromDb.UsersCommentOnWorkout,
DateWhenFinished = workoutFromDb.DateTimeWhenTreningCreated,
NumberOfRespects = workoutFromDb.NumberOfLikes,
NumberOfComments = workoutFromDb.NumberOfComments,
UserId = workoutFromDb.UserId,
Length = workoutFromDb.LengthInSeconds,
Points = workoutFromDb.Score
};
workoutComment = workoutFromDb.UsersCommentOnWorkout;
hasRespected = databaseContext.TreningRespects
.FirstOrDefault(r => r.TreningId == workoutGuid && r.UserId == userId) != null;
}
string workoutLength;
if (workout.Length >= 3600)
{
var t = TimeSpan.FromSeconds(workout.Length);
workoutLength = $"{t.Hours:D2}:{t.Minutes:D2}:{t.Seconds:D2}";
}
else
{
var t = TimeSpan.FromSeconds(workout.Length);
workoutLength = $"{t.Minutes:D2}:{t.Seconds:D2}";
}
listToReturn.Add(new FeedMobileHelper
{
Id = feedDeserialized[i].Id.ToString(),
UserId = workout.UserId,
WorkoutId = feedDeserialized[i].WorkoutId,
Points = workout.Points.ToString("N0", new NumberFormatInfo
{
NumberGroupSizes = new[] { 3 },
NumberGroupSeparator = "."
}),
WorkoutName = workout.Name,
WorkoutLength = workoutLength,
NumberOfRespects = workout.NumberOfRespects,
NumberOfComments = workout.NumberOfComments,
WorkoutComment = workoutComment,
HasRespected = hasRespected,
UserImageUrl = profilePic,
UserName = username,
DisplayName = string.IsNullOrWhiteSpace(fullName) ? username : fullName,
TimeStamp = workout.DateWhenFinished,
DateFormatted = workout.DateWhenFinished.FormatDateToHrsDaysWeeksString()
});
}
}
return listToReturn;
}
我相信这种方法存在问题,但我无法识别它:
public async Task<RedisFeedDataModel> GetDataForFeed(IDatabase redisDb, string userId, string[] workoutIds, string[] userIds)
{
var listOfUsers = new Task<HashEntry[]>[userIds.Length];
var workoutsToReturn = new List<RedisValue>();
var usersToReturn = new List<Dictionary<RedisValue, RedisValue>>();
var commentsToReturn = new List<RedisValue>();
var listOfTaks = new List<Task<RedisValue>>();
for (var i = 0; i < userIds.Length; i++)
{
listOfTaks.Add(redisDb.StringGetAsync("workout:" + workoutIds[i]));
listOfTaks.Add(redisDb.StringGetAsync("workoutComment:" + workoutIds[i]));
listOfUsers[i] = redisDb.HashGetAllAsync("user:" + userIds[i]);
}
var resultForListOfTasks = await Task.WhenAll(listOfTaks);
var resultForListOfUsers = await Task.WhenAll(listOfUsers);
for (var i = 0; i < resultForListOfTasks.Length; i += 2)
{
workoutsToReturn.Add(resultForListOfTasks[i]);
commentsToReturn.Add( resultForListOfTasks[i + 1]);
}
for (int i = 0; i < resultForListOfUsers.Length; i++)
{
var redisUser = resultForListOfUsers[i];
var userToReturn = redisUser.Length > 0 ? redisUser.ToDictionary(t => t.Name, t => t.Value) : null;
usersToReturn.Add(userToReturn);
}
return new RedisFeedDataModel
{
Comments = commentsToReturn,
Workouts = workoutsToReturn,
Users = usersToReturn
};
}
有趣的是,当生产中的提要调用减慢时,如果我尝试使用相同的数据对本地主机上的同一个 RedisDB 进行相同的调用,它比生产上要快得多(例如在生产上持续 8000 毫秒,在本地主机上需要 500 毫秒)