C#-抢红包功能的分布式情况下处理多并发

需求

需求经理设计了一个分享出去后,可以在微信群中抢优惠的活动。
简单来说,就是每个参与活动的商品可以生成一个红包池,分享到群里后,可以像抢红包一样,去抢优惠金额。

级别

重要
双十一活动是本活动第一次尝试正式投入使用,领导很注重。
此活动的开发是为后面的限时/限量的抢购/秒杀活动累计经验和设计思路。直接影响后续的活动设计与开发。

问题

接口很快就根据需求设计开发出来了,并完善了相关活动规则。

  1. 但是多并发情况下,分享出去的红包池的,可抢次数与可抢金额,控制的并不严格,会出现多抢几次或多抢金额的情况。
  2. 并且希望当当前用户因某些原因抢不到红包时,能够快速响应,不必走完全部代码。
  3. 服务使用的是分布式架构,单纯的Lock无法锁住资源。
  4. Redis暂未开放Lua脚本,无法开发带有逻辑的原子性操作。

思路

阻塞锁
非阻塞锁
redis缓存

方案

  • 在商品详情页参与活动,生成红包池。生成红包池后,先将红包池信息写入redis,再返回给前端。

为了合理利用缓存空间,缓存失效时间设置为当前时间到活动结束时间,并加一天。加一天是为了防止边缘时间出现问题。

int dateDiffDays = checkBeforeCreateRedPacke.EndTime.Subtract(DateTime.Now).Days + 1; //当前时间到活动结束时间的天数+1
CacheProvider.SetEntry(string.Format(CacheKey.RedPackSurplusNum, entity.ID), (checkBeforeCreateRedPacke.RedPacketNum + 1).ToString(), TimeSpan.FromDays(dateDiffDays)); //缓存红包池剩余可抢次数
CacheProvider.SetEntry(string.Format(CacheKey.RedPackSurplusMoney, entity.ID), (checkBeforeCreateRedPacke.GoodPrice).ToString(), TimeSpan.FromDays(dateDiffDays)); //缓存红包池剩余金额
  • 抢红包,校验红包活动和红包池的有效性后。第一道阻流。用redis实现的分布式阻塞锁。保证等待队列最大长度为一个红包池的可枪次数。

校验红包池剩余可抢次数。此处同时作为队列排队作用。需要在redis中达到查询并添加一排队人数的作用。
因为redis还没有放开插件,无法开发Lua插件,所以使用的是redis自带的自减功能。这也是前面缓存剩余可抢次数和剩余金额分开的原因,方便进行自减操作。
剩余次数减1并返回减1后的结果。
若自减后返回结果小于1则为该红包池已抢完,程序返回结果。
若自减后返回结果大于1则为红包池可抢,进入队列。
若自减后返回结果等于1则为剩余最后一个红包,直接将剩余金额作为抢到金额,进入队列。

PS:redis的自减操作是一个原子性操作,可以解决目前我的排队问题,但是它在redis查询无果时,会创建一个初始值为0的数据,减一后返回。这就导致一个问题,当这个自减操作返回-1时,我如何判断是redis丢失数据导致的,还是正常减1减至-1导致的。任何时候都要对于第三方功能的异常情况的进行考虑

redis自减操作:

//缓存控制抢红包排队序列
int surplusNum = (int)CacheProvider.DecrementValue(string.Format(CacheKey.RedPackSurplusNum, filter.RPId));
if (surplusNum < 1)
{
//红包池中的红包已经被抢完
data.State = 4;
}
else
{
CheckInfo.SurplusNum = surplusNum;
}

进入队列:

//添加等待数据锁key
string lockKey = string.Format("ECC:{0}:{1}", "GrabRedPack", filter.RPId);
//加分布式阻塞锁 无锁添加锁,有锁等待释放
CacheProvider.AddWaitLock(lockKey);

封装的自减操作:

