Skip to content

AlbertZhaoz/tuling-hmdianping-net

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

8 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

hm-dianping-net

Redis 黑马点评项目 C# 版本实现。

项目如何启动:

  1. 打开【1_基础资料】在数据库中执行 hmdp.sql(用 MySQL 数据库)
  2. 打开【1_基础资料】进入到 nginx-1.18.0 文件夹下用命令行执行 start nginx.exe
  3. 安装 Redis(【1_基础资料】提供了 Linux 和 Windows 两个版本的 Redis,当然也可以通过 Docker 自行安装),并在 Redis 命令行中执行 xgroup create streamOrders g1 0-0 MKSTREAM创建消息队列,注意如果你是 Windows 上安装 Redis,请确保 Redis 版本高于 5.0(5.0 才支持消费者组消息队列和其他一些需要用到的高级功能)
  4. 打开【5_C# 代码】修改 appsettings.json 中数据库和 Redis 的连接字符串为个人的
  5. 启动项目执行单元测试 TestMigrationShopFromDb2Redis()将店铺数据预热到 Redis 中。

1 项目结构分析

此后端项目是基于 .NET8 WebApi 实现的,代码在

1.1 依赖包使用

  • Autofac.Extensions.DependencyInjection:Autofac 批量依赖注入、属性注入等功能
  • Autofac.Extras.DynamicProxy:Autofac 批量依赖注入、属性注入等功能
  • AutoMapper:映射功能
  • Mapster:配合 AutoMapper 做映射功能
  • DistributedLock.Redis:各类分布式锁(此项目只用了 Redis 相关的分布式锁),同时这个包底层用了 StackExchange.Redis 客户端
  • Masuit.Tools.Core:全能工具库,类似于 Java 中的 HuTool
  • Microsoft.Extensions.Configuration:配置基础套件
  • Microsoft.Extensions.Configuration.Json:配置 Json 基础套件
  • Microsoft.Extensions.DependencyInjection:依赖注入基础套件
  • Microsoft.Extensions.DependencyInjection.Abstractions:依赖注入基础套件
  • SqlSugar.IOC:SqlSugar IOC 注入
  • SqlSugarCore:SqlSugar ORM 框架

1.2 项目目录结构分析

image.png

2 核心代码

2.1 Autofac 依赖注入

// 1、配置host与容器
builder.Host
    .UseServiceProviderFactory(new AutofacServiceProviderFactory())
    .ConfigureContainer<ContainerBuilder>(builder =>
    {
        builder.RegisterModule(new AutofacModuleRegister());
    });
// 2. 替换默认的控制器由 Autofac 来创建
builder.Services.Replace(ServiceDescriptor.Transient<IControllerActivator, ServiceBasedControllerActivator>());


using System.Reflection;
using Autofac;
using Autofac.Extras.DynamicProxy;
using com.hmdp.attribute;
using com.hmdp.bgservices;
using com.hmdp.controller;
using com.hmdp.service.impl;

namespace com.hmdp.config;

