部分场景与项目实战

语雀完整版:

https://www.yuque.com/g/mingrun/embiys/qthuog/collaborator/join?token=GWbvRHnmcZardDoZ&source=doc_collaborator# 《项目实战》

项目实战

延时任务

如何实现延时任务? (qq.com)

  1. 数据库轮询
    • 用定时任务,比如三秒一次,去数据库查是否有过期的订单
    • 优点是简单,缺点是数据量大的话 比如上千万,这样全部扫描一次会有很大的损耗,而且三秒一扫描,那就可能订单过期了三秒才会被扫描到
  1. 使用JDK的延迟队列 DelayQueue
    • 放入该队列的对象要实现 Delayed接口,设置的延迟期满的时候才能被获取到
    • 使用put方法,放入元素并且设置延迟时间,然后使用take方法获取延期元素,如果获取不到就会进行wait
    • 优点是任务没有延低,缺点是宕机了数据就完蛋了
  1. 时间轮算法
    • 假设一个表盘,从第一秒到第六秒划分6格,假设3秒后有个任务要执行,那就把这个任务放到4的位置,而如果是10秒那就要多转一圈,可使用 Netty的HashedWheelTimer来实现
    • 优点是触发延迟 比DelayQueue更低,缺点也是宕机丢数据
  1. 使用Redis
    • 方案一:使用ZSet
      • 相关命令
添加元素:ZADD key score member [[score member] [score member] …]
按顺序查询元素:ZRANGE key start stop [WITHSCORES]
查询元素:score:ZSCORE key member
移除元素:ZREM key member [member …]

      • 放入订单的时候,key随便根据业务表示下,重点是score和member,假设是10秒后执行,那么score也就是排序值 就放10秒后的时间戳,member就放订单号
      • 取订单的时候,定义一个for循环,在循环中按照顺序取一个元素,要是没取到就休眠一会儿,取到了就把它的socre值拿出来,让当前时间戳与其比较,要是大于的话就把当前这个 member删除掉,如果删除成功,说明他还没有被其它的线程消费,那我就可以把这个个消费掉了
    • 方案二:使用键空间机制
      • 在redis2.8版本之后,key失效之后会给客户端发一个回调
  1. 使用消息队列
    • 如rabbit写一个 带过期时间的消息,等消息过期后就会变成死信 进入到死信队列中

抽奖

参考

企业红包雨项目缓存预热&抽奖逻辑核心步骤设计思路总结&资料分享_洺润Star的博客-优快云博客

实现

  1. 业务:可以用到兑吧的抽奖实现上,比如转盘抽奖、红包雨抽奖、普通抽奖、
  1. 缓存
    • 活动开始前1分钟(缓存预热) 将发奖策略放入到缓存中(时间戳从小到大),令牌由(时间戳+随机数)组成,出队时比较时间戳,如果now >时间戳就代表中奖了,否则没中奖,需要重新放回队列。 出队和重新放回队列逻辑由lua脚本完成,在脚本执行期间没有其它命令与其并行

    • 令牌的设计:这里不再使用上述途中k-v结构,令牌直接由三部分组成,第一部分是时间戳,第二部分是发奖策略Id,第三部分是随机数(防止令牌重复)
    • 使用令牌出奖:从令牌中拿到发奖策略,再用发奖策略取出奖品信息(奖品信息也在缓存中存放)
  1. 应用
    • 适用业务场景:
      • 兑吧的转盘抽奖是要配置具体 的抽奖概率,但这里的实现并不支持这个,一个令牌对应一个奖品,只能加大奖品数量,来提高中奖概率
      • 总结:兑吧的抽奖 如果去实现那就是概率抽奖组件,用红包雨这一套实现的就是活动抽奖组件
    • 活动抽奖组件
      • 在活动开始的这段时间内 投放奖品,每个奖品对应一定的数量
      • 时间越短,就意味着流量越大,越是凸显缓存预热的作用
      • 应用
        • 转盘抽奖:从该缓存中获取奖品信息,如果没抽中,就要给一个默认奖品
        • 红包雨抽奖:每一个红包会有一个红包Id,如果抽中奖就会 对应一个令牌
        • 普通营销活动应用:从该缓存中获取奖品信息
  1. 其它细节
    • 缓存预热:缓存默认在活动开始前1分钟加载,如果获取抽奖时没有存储桶那也要加载
    • 令牌均匀分布策略
      • 如果不开启该策略,令牌的生成方式如下所示,可能会造成奖品集中在某个时间段