/// <summary>
/// 数值自减1
/// </summary>
/// <param name="key">键</param>
/// <param name="value">值</param>
/// <param name="expiresIn">超时时间</param>
/// <returns></returns>
public static long DecrementValue(string key)
{
try
{
using (ICacheClient client = cacheService.GetClient())
{
return client.DecrementValue(key);
}
}
catch { return -1; }
}
/// <summary>
/// 数值减值
/// </summary>
/// <param name="key">键</param>
/// <param name="value">值</param>
/// <param name="expiresIn">超时时间</param>
/// <returns></returns>
public static long DecrementValueBy(string key, int count)
{
try
{
using (ICacheClient client = cacheService.GetClient())
{
return client.DecrementValueBy(key, count);
}
}
catch { return -1; }
}
/// <summary>
/// 数值加值
/// </summary>
/// <param name="key">键</param>
/// <param name="value">值</param>
/// <returns></returns>
public static long IncrementValueBy(string key, int count)
{
try
{
using (ICacheClient client = cacheService.GetClient())
{
return client.IncrementValueBy(key, count);
}
}
catch { return -1; }
}

PSUSING关键字用法。此处使用了using的第三种用法:
using语句允许程序员指定使用资源的对象应当何时释放资源.using语句中使用的对象必须实现IDisposable接口.此接口提供了Dispose方法,该方法将释放此对象的资源
使用规则:
a) using语句只能用于实现了IDisposable接口的类型,禁止为不支持IDisposable接口类型使用using语句,否则会出现编译错误
b) using语句适用于清理单个非托管资源的情况,而多个非托管对象的清理最好以try-finaly来实现,因为嵌套using语句可能存在隐藏的Bug.内层using块引发异常时,将不能释放外层using块的对象资源
using实质:
在程序编译阶段,编译器会自动将using语句生成try-finally语句,并在finally块中调用对象的Dispose方法,来清理资源.所以,using语句等效于try-finally语句

redis自减基层方法(解释为啥catch中返回-1):

//
// 摘要:
// Decrements the number stored at key by one. If the key does not exist, it is
// set to 0 before performing the operation.
//
// 参数:
// key:
long DecrementValue(string key);
//
// 摘要:
// Decrements the number stored at key by decrement. If the key does not exist,
// it is set to 0 before performing the operation.
//
// 参数:
// key:
//
// count:
long DecrementValueBy(string key, int count);
  • 第二道防剩余金额并发。

队列中等待进程开始执行。
先重新从redis中获取最新的剩余金额。因为上一个进程生成红包后剩余金额有变化。
在进行业务逻辑后,生成了红包后,更新redis的红包池剩余金额。
然后可以释放锁。剩余的就是数据入库操作,不必锁资源。
当然队列进行开始到生成红包金额,全部放在try-cathc语句中执行。

PS:一定要注意程序的闭环。加锁一定要解锁。

//锁开始
try
{
//生成抢红包金额
//更新剩余金额
}
catch (Exception)
{}
finally
{
//移除锁
CacheProvider.RemoveLock(lockKey);
}
//锁结束
  • 至此,就完成了。

成果

数据没有出错,无多发无少发,抗住了并发请求,并通过了压力测试。

压测结果

总结

  • 一开始我们使用的C#的Lock()。 ()内需为静态object,不能为字符串。但依旧无法有效锁住剩余金额。
  • 在多次并发测试中发现,红包个数没有问题,但是有那么几次生成红包的总金额是多于红包池的设定的,也就是说有的进程取到的剩余金额是不对的,生成金额时因为是依旧旧金额生成的,导致抢到的金额多了。
  • 是因为生产环境使用的是两台服务器,搭建了两个host来处理请求。原本目的是希望能够起到分流,共同承担压力的目的。
  • 所以lock()是锁不住资源的。一直到使用了redis的分布式锁,才解决问题。

createtime:2018-11-13

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值