public class AutofacModuleRegister: Autofac.Module
{
    protected override void Load(ContainerBuilder builder)
    {
        // 1. 批量注册所有服务层服务
        builder.RegisterAssemblyTypes(typeof(AutofacModuleRegister).Assembly)
            .Where(t => t.Name.EndsWith("Service"))
            .AsImplementedInterfaces()
            .PropertiesAutowired(new PropertySelector());
      
        // 2. 注册每一个控制器和抽象之间的关系
        var controllerBaseType = typeof(BaseController);
        builder.RegisterAssemblyTypes(typeof(AutofacModuleRegister).Assembly)
            .Where(t => controllerBaseType.IsAssignableFrom(t) && t != controllerBaseType)
            // 支持属性注入
            .PropertiesAutowired(new PropertySelector());
      
        #region 没有接口层的服务层注入
        //因为没有接口层,所以不能实现解耦,只能用 Load 方法。
        //注意如果使用没有接口的服务,并想对其使用 AOP 拦截,就必须设置为虚方法
        //var assemblysServicesNoInterfaces = Assembly.Load("Blog.Core.Services");
        //builder.RegisterAssemblyTypes(assemblysServicesNoInterfaces);
        // // 1. 注册所有服务层服务
        // builder.RegisterGeneric(typeof(BaseService<>))
        //     .As(typeof(IBaseService<>))
        //     .InstancePerDependency(); 
        //
        // builder.RegisterAssemblyTypes(Assembly.GetAssembly(typeof(AutofacModuleRegister)))
        //     .AsImplementedInterfaces()
        //     .InstancePerDependency()
        //     .PropertiesAutowired(); //允许将拦截器服务的列表分配给注册。
        // builder.RegisterType<ShopService>().As<IShopService>()
        //     .AsImplementedInterfaces()
        //     .InstancePerDependency();

        // 2. 注册 ShopService 类作为 IShopService 接口的实现
        // 获取 Service.dll 程序集服务,并注册
        // var assemblysServices = Assembly.LoadFrom(servicesDllFile);
        // builder.RegisterAssemblyTypes(assemblysServices)
        //     .AsImplementedInterfaces()
        //     .InstancePerDependency()
        //     .PropertiesAutowired()
        //     .EnableInterfaceInterceptors()       //引用Autofac.Extras.DynamicProxy;
        //     .InterceptedBy(cacheType.ToArray()); //允许将拦截器服务的列表分配给注册。
        #endregion

        #region 没有接口的单独类,启用class代理拦截
        //只能注入该类中的虚方法,且必须是public
        //这里仅仅是一个单独类无接口测试,不用过多追问
        // builder.RegisterAssemblyTypes(Assembly.GetAssembly(typeof(Love)))
        //     .EnableClassInterceptors()
        //     .InterceptedBy(cacheType.ToArray());
        #endregion

        #region 单独注册一个含有接口的类,启用interface代理拦截
        //不用虚方法
        //builder.RegisterType<AopService>().As<IAopService>()
        //   .AsImplementedInterfaces()
        //   .EnableInterfaceInterceptors()
        //   .InterceptedBy(typeof(BlogCacheAOP));
        #endregion
    }
}

[AttributeUsage(AttributeTargets.Property)]
public class AutoWireAttribute:Attribute
{
  
}

public class PropertySelector:Autofac.Core.IPropertySelector
{
    public bool InjectProperty(PropertyInfo propertyInfo, object instance)
    {
        return propertyInfo.GetCustomAttribute<AutoWireAttribute>() != null;
    }
}

2.2 AutoMapper 映射

// AutoMapper Config
builder.Services.AddutoMapperSetup();

public static class AutoMapperExtension
{
    public static void AddutoMapperSetup(this IServiceCollection services)
    {
        services.AddAutoMapper(typeof(AutoMapperConfig));
    }
}

/// <summary>
/// 静态全局 AutoMapper 配置文件
/// </summary>
public class AutoMapperConfig:Profile
{
    /// <summary>
    /// 配置构造函数,用来创建关系映射
    /// </summary>
    public AutoMapperConfig()
    {
        // 如果想要使用直接注入 IMapper.Mapper 即可
        CreateMap<tb_user, tb_user_dto>();
        // CreateMap<SysUserInfo, SysUserInfoDto>()
        // .ForMember(a => a.uID, o => o.MapFrom(d => d.Id))
        // .ForMember(a => a.RIDs, o => o.MapFrom(d => d.RIDs))
    }
}

// 调用 IMapper 注入即可

2.3 一人一单+超卖问题实现

using Castle.Components.DictionaryAdapter.Xml;
using com.hmdp.attribute;
using com.hmdp.Const;
using com.hmdp.dto;
using com.hmdp.entity;
using com.hmdp.repo;
using com.hmdp.utils;
using StackExchange.Redis;

namespace com.hmdp.service.impl;

/// <summary>
/// 这个服务有很复杂的业务逻辑,里面会涉及到分布式锁,事务,缓存,消息队列等等
/// </summary>
public class VoucherOrderService:BaseService<tb_voucher_order>,IVoucherOrderService
{
    [AutoWire] public ISeckillVoucherService seckillVoucherService { get; set; }
    [AutoWire] public IDatabase redisDb { get; set; }
  