//活动持续时间(ms)
long duration = end ‐ start;
long rnd = start + new Random().nextInt((int)duration);
//为什么乘1000,再额外加一个随机数呢? ‐ 防止时间段奖品多时重复
long token = rnd * 1000 + new Random().nextInt(999);

      • 均匀分布策略分两种情况
        • 活动总时间,大于一小时,小于一天:比如活动持续时间为 10小时,且奖品数大于10,就会循环奖品列表,把随机 时间戳均匀的在每个时间段内生成
        • 活动总时间,大于一天:比如活动持续时间为 10天,逻辑与上面相同,之不过奖品要均匀分不到这十天里面
    • 时间戳
      • 时间戳一般是 从1970年1月1日(00:00:00GMT)至当前时间的总秒数/毫秒数
      • 获取方式
//当前时间与 UTC 时间 1970 年 1 月 1 日午夜之间的差值(以毫秒为单位)。
System.out.println(System.currentTimeMillis());
//与上面相同
System.out.println(new Date().getTime());

排行榜实现

参考

  1. 资料
    手把手教你使用 Java 和 Redis 实现排行榜! (qq.com)
    好好的 Tair 排行榜不用,非得自己写?20 行代码实现高性能排行榜 (qq.com)
    Redis排行榜功能实现?多排序条件 (qq.com)
  1. 功能(参考星速台)
    • 页面

    • 用例
      • 获取排行耪列表(有名词限制,比如500名)
      • 查询当前用户在第几名
      • 查询排行榜期数信息
      • 查询排行榜发奖配置

实现(Redis Zset)

  1. 使用redis的zet数据结构,它的主要操作如下Redis 有序集合命令

序号

命令及描述

1

[ZADD key score1 member1 score2 member2]

向有序集合添加一个或多个成员,或者更新已存在成员的分数

2

ZCARD key

获取有序集合的成员数

3

ZCOUNT key min max

计算在有序集合中指定区间分数的成员数

4

ZINCRBY key increment member

有序集合中对指定成员的分数加上增量 increment

5

[ZINTERSTORE destination numkeys key key ...]

计算给定的一个或多个有序集的交集并将结果集存储在新的有序集合 destination 中

6

ZLEXCOUNT key min max

在有序集合中计算指定字典区间内成员数量

7

[ZRANGE key start stop WITHSCORES]

通过索引区间返回有序集合指定区间内的成员

8

[ZRANGEBYLEX key min max LIMIT offset count]

通过字典区间返回有序集合的成员

9

[ZRANGEBYSCORE key min max WITHSCORES] [LIMIT]

通过分数返回有序集合指定区间内的成员

10

ZRANK key member

返回有序集合中指定成员的索引

11

[ZREM key member member ...]

移除有序集合中的一个或多个成员

12

ZREMRANGEBYLEX key min max

移除有序集合中给定的字典区间的所有成员

13

ZREMRANGEBYRANK key start stop

移除有序集合中给定的排名区间的所有成员

14

ZREMRANGEBYSCORE key min max

移除有序集合中给定的分数区间的所有成员

15

[ZREVRANGE key start stop WITHSCORES]

返回有序集中指定区间内的成员,通过索引,分数从高到低

16

[ZREVRANGEBYSCORE key max min WITHSCORES]

返回有序集中指定分数区间内的成员,分数从高到低排序

17

ZREVRANK key member

返回有序集合中指定成员的排名,有序集成员按分数值递减(从大到小)排序

18

ZSCORE key member

返回有序集中,成员的分数值

19

[ZUNIONSTORE destination numkeys key key ...]

计算给定的一个或多个有序集的并集,并存储在新的 key 中

20

[ZSCAN key cursor MATCH pattern] [COUNT count]

迭代有序集合中的元素(包括元素成员和元素分值)

  1. 值的设计
    • key(多级索引):
      • 一个key值可分解出多层意思,用 #分割,例如 activityId#7#2#6#16,代表 活动Id为 activityId,后面是活动开始后的对应时间,第七月的第二周的第六天,第16个小时
    • socre
      • 排序值可以使用 比如游戏里面的积分,但如果遇到多个条件,比如奖牌榜需要综合 金牌、银牌、铜牌来排序,这是就需要自行转换,比如金牌对应三个积分值,铜牌对应一个积分值,最终加起来再放入zset中
    • member
      • 使用用户Id
  1. 其它细节
    • 为什么不使用MySQL
      • 首先肯定要在这个排序值上面建立索引,如果有修改或者添加比较频繁,索引也会重建,比较消耗性能
      • 数据量大的时候比较消耗性能

