en -> new DefaultTypedTuple<>((String) en.get(“userId”),
(Double) en.get(“score”))).collect(Collectors.toSet());
// 计算排行榜前先将topN桶删除,防止之前进入桶的用户干扰
redisUtil.delAndZaddExec(GameConstant.USER_SCORE_RANKING_TOPN, vals);
return doTopNScoreList(topN);
}
public List
Set
= redisUtil.zRevrangeWithScore(bucketValue, 0L, topN - 1);
List
return userScoreTuples.stream().map(tuple -> {
String userId = tuple.getValue();
Double score = tuple.getScore();
Map
map.put(“userId”, userId);
map.put(“score”, score);
return map;
}).collect(Collectors.toList());
}
public List doTopNScoreList(Long topN) {
List userIdList = new ArrayList<>();
Set
= redisUtil.zRevrangeWithScore(GameConstant.USER_SCORE_GENERAL_RANKING_TOPN, 0L, topN - 1);
List
List collect = userScoreTuples.stream().map(tuple -> {
String userId = tuple.getValue();
Double score = tuple.getScore();
userIdList.add(userId);
return GameRanking.builder()
.userNo(userId)
.leaderboardScore(score)
.ranking((long) (userScoreTuples.indexOf(tuple) + 1))
.build();
}).collect(Collectors.toList());
List
collect.stream().forEach(gameRanking -> {
Map
.filter(map -> map.get(“userNo”).equals(gameRanking.getUserNo())).findFirst().orElse(null);
if (entity != null) {
gameRanking.setNickName(entity.get(“nickName”));
}
});
// 增加段位功能
long count = 0;
for (int i = 0; i < collect.size(); i++) {
count++;
collect.get(i).setGrade(getUserGrade(count));
}
return collect;
}
public GameRanking doGameRankingSelf(String userNo) {
Long selfRank = null;
Double score = null;
String nickName = null;
try {
GameRanking gameRanking = gameRankingMapper.selectOneByUserNo(userNo);
if (Objects.isNull(gameRanking)) {
nickName = getNickName(userNo);
} else {
nickName = gameRanking.getNickName();
}
score = gameRanking.getLeaderboardScore();
// 看该用户是否在topN的排行里
GameRanking rankingSelf = rankingSelfInTopN(userNo);
if (rankingSelf != null) {
return rankingSelf;
}
String bucketName = getBucketNameParseFromConfigCenter(score);
Map
Map
// 桶位置比较
for (Map.Entry
if (entry.getValue().compareTo(bucketName) >= 0) {
Long perBucketSize = redisUtil.zCard(entry.getValue());
map.put(entry.getValue(), perBucketSize);
}
}
Long totalNum = 0L;
for (Map.Entry
if (Objects.isNull(entry.getValue())) {
continue;
}
if (bucketName.equals(entry.getKey())) {
// 自身桶的排名
Long selfNum = redisUtil.zRevrank(bucketName, userNo) + 1;
// 自身桶排名与自身桶前面的总人数相加
totalNum += selfNum;
} else {
totalNum += Long.parseLong(entry.getValue().toString());
}
}
selfRank = totalNum;
} catch (NullPointerException e) {
selfRank = null;
score = null;
log.warn(“gameRanking userNo:{”+userNo+“} score is null”, e);
}
return GameRanking.builder()
.userNo(userNo)
.leaderboardScore(score)
.nickName(nickName)
.ranking(selfRank)
.grade(getUserGrade(selfRank))
.build();
}
public GameRanking rankingSelfInTopN(String userNo) {
Double score = redisUtil.zScore(GameConstant.USER_SCORE_GENERAL_RANKING_TOPN, userNo);
if (score == null) {
return null;
} else {
Long rank = redisUtil.zRevrank(GameConstant.USER_SCORE_GENERAL_RANKING_TOPN, userNo);
return GameRanking.builder()
.userNo(userNo)
.leaderboardScore(score)
.ranking(rank + 1)
.nickName(getNickName(userNo))
.grade(getUserGrade(rank + 1))
.build();
}
}
public String getBucketNameParseFromConfigCenter(Double score) {
JSONArray jsonArray = JSONArray.parseArray(ConfigManager.get(GameConstant.USER_SCORE_GENERAL_RANKING_INTERVAL));
int size = jsonArray.size();
boolean flag = false;
for (int i = 0; i < size; i++) {
JSONObject jsonObject = jsonArray.getJSONObject(i);
String bucketInterval = jsonObject.getString(“bucketInterval”);
String bucketName = jsonObject.getString(“bucketName”);
String[] split = bucketInterval.substring(1, bucketInterval.length() - 1).split(“,”);
if ((score >= Double.parseDouble(split[0]) && “+endless”.equals(split[1])) ||
(score >= Double.parseDouble(split[0]) && score < Double.parseDouble(split[1]))) {
flag = true;
} else {
flag = false;
}
if (flag) {
return bucketName;
}
}
return “”;
}
}
4、原子性操作导致并发安全问题
redisUtil.delAndZaddExec(GameConstant.USER_SCORE_RANKING_TOPN, vals);
通过lua脚本保证原子一致性,解决并发安全问题。
public class RedisUtil {
@Autowired
private StringRedisTemplate stringRedisTemplate;
private static final String DELANDZADDSCRIPT =
“if redis.call(‘zcard’, KEYS[1]) > 0 then\n” +
" redis.call(‘del’, KEYS[1])\n" +
" for i, v in pairs(ARGV) do\n" +
" if i > (table.getn(ARGV)) / 2 then\n" +
" break\n" +
" end\n" +
" redis.call(‘zadd’, KEYS[1], ARGV[2i - 1], ARGV[2i])\n" +
" end\n" +
" return 1\n" +
“else\n” +
" for i, v in pairs(ARGV) do\n" +
" if i > (table.getn(ARGV)) / 2 then\n" +
" break\n" +
" end\n" +
" redis.call(‘zadd’,KEYS[1], ARGV[2i - 1], ARGV[2i])\n" +
" end\n" +
" return 1\n" +
“end”;
private RedisScript redisDelAndZaddScript = new DefaultRedisScript<>(DELANDZADDSCRIPT, Long.class);
/**
刪除及插入
@param key 键
@param val 批量值
*/
public void delAndZaddExec(String key, Set
if (StringUtils.isEmpty(key)) {
throw new IllegalArgumentException();
}
Object[] args = new Object[val.size()*2];
int i= 0;
for (ZSetOperations.TypedTuple it : val ) {
args[2*i] = String.valueOf(it.getScore());
args[2*i + 1] = it.getValue();
i++;