    private string luaScript = @"
            -- 0.判断消息队列是否存在,不存在则创建
            if(not redis.call('exists', KEYS[1])) then
                redis.call('XGROUP', 'CREATE', KEYS[1], 'g1', '0-0', 'MKSTREAM')
            end

            -- 1.参数列表
            -- 1.1.优惠券id
            local voucherId = ARGV[1]
            -- 1.2.用户id
            local userId = ARGV[2]
            -- 1.3.订单id
            local orderId = ARGV[3]
          
            -- 2.数据key
            -- 2.1.库存key
            -- 'seckill:stock:'
            local stockKey = KEYS[2] .. voucherId
            -- 2.2.订单key
            -- 'seckill:order:'
            local orderKey = KEYS[3] .. voucherId
          
            -- 3.脚本业务
            -- 3.1.判断库存是否充足 get stockKey
            if(tonumber(redis.call('get', stockKey)) <= 0) then
                -- 3.2.库存不足,返回1
                return 1
            end
            -- 3.2.判断用户是否下单 SISMEMBER orderKey userId
            if(redis.call('sismember', orderKey, userId) == 1) then
                -- 3.3.存在,说明是重复下单,返回2
                return 2
            end
            -- 3.4.扣库存 incrby stockKey -1
            redis.call('incrby', stockKey, -1)
            -- 3.5.下单(保存用户)sadd orderKey userId
            redis.call('sadd', orderKey, userId)
            -- 3.6.发送消息到队列中, XADD stream.orders * k1 v1 k2 v2 ...
            redis.call('xadd', KEYS[1], '*', 'userId', userId, 'voucherId', voucherId, 'id', orderId)
            return 0
    ";
  
    public async Task<Result> SeckillVoucher(ulong voucherId)
    {
        var userId = UserHolder.GetUser().id;
        var redisIdWorker = new RedisIdWorker(redisDb);
        var orderId = redisIdWorker.NextId(RedisConst.ORDER);
      
        // 1.执行 Lua 脚本:这里面有一个消息队列,这段脚本中包含消息队列不存在则创建
        var result = await redisDb.ScriptEvaluateAsync(luaScript,
            new RedisKey[] {
                RedisConst.MESSAGE_STREAM_KEY,
                RedisConst.SECKILL_STOCK_KEY, 
                RedisConst.SECKILL_ORDER_KEY}, 
            new RedisValue[] {voucherId, userId, orderId});
      
        // 2.解析结果
        if (result.ToString() != "0")
        {
            // 2.1.不为0 ,代表没有购买资格
            return Result.Fail(result.ToString() == "1" ? "库存不足" : "不能重复下单");
        }

        return Result.Success(orderId);
    }
}
using System.Linq.Expressions;
using com.hmdp.attribute;
using com.hmdp.Const;
using com.hmdp.entity;
using com.hmdp.service;
using com.hmdp.utils;
using SqlSugar.IOC;
using StackExchange.Redis;

namespace com.hmdp.bgservices;

public class SekillOrderBgService:BackgroundService
{
    public IDatabase _redisDb { get; set; }
    public IVoucherOrderService _voucherOrderService { get; set; }
    public ISeckillVoucherService _seckillVoucherService { get; set; }
    public ILogger<SekillOrderBgService> _log { get; set; }
  