兑吧用户锁与普通锁的实现

用户锁

用途

在星速台的活动中 通过添加注解,和自带的玩法框架为方法加锁,一个方法就是一个操作,加了用户锁之后,用户同一时刻只能 进行一个操作(只能执行其中一个加了用户锁的方法)

实现

UserLock

  • 代码位置:cn.com.duiba.projectx.sdk.utils.UserLock
    • 主要方法有tryLock()和unlock()
    • 解锁要放到try-catch中,且该锁是非可重入锁
    • 锁的默认过期时间是 3s,且默认对预设方法不加锁
  • UserLock是个接口,他在以下类中都有实现


    以ProjectRequestContextImpl为例,该类在一些玩法类中都有使用,比如ScoringPlayway

  • ProjectRequestContextImpl对userLock的实现
@Override
public UserLock getUserLock() {
    if (userLock == null) {
        userLock = new UserLock() {
            private RedisLock lock;
            //锁的key:project+userId
            private String getKey() {
                return "ul_" + getProject().getId() + "_" + getUserId();
            }

            @Override
            public boolean tryLock(int expireSeconds) {
                if (lock != null) {
                    throw new RuntimeException("cann't  tryLock before unlock");
                }
                lock = DuibaService.getRedisAtomicClient().getLock(getKey(), expireSeconds);
                return lock != null;
            }
            @Override
            public void unlock() {
                if (lock != null) {
                    lock.unlock();
                    lock = null;
                }
            }
        };
    }
    return userLock;
}

    • 获取锁的实现
@Component("redisAtomicClient")
public class RedisAtomicClientServiceImpl implements edisAtomiClientService{
    // ...
    @Override
    public RedisLockInner getLock(final String key, long expireSeconds) {
        RedisLock lock = redisAtomicClient.getLock(key, expireSeconds);
        if(lock == null){
            return null;
        }
        return new RedisLockInner(lock);
    }
    // ...
}

      • 其中这里给的最大重试次数和重试等待时间都是0,也就是说用户锁只尝试一次,加锁失败就返回null(false),在playway中就直接返回给用户你的操作过于频繁,请稍后再试

普通锁

用途

  1. 用途是为资源加锁
  1. 使用(代码 cn/com/duiba/project/q1/zhangtao/assist/FreeGetPrizeAssist.java:164)
//声明锁
DistributedLock lock = api.newLock(key, 3);
//尝试加锁
lock.tryLock()
//解锁
lock.unlock();

实现

	DistributedLock newLock(String key, int expireSeconds);

    @Override
    public DistributedLock newLock(String key, int expireSeconds) {
        String realKey = "LK_" + getContext().getProjectId() + "_" + key;
        return lockService.newLock(realKey, expireSeconds);
    }

重点代码如下,提供了两个tryLock和unlock,其中调用的getLock方法,与上面用户锁的getLock是同一个,其中要特别注意最大尝试次数和尝试时间

@Override
    public DistributedLock newLock(String key, int expireSeconds) {
        return new DistributedLock() {
            private RedisLock redisLock;

            @Override
            public boolean tryLock() {
                redisLock = redisAtomicClientService.getLock(key, expireSeconds);
                return Objects.nonNull(redisLock);
            }

            @Override
            public boolean tryLock(int maxRetryTimes, long retryIntervalTimeMillis) {
                redisLock = redisAtomicClientService.getLock(key, expireSeconds, maxRetryTimes, retryIntervalTimeMillis);
                return Objects.nonNull(redisLock);
            }

            @Override
            public void unlock() {
                if (Objects.nonNull(this.redisLock)) {
                    this.redisLock.unlock();
                }
            }
        };
    }

解锁代码

    private static final String COMPARE_AND_DELETE =
            "if redis.call('get',KEYS[1]) == ARGV[1]\n" +
            "then\n" +
            "    return redis.call('del',KEYS[1])\n" +
            "else\n" +
            "    return 0\n" +
            "end";

