序
redis实战是本好东东,值得详细研读。本文主要是对第一章的投票服务的代码从jredis改为SpringBoot的redis template版本。
功能说明
- 用redis来存储文章
- 对文章投票
- 每个用户只能投一次
- 文章按投票和时间排行
- 文章打标签
数据结构选择
- 文章采用hash存储
- 用redis的incr来实现文章的自增id
- 用set来记录每篇文章已投票的用户
- 用zset存储文章与得分,创建时间
- 用set来存储标签里有哪些文章
常量声明
/**
* incr,获取文章的id
*/
public static final String KEY_ARTICLE_ID_SEQ = "articleIdSeq";
/**
* 数据结构:SET
* key -- voted:articleId
* value -- user
* 记录每篇文章的投票用户
*/
public static final String KEY_VOTE_ARTICLE_PREFIX = "votedSet:";
/**
* 数据结构:map
* key -- articleMap:id
* value -- article map
*/
public static final String KEY_ARTICLE_PREFIX = "articleMap:";
/**
* 数据结构:zset
* key -- scoreZSet
* value -- 得分 articleMap:id
*/
public static final String KEY_SCORE = "scoreZSet";
/**
* 数据结构:zset
* key -- timeZSet
* value -- 得分 articleMap:id
*/
public static final String KEY_TIME = "timeZSet";
/**
* 文章标签前缀
* 数据结构 set
* key -- tagSet:tag
* value -- articleMap:id
*/
public static final String KEY_TAG_PREFIX = "tagSet:";
/**
* 文章用户投票记录的过期时间
*/
public static final int ARTICLE_USER_VOTE_EXPIRE_DAY = 7;
/**
* 文章用户投票记录的过期时间
* 秒为单位
*/
public static final int ONE_WEEK_IN_SECONDS = 7 * 86400;
/**
* 投票得分
*/
public static final int VOTE_SCORE = 432;
/**
* 每页多少条记录
*/
public static final int ARTICLES_PER_PAGE = 25;
主要功能
添加文章
/**
* 添加文章
* @param user
* @param title
* @param link
* @return 文章的自增id
*/
public Long postArticle(String user,String title,String link){
ValueOperations<String, Long> intOps = redisTemplate.opsForValue();
Long id = intOps.increment(KEY_ARTICLE_ID_SEQ,1L);
//本人发布的文章,自动添加到已投票set中
voteInternal(id,user);
Map<String,Object> articleData = new HashMap<String,Object>();
articleData.put("title", title);
articleData.put("link", link);
articleData.put("user", user);
long nowInSecs = System.currentTimeMillis() / 1000;
articleData.put("now", String.valueOf(nowInSecs));
articleData.put("votes", "1"); //主要投票数的计数器
//保存文章
String articleKey = formArticleKey(id);
redisTemplate.opsForHash().putAll(articleKey, articleData);
//记录得分和时间
redisTemplate.boundZSetOps(KEY_SCORE).add(articleKey,nowInSecs + VOTE_SCORE);
redisTemplate.boundZSetOps(KEY_TIME).add(articleKey,nowInSecs);
return id;
}
给文章投票
/**
* 用户给文章投票
* 严格一点需要加事务
* @param user
* @param id
*/
public void voteArticle(String user,Long id){
long now = System.currentTimeMillis() / 1000;
String articleKey = formArticleKey(id);
Double score = redisTemplate.boundZSetOps(KEY_SCORE).score(articleKey);
//文章发布超过一周,不能投票
if(score.longValue() + ONE_WEEK_IN_SECONDS < now){
return;
}
//判断用户是否可以投票
//若已经投票则返回,未投票则投票,并增加投票记录
Long tryVote = voteInternal(id,user);
if(tryVote == 0){
return;
}
//给文章增加投票
redisTemplate.opsForHash().increment(articleKey,"votes",1L);
//增加文章的得分
redisTemplate.boundZSetOps(KEY_SCORE).incrementScore(articleKey,VOTE_SCORE);
}
根据id获取文章
/**
* 根据id获取文章内容
* @param id
* @return
*/
public ArticleBean getArticleById(Long id){
Map<String,Object> data = redisTemplate.opsForHash().entries(formArticleKey(id));
ArticleBean bean = new ArticleBean();
bean.setId(id);
try {
BeanUtils.populate(bean,data);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
return bean;
}
给文章打标签
/**
* 给文章打标签
* @param id
* @param tags
*/
public void tagArticle(Long id,String[] tags){
String articleKey = formArticleKey(id);
for(String tag:tags){
String tagKey = formTagKey(tag);
redisTemplate.opsForSet().add(tagKey,articleKey);
}
}
获取排行具体实现
/**
* 根据排序及页数获取文章
* @param page
* @param zrangeKey
* @return
* @throws InvocationTargetException
* @throws IllegalAccessException
*/
private List<ArticleBean> getArticleRank(int page,String zrangeKey){
int start = (page - 1) * ARTICLES_PER_PAGE;
int end = start + ARTICLES_PER_PAGE - 1;
BoundZSetOperations<String,String> ops = redisTemplate.boundZSetOps(zrangeKey);
//根据得分从大到小
Set<String> ids = ops.reverseRange(start, end);
List<ArticleBean> rs = ids.stream().map(new Function<String, ArticleBean>() {
@Override
public ArticleBean apply(String id) {
HashOperations<String, String, String> hashOps = redisTemplate.opsForHash();
Map<String, String> map = hashOps.entries(id);
ArticleBean bean = new ArticleBean();
bean.setId(Long.valueOf(id.substring(id.indexOf(':') + 1)));
try {
BeanUtils.populate(bean, map);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
return bean;
}
}).collect(Collectors.toList());
return rs;
}
获取各种排行
/**
* 根据标签获取文章排名
* @param page
* @param zrangeKey
* @param tag
* @return
*/
public List<ArticleBean> getArticleRankByTag(int page,String zrangeKey,String tag){
String scoreTagKey = formScoreTagKey(zrangeKey, tag);
Boolean exist = redisTemplate.hasKey(scoreTagKey);
if(!exist){
Long rs = redisTemplate.opsForZSet().intersectAndStore(formTagKey(tag),zrangeKey,scoreTagKey);
if(rs == 1){
redisTemplate.expire(scoreTagKey,1,TimeUnit.MINUTES);
}
}
return getArticleRank(page,scoreTagKey);
}
/**
* 根据得分排序
* @param page
* @return
*/
public List<ArticleBean> getArticleRankByScore(int page){
return getArticleRank(page,KEY_SCORE);
}
/**
* 根据时间排序
* @param page
* @return
*/
public List<ArticleBean> getArticleRankByTime(int page){
return getArticleRank(page,KEY_TIME);
}
junit测试
/**
* Created by codecraft on 2016-02-11.
*/
public class VoteServiceTest extends RedisdemoApplicationTests{
@Autowired
VoteService voteService;
@Autowired
RedisTemplate redisTemplate;
@Test
public void incrMap(){
//template.setHashValueSerializer(new StringRedisSerializer(StandardCharsets.UTF_8));
//这里得用string写进去
redisTemplate.<String,Object>opsForHash().put("user-abcde", "count", "1");
redisTemplate.opsForHash().increment("user-abcde", "count", 5);
//error java.lang.ClassCastException: java.lang.Long cannot be cast to java.lang.String
redisTemplate.<String,Object>opsForHash().put("user-abcde", "now", System.currentTimeMillis());
System.out.println(redisTemplate.opsForHash().get("user-abcde", "now"));
}
@Test
public void delete(){
for(int i=0;i<100;i++){
voteService.delete(i+1L);
}
redisTemplate.delete("articleIdSeq");
}
@Test
public void post() throws InterruptedException {
for(int i=0;i<100;i++){
voteService.postArticle("user" + i, "title" + i, "link" + i);
Thread.sleep(1000);
}
for(int i=0;i<100;i++){
ArticleBean bean = voteService.getArticleById(i + 1L);
System.out.println(ToStringBuilder.reflectionToString(bean));
}
}
@Test
public void vote(){
Random random = new Random();
IntStream.range(1,31).forEach(x -> {
Long articleId = random.nextInt(100) + 1L;
String user = "user" + (random.nextInt(100) + 1);
voteService.voteArticle(user,articleId);
});
}
@Test
public void addTag(){
Random random = new Random();
IntStream.range(1,31).forEach(x -> {
Long articleId = random.nextInt(100) + 1L;
String tag = "tag" + (random.nextInt(5) + 1);
voteService.tagArticle(articleId, new String[]{tag});
});
}
@Test
public void getByScore(){
List<ArticleBean> data = voteService.getArticleRankByScore(1);
data.stream().forEach(System.out::println);
}
@Test
public void getByTime(){
List<ArticleBean> data = voteService.getArticleRankByTime(1);
data.stream().forEach(System.out::println);
}
@Test
public void getByTag(){
List<ArticleBean> data = voteService.getArticleRankByTag(1, VoteService.KEY_SCORE,"tag1");
data.stream().forEach(System.out::println);
}
}