有一个这样的需求:

客户端不间断上报数据到后台服务器,后台服务判断TOP N里面是否包含当前上报的客户端,其中TOP N来源于上报数据值为前N的客户端。即这个top N数据是不断在变化的。 若客户端在TOP N中则可以继续上报,否则停止上报,如果之前属于TOP N,但后面又被移除的要告知客户端停止上报,新加入的要告知继续上报。

redis 削峰 redis topn_客户端


方案1:mysql

  1. 服务端收集客户端上报的数据同步写到mysql的一张表中。
  2. 存在就按客户端唯一标识更新数据,不存在就插入一条。
  3. 指定一个字段column A用来排序。
  4. 开启一个定时任务,定时刷新column A 为 top N的数据到一个列表List。
  5. 每次刷新列表List之前都要和旧的列表做对比,找出差异。

此方案简单,但是缺点有以下几点:

  1. 高并发情况下直接写mysql导致磁盘IO过高,数据库会是瓶颈。
  2. 每次都要判断是否存在。
  3. 维护列表B过大导致JVM内存占用。
  4. 为了对比前后列表差值,内存开启频繁,gc频繁。

    上面找出C并且告知客户端C停止上报,找出新增的D并告知客户端D重新上报数据。
    两个List找出差异这并不难,用JAVA里面HashSet的remove api 或者jdk 8的stream实现。

方案2:redis

  1. 上报数据直接写到zset,客户端唯一标识为member, score为上报的数据(如客户端等级)实时动态更新列表顺序。
  2. 定时取出zset score为top N的客户端,刷新到本地列表LIST。
  3. 比较取出前后列表的差值。

优点:

  1. redis读写纯内存操作,性能较好。
  2. zset每次add即可,无需判断是否存在,并且纯天然支持排序。

代码演示:

redisTemplate.opsForZSet().add(key, value, scoreVal)

key自定义,value必须唯一,如客户端ID,scoreVal表示客户端的排序值,如客户端等级。这样每次上报数据都实时更新,动态实现排序。
下面取出top N的数据:

redisTemplate.opsForZSet().reverseRangeWithScores(key, 0, N - 1);

取出top N后,要比较差集,这个可以用java api实现,但是这里可以借助redis的set方法difference来实现。
首先上面得到 top N集合后,写入到一个临时的集合Set中,假如它的key为tmpKey:

redisTemplate.opsForSet().add(tmpKey, top N集合)

假如旧的集合为key,它也是set结构的,那么要得到上面要被移除的C客户端,可以这样:

redisTemplate.opsForSet().difference(key, tmpKey)

要得到上面新加入的 D客户端,可以这样:

redisTemplate.opsForSet().difference(tmpKey, key)

最终将旧的集合key换为最新的top N的数据,实现如下:

#先从key集合中移除掉C
redisTemplate.opsForSet().remove(key, 被移除的C)  //这里可批量删,value支持数组
#再对key和tmpKey集合元素合并写到key中,即
redisTemplate.opsForSet().unionAndStore(key, tmpKey, key)

这时集合key中的元素就是最新的: A ,B, D
下一个定时任务周期,继续以上操作。。。
注意:上面临时tmpKey每次循环之前都要清空,最好再设置个过期时间,以免一直占用redis内存。

这里还有一个问题:
上面用zset存储的value必须为唯一对象,一般都是ID之类的,但是我们会有这样的需求:不仅仅只有ID,可能还有名称、当前等级这些附加属性。 这时怎么办,不可能把整个对象放到zset的value中,因为这样每次都是不同的value了,做不到更新score使其动态排序。

怎么办?

我们可以再利用redis的另外一个数据结构hash来帮我们实现,hash结构专门负责存储附加属性的值。zset的value依然是id,当我们要去拿这个id的附加属性值,可以去hash这边拿回来,接着做后续的处理逻辑。

这个hash结构我们可以这样设计:

hashKey: 自定义

key: id 对应zset的 value

value:附加属性的各种信息

redis 削峰 redis topn_redis_02