    public SekillOrderBgService(IDatabase redisDb, IVoucherOrderService voucherOrderService, ISeckillVoucherService seckillVoucherService, ILogger<SekillOrderBgService> log)
    {
        _redisDb = redisDb;
        _voucherOrderService = voucherOrderService;
        _seckillVoucherService = seckillVoucherService;
        _log = log;
    }
  
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (true)
        {
            try
            {
                // 1.获取消息队列中的订单信息 XREADGROUP GROUP g1 c1 COUNT 1 [BLOCK 2000] STREAMS MESSAGE_STREAM_KEY >
                // https://github.com/StackExchange/StackExchange.Redis/issues/1109
                var entrys = _redisDb.StreamReadGroup(
                    RedisConst.MESSAGE_STREAM_KEY, "g1", "c1", ">", 1, false);
                  
                // 2.判断订单信息是否为空
                if (entrys == null || entrys.Length == 0)
                {
                    await Task.Delay(2000);
                    continue;
                }
                  
                // 3.解析出订单 id 处理订单信息
                var entry = entrys[0];
                var order = new tb_voucher_order();
              
                if (entry.Values.Length > 2)
                {
                    order.user_id = entry.Values[0].Value.ObjToLong();
                    order.voucher_id = entry.Values[1].Value.ObjToLong();
                    order.id = entry.Values[2].Value.ObjToLong();
                    order.create_time = DateTime.Now;
                    order.update_time = DateTime.Now;
                }
              
                // 3.创建订单会用到分布式锁
                await CreateVoucherOrder(order);

                await Task.Delay(2000);
            }
            catch (Exception e)
            {
                _log.LogError(e.Message);
            }
        }
    }

    private async Task CreateVoucherOrder(tb_voucher_order order)
    {
        var userId = order.user_id;
        var voucherId = order.voucher_id;
        // 创建锁对象
        var isLock = await _redisDb.LockTakeAsync(RedisConst.LOCK_ORDER+ userId, voucherId, TimeSpan.FromSeconds(50));
        // 判断
        if (!isLock) {
            // 获取锁失败,直接返回失败或者重试
            _log.LogError("获取锁失败,直接返回失败或者重试");
            return;
        }

        try
        {
            // 5.1.查询订单
            var count = await _voucherOrderService.Count(a =>
                a.user_id == userId &&
                a.voucher_id == voucherId);

            // 5.2.判断是否存在
            if (count > 0)
            {
                // 用户已经购买过了
                _log.LogError("不允许重复下单!");
                return;
            }

            await DbScoped.SugarScope.Ado.BeginTranAsync();
            // 6.扣减库存
            var updateLine = await _seckillVoucherService.Db
                .Updateable<tb_seckill_voucher>()
                // set stock = stock - 1
                .SetColumns(x => new tb_seckill_voucher()
                {
                    stock = x.stock - 1
                })
                // where id = ? and stock > 0
                .Where(x => x.voucherId == voucherId && x.stock > 0)
                .ExecuteCommandAsync();

            if (updateLine < 1)
            {
                // 扣减失败
                _log.LogError("库存不足!");
                return;
            }

            // 7.创建订单
            await _voucherOrderService.Add(order);
          
            // 8.提交事务
            await DbScoped.SugarScope.Ado.CommitTranAsync();
        }
        catch (Exception e)
        {
            // 回滚事务
            await DbScoped.SugarScope.Ado.RollbackTranAsync();
            _log.LogError(e.Message);
        }
        finally {
            // 释放锁
            await _redisDb.LockReleaseAsync(RedisConst.LOCK_ORDER+ userId, voucherId);
        }
    }
}

2.4 缓存穿透解决方案+GEO 类型使用

using System.Linq.Expressions;
using System.Text.Json;
using System.Text.Json.Serialization;
using com.hmdp.attribute;
using com.hmdp.Const;
using com.hmdp.dto;
using com.hmdp.entity;
using com.hmdp.repo;
using com.hmdp.utils;
using Masuit.Tools;
using StackExchange.Redis;

namespace com.hmdp.service.impl;

public class ShopService:BaseService<tb_shop>,IShopService
{
    [AutoWire]
    public IDatabase RedisDb { get; set; }
  
    public async Task<Result> QueryById(long id)
    {
        var result = await QueryWithPassThrough(RedisConst.CACHE_SHOP_KEY+id,id);
      
        return result;
    }

    public async Task<Result> Update(tb_shop shop)
    {
        var id = shop.id;

        if (id < 0)
        {
            return Result.Fail("id 不能小于 0");
        }
      
        // 这边要保证事务
        try
        {
            await base.Db.Ado.BeginTranAsync();
            // 1. 更新数据库
            var result = await base.Update(shop);
            // 2. 删除缓存
            RedisDb.KeyDelete(RedisConst.CACHE_SHOP_KEY+id);
            await base.Db.Ado.CommitTranAsync();
        }
        catch (Exception e)
        {
            await base.Db.Ado.RollbackTranAsync();
            return Result.Fail(e.Message);
        }

        return Result.Success();
    }