public void unlock(){
    if(Thread.currentThread().getId() != threadId){
        //只允许创建锁的线程解锁
        throw new IllegalMonitorStateException();
    }
    List<String> keys = Collections.singletonList(key);
    stringRedisTemplate.execute(new DefaultRedisScript<>(COMPARE_AND_DELETE, Long.class), keys, expectedValue);
}

存在的问题

  1. 解锁的时候需要是当前线程才解锁,这里衍生出来两个问题
    • 为什么要保证解锁的是当前线程
      • 假设锁的有效期是30s, A线程执行30秒后还未执行完,此时B线程获取到了锁,B线程在执行过程中 A执行完毕,要释放锁,
        这时如果不对线程进行校检,A线程就会把B的锁给释放掉
      • 注意:上面所说的这个过程没有加看门口机制,看门狗机制是 比如A线程创建一个定时任务(守护线程,每隔一定时间去给锁续期用的)
    • ThreadId问题
      • getId方法如下所示
/**
     * Returns the identifier of this Thread.  The thread ID is a positive
     * {@code long} number generated when this thread was created.
     * The thread ID is unique and remains unchanged during its lifetime.
     * When a thread is terminated, this thread ID may be reused.
     *
     * @return this thread's ID.
     * @since 1.5
     */
public long getId() {
    return tid;
}

      • tid的 值是在构造方法中进行设置的
  private Thread(ThreadGroup g, Runnable target, String name,
                   long stackSize, AccessControlContext acc,
                   boolean inheritThreadLocals) {
     //省略 。。。
        /* Set thread ID */
        this.tid = nextThreadID();
    }

      • 那么这里就出现了问题,如果应用是集群部署,上面的A线程是在 R1机器上运行,B线程是在R2机器上运行,那么他们的threadId就有可能相同
      • 解决办法是,在放入threaId的时候加上uuid
public class GenerateUniqueCode {
    
    public static final String uniqueCode =  UUID.randomUUID().toString();
}

领域驱动

  1. 模型
    • 模型是对现实世界的抽象,建模有很多种方法,不过最终产出的模型,都应该是以帮助我们 解决问题,理解问题为目的
    • 模型的种类分 物理模型、数学模型、思维模型、概念模型(将现实世界转换为信息时间)
  1. 领域建模(面向对象精髓)
    • 概述:从本质上来说,软件开发过程就是问题空间到解决方案空间的一个映射转化

      • 问题空间就是系统要解决的领域问题,因此,也可以简单理解为一个领域就对应一个问题空间,是一个特定范围边界内的业务需求的总和。
      • 领域模型”就是“解决方案空间”,是针对特定领域里的关键事物及其关系的可视化表现,是为了准确定义需要解决问题而构造的抽象模型,是业务功能场景在软件系统里的映射转化,其目标是为软件系统的构建统一的认知。
        • 例如:
          • 请假系统解决的是人力工时的问题,属于人力资源领域,对口的是HR部门;
          • 费用报销系统解决的是员工和公司之间的财务问题,属于财务领域,对口的是财务部门;
          • 电商平台解决的是网上购物问题,属于电商领域。
          • 可以看出,每个软件系统本质上都解决了特定的问题,属于某一个特定领域,实现了同样的核心业务功能来解决该领域中核心的业务需求。
  1. DDD
    • Domain-Driven Design, 指通过统一语言、业务抽象、领域划分和领域建模等一系列手段来控制软件复杂度的方法论
    • 初步体验DDD(小demo 银行转账)
    • 数据驱动
    • 领域驱动:兑吧的开发流程就是领域驱动的(本人认为),它主要分成下面两个步骤
      • 领域划分:根据需求划分为不同的模块,在我们组叫做划分成不同的玩法或者组件,比如我们现在有一个营销活动,根据需求 我们划分出了 任务组件定期组件发奖组件邀请玩法
      • 领域建模:领域划分完成之后 就是领域建模,领域建模一般使用用例分析法,把需求中的关键词给抽离出来,一般写的代码会跟需求非常贴近


热点数据自动统计

  1. 背景:ali tair缓存数据库,对于热点key,由于采用一致性哈希算法,导致同样的key的数据都会 在同一台机器上进行读写,从而有单机的读写瓶颈,tair则提供了 热点数据自动发现的能力

  2. 设想应用:可以结合mysql的 bufferPool中的冷热链表和上图中的技术细节,应用到管控台中,做一个业务场景

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值