    public async Task<Result>  QueryShopByType(int typeId, int current, double? x, double? y)
    {
         // 1.判断是否需要根据坐标查询
        if (x == null || y == null) {
            // 不需要坐标查询,按数据库查询
            var page = await base.QueryPage(a => 
                a.typeId == (ulong)typeId, current, SystemConst.DEFAULT_PAGE_SIZE);
          
            return Result.Success(page.data);
        }

        // 2.计算分页参数
        int from = (current - 1) * SystemConst.DEFAULT_PAGE_SIZE;
        int end = current * SystemConst.DEFAULT_PAGE_SIZE;

        // 3.查询redis、按照距离排序、分页。结果:shopId、distance
        string key = RedisConst.SHOP_GEO_KEY + typeId;
      
        // GEOSEARCH key BYLONLAT x y BYRADIUS 10 WITHDISTANCE
        var results = RedisDb.GeoRadius(
            key, (double)x, (double)y,5000L, GeoUnit.Meters, end, 
            Order.Ascending,
            GeoRadiusOptions.WithDistance);
      
        // 4.没有下一页了
        if (results == null && results?.Length <= from){
            return Result.Success(Enumerable.Empty<tb_shop>());
        }

        // 4.1.截取 from ~ end的部分
        var list = results.Skip(from).Take(end - from).ToList();
        // 4.2.获取店铺id
        var ids = list.Select(a => ulong.Parse(a.Member)).ToList();
        // 4.3.获取距离
        var distanceMap = list.ToDictionary(a => a.Member, a => a.Distance);
        // 5.根据id查询Shop
        Expression<Func<tb_shop,bool>> where = a => ids.Contains(a.id);
        var shops = await base.Query(where);
      
        // 6.设置距离
        foreach (var shop in shops) {
            shop.distance = distanceMap[shop.id.ToString()].Value;
        }
      
        // 6.返回
        return Result.Success(shops);
    }

    /// <summary>
    /// 缓存穿透解决方案
    /// </summary>
    /// <returns></returns>
    private async Task<Result> QueryWithPassThrough(string key,long id)
    {
        // 1.从redis查询商铺缓存
        var shopCache  = RedisDb.StringGet(key);

        tb_shop? shop = null;
      
        // 2. 判断 shopCache 是否存在,存在则反序列化给前端
        if (!string.IsNullOrEmpty(shopCache))
        {
            // 如果查找到是自定义的缓存穿透字符串,说明是我们缓存的空数据
            if (shopCache == RedisConst.CACHE_NULL_VALUE)
            {
                return Result.Fail("查询空值,请检查 id");
            }
          
            // 3. 存在,序列化返回
            try
            {
                shop = JsonSerializer.Deserialize<tb_shop>(shopCache);
                return Result.Success(shop);
            }
            catch (Exception e)
            {
                return Result.Fail(e.Message);
            }
         
        }
      
        // 如果缓存不存在(连空字符串都没有),则查询数据库
        shop = await base.QueryById(id);

        if (shop == null)
        {
            //这里的常量值是 2 分钟,如果从缓存中查询不到则写 ""
            RedisDb.StringSet(key, RedisConst.CACHE_NULL_VALUE, TimeSpan.FromMinutes(2));
            return Result.Fail("店铺不存在!!");
        }
      
        // 如果存在则设置过期时间 30 分钟
        RedisDb.StringSet(key, shop.ToJsonString(), TimeSpan.FromMinutes(30));
        return Result.Success(shop);
    }
}

2.5 排行榜实现 SortedSet

using System.Linq.Expressions;
using AutoMapper;
using com.hmdp.attribute;
using com.hmdp.Const;
using com.hmdp.dto;
using com.hmdp.entity;
using com.hmdp.repo;
using com.hmdp.utils;
using Masuit.Tools;
using Microsoft.AspNetCore.Authorization;
using NetTaste;
using StackExchange.Redis;

namespace com.hmdp.service.impl;

public class BlogService:BaseService<tb_blog>,IBlogService
{
    [AutoWire]
    public IUserService userService { get; set; }
    [AutoWire]
    public IDatabase redisDb { get; set; }
    [AutoWire]
    public IFollowService followService { get; set; }
    [AutoWire]
    public IMapper mapper { get; set; }
  
    public async Task<Result> QueryHotBlog(int current)
    {
        var queryPage = await base.QueryPage(a => 
            true, current, 10, "liked desc");

        var result = queryPage.data.Select(blog =>
        {
            QueryBlogUser(blog);
            IsBlogLiked(blog);
          
            return blog;
        }).ToList();
      
        return Result.Success(result);
    }

    public async Task<Result> QueryBlogById(long id)
    {
        // 1.查询 blog
        var blog = await base.QueryById(id);
      
        if (blog == null)
        {
            return Result.Fail("博客不存在");
        }
      
        // 2.查询 blog 有关的用户
        await QueryBlogUser(blog);
        // 3.查询 blog 是否被当前登录用户点赞
        await IsBlogLiked(blog);

        return Result.Success(blog);
    }

    public async Task<Result> LikeBlog(long id)
    {
        // 1.获取登录用户
        var userId = UserHolder.GetUser().id;
        // 2.判断当前登录用户是否已经点赞
        var key = RedisConst.USER_BLOG_LIKE + id;
        var sortedSetScore = redisDb.SortedSetScore(key,userId);
      
        if (sortedSetScore == null) {
            // 3.如果未点赞,可以点赞
            // 3.1.数据库点赞数 + 1
            var blog = await base.QueryById(id);
            blog.liked += 1;
            bool isSuccess = await base.Update(blog);
          
            // 3.2.保存用户到Redis的set集合  zadd key value score
            if (isSuccess)
            {
                redisDb.SortedSetAdd(key, userId, DateTimeOffset.Now.ToUnixTimeMilliseconds());
            }
        } else {
            // 4.如果已点赞,取消点赞
            // 4.1.数据库点赞数 -1
            var blog = await base.QueryById(id);
            blog.liked -= 1;
            bool isSuccess = await base.Update(blog);
            // 4.2.把用户从Redis的set集合移除
            if (isSuccess) {
                redisDb.SortedSetRemove(key, userId);
            }
        }

        return Result.Success();
    }

    public async Task<Result> QueryBlogLikes(long id)
    {
        string key = RedisConst.USER_BLOG_LIKE + id;
        // 1.查询top5的点赞用户 zrange key 0 4
        var top5 = redisDb.SortedSetRangeByRank(key, 0, 4);
      
        if (top5 == null && top5.IsNullOrEmpty()) {
            return Result.Success(Enumerable.Empty<string>());
        }
        // 2.解析出其中的用户id
        var ids = top5.Select(x => x.ToString()).Select(ulong.Parse).ToList();
        String idStr = String.Join(",", ids);
        // 3.根据用户id查询用户 WHERE id IN ( 5 , 1 ) ORDER BY FIELD(id, 5, 1)
        Expression<Func<tb_user, bool>> expression = a => ids.Contains(a.id);

        var userList = await userService.QueryByIDsWithExpression(expression);
        var userDtoList = userList.Select(x=>mapper.Map<tb_user_dto>(x)).ToList();
      
        // 4.返回
        return Result.Success(userDtoList);
    }

    public async Task<Result> SaveBlog(tb_blog blog)
    {
        // 1.获取登录用户
        var user = UserHolder.GetUser();
        blog.userId = user.id;
        // 2.保存探店笔记
        var count = await base.Add(blog);
      
        if(count<1){
            return Result.Fail("新增笔记失败!");
        }
      
        // 3.查询笔记作者的所有粉丝 select * from tb_follow where follow_user_id = ?
        var follows = await followService.Query(x=>x.followUserId == user.id);
      
        // 4.推送笔记id给所有粉丝
        foreach (var follow in follows)
        {
            // 4.1.获取粉丝id
            var userId = follow.userId;
            // 4.2.推送
            var key = RedisConst.FEED_KEY + userId;
            redisDb.SortedSetAdd(key, blog.id, DateTimeOffset.Now.ToUnixTimeMilliseconds());
        }
      
        // 5.返回id
        return Result.Success(blog.id);
    }

    public async Task<Result> QueryBlogOfFollow(long max, int offset)
    {
        // 1.获取当前用户
        var userId = UserHolder.GetUser().id;
        // 2.查询收件箱 ZREVRANGEBYSCORE key Max Min LIMIT offset count
        string key = RedisConst.FEED_KEY + userId;
      
        var sortedSetEntries = redisDb.SortedSetRangeByScoreWithScores(
            key, 0, max,Exclude.Start, Order.Descending,offset,2);

        // 3.非空判断
        if (sortedSetEntries.IsNullOrEmpty()) {
          
            return Result.Success();
        }
      
        // 4.解析数据:blogId、minTime(时间戳)、offset
        var ids = new List<ulong>(sortedSetEntries.Length);
        long minTime = 0; // 2
        int os = 1; // 2

        foreach (var sortedSetEntry in sortedSetEntries)
        {
            // 4.1.获取id
            ids.Add(ulong.Parse(sortedSetEntry.Element));
            // 4.2.获取分数(时间戳)
            long time = (long)sortedSetEntry.Score;
            if(time == minTime){
                os++;
            }else{
                minTime = time;
                os = 1;
            }
        }

        // 5.根据id查询blog,能够实现 ORDER BY FIELD(id," + idStr + ")" 按照给的 id 的顺序排序
        Expression<Func<tb_blog, bool>> expression = a => ids.Contains(a.id);
        var blogs = await base.QueryByIDsWithExpression(expression);

        foreach (var blog in blogs)
        {
            // 5.1.查询blog有关的用户
            await QueryBlogUser(blog);
            // 5.2.查询blog是否被点赞
            await IsBlogLiked(blog);
        }

        // 6.封装并返回
        ScrollResult r = new ScrollResult();
        r.list = blogs.Select(a => (object)a).ToList();
        r.offset = os;
        r.minTime = minTime;

        return Result.Success(r);
    }

    private async Task QueryBlogUser(tb_blog blog)
    {
        var userId = blog.userId;
        var user = await userService.QueryById(userId);
        blog.name = user.nick_name;
        blog.icon = user.icon;
    }

    private async Task IsBlogLiked(tb_blog blog)
    {
        // 1. 获取当前登录用户
        var user = UserHolder.GetUser();
      
        if (user == null)
        {
            return;
        }
      
        // 2. 判断当前登录用户是否已经点赞
        var key = RedisConst.USER_BLOG_LIKE + blog.id;
        var sortedSetScore = redisDb.SortedSetScore(key,user.id);
        blog.isLike = sortedSetScore != null;
    }
}

2.6 用户签到 BitMap 使用

    public async Task<Result> Sign()
    {
        var (key,day) = GetSignKeyAndDay();
        // 5.签到:写入Redis SETBIT key offset 1
        RedisDb.StringSetBit(key,day-1,true);
      
        return Result.Success();
    }

    public async Task<Result> SignCount()
    {
        var (key,day) = GetSignKeyAndDay();

        return Result.Success();
    }

    private (string,int) GetSignKeyAndDay()
    {
        // 1.获取当前登录用户
        var userId = UserHolder.GetUser().id;
        // 2.获取日期
        var now = DateTime.Now.ToString(":yyyyMM");
        // 3.拼接 key
        var key  = RedisConst.USER_SIGN_KEY+ userId + now;
        // 4.获取今天是本月的第几天
        var day = DateTime.Now.Day;
        // 5.获取本月截止今天为止的所有的签到记录,返回的是一个十进制的数字 BITFIELD sign:5:202203 GET u14 0
        return (key,day);
    }

About

Redis 黑马点评项目 C# 版本实现。

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published