文章投票功能模块需求

用户可以发表文章,发表时默认给自己的文章投了一票

用户在查看网站时可以按评分进行排列查看

用户也可以按照文章发布时间进行排序

为节约内存,一篇文章发表后,7天内可以投票,7天过后就不能再投票了

为防止同一用户多次投票,用户只能给一篇文章投一次票

关系数据库设计

文章基本信息表 t_article

article_id

title

content

post_time

user_id

文章票数与分值表 t_vote_data

article_id

votes

scores

文章投票详表 t_vote_details

article_id

vote_time

user_id

传统实现方式

存在多表查询,排序,复杂,不适合高并发场景

应用场景Redis实现

投票网站应用场景会使用到的Redis相关指令如下:

HASH类型命令:hset  hincrBy  hgetAll   expire

SET集合命令:sadd  smembers

ZSET集合命令:zadd   zscore  zincrby  zrevrange

redis的Key设计思路

Key-Value键值对:     比如set key value

key的设计:一般以业务、功能模块或表名开头,后跟主键(或能表示数据唯一性的值)

例子:用户模块,其中用户ID  001, 用户名称 caojiulu,那么Key如何设计?

set user:001:name  caojiulu                    

key为  user:001:name

记录已投票用户,防重投

已对002文章投票过的用户,使用SET存储(无序,不能重复

Redis学习  基于Redis设计的投票网站实战(三)_主键

使用ZSET记录文章投票分数,和按文章发布的时间戳(有序列,不能重复)

Redis学习  基于Redis设计的投票网站实战(三)_redis_02

Redis缓存设计实战

使用HASH来存储文章信息

Redis学习  基于Redis设计的投票网站实战(三)_ide_03

使用ZSET按文章发布时间排

Redis学习  基于Redis设计的投票网站实战(三)_redis_04

使用ZSET按文章评分排序

Redis学习  基于Redis设计的投票网站实战(三)_ide_05

缓存数据变化

004号用户对文章article:002投了1张票,文章的评分增加了400

Redis学习  基于Redis设计的投票网站实战(三)_redis_06

004号用户对article:002文章投票后,会被追加到已投票用户名单里

Redis学习  基于Redis设计的投票网站实战(三)_主键_07

代码实现:

常量类:

/**
* 常量类
*/
public class Constants {
public static final int ONE_WEEK_IN_SECONDS = 7 * 86400; //文章发布7天后失效,不能投票
public static final int VOTE_SCORE = 400;//获取一票后文章分值加400
public static final int ARTICLES_PER_PAGE = 25; //分页查询每页显示25条
}

实现类:核心类

/**
* 文章发布使用redis技术
*/
@Service
public class RedisArticleServiceImpl implements RedisArticleService{


@Resource
private JedisUtils jedis;
/**
* 文章提交发布
* @param 标题 内容 链接 用户ID
* @return 文章的ID
*/
@Override
public String postArticle(String title, String content, String link, String userId) {
//基于redis: key--->唯一标识---实体---属性值
//mysql: 主键:自增

//文章ID,自增 UUID 主键 1---id 主键
String articleId = String.valueOf(jedis.incr("article:")); // articleId=1

//用来记录投票 key voted:1 set key value set


String voted = "voted:" + articleId; // voted:1

jedis.sadd(voted, userId); //将投票的用户记录到voted:1键集合来……001
jedis.expire(voted, Constants.ONE_WEEK_IN_SECONDS); //设置失效时间
//删数据之前,是不是要转移一下


long now = System.currentTimeMillis() / 1000; //时间
//long score = 0l;
//生成文章ID 二维数据 hash
String article = "article:" + articleId;//article:1
//article:1
HashMap<String,String> articleData = new HashMap<String,String>();
articleData.put("title", title);
articleData.put("link", link);
articleData.put("user", userId);
articleData.put("now", String.valueOf(now));
articleData.put("votes", "1");
//发布一篇文章,hmset article:1 title 标题 link googlecom user username
jedis.hmset(article, articleData);
//zadd user:zan 200 caojiulu caojiulu的点赞数1, 返回操作成功的条数1
jedis.zadd("score:info", now + Constants.VOTE_SCORE, article);//根据分值排序文章的有序集合
jedis.zadd("time:", now, article); //根据文章发布时间排序文章的有序集合

return articleId;
}




/**
* 文章投票
* @param 用户ID 文章ID(article:001) //001
*/
@Override
public void articleVote(String userId, String article) {


//计算投票截止时间
long cutoff = (System.currentTimeMillis() / 1000) - Constants.ONE_WEEK_IN_SECONDS;
//检查是否还可以对文章进行投票,如果该文章的发布时间比截止时间小,则已过期,不能进行投票
if (jedis.zscore("time:", article) < cutoff){
return;
}
//获取文章主键id
String articleId = article.substring(article.indexOf(':') + 1); article:1 1


//将投票的用户加入到键为voted:1的集合中,表示该用户已投过票了 voted:1 set集合里来
//0 并不1

if (jedis.sadd("voted:" + articleId, userId) == 1) {
jedis.zincrby("score:info", Constants.VOTE_SCORE, article);//分值加400
jedis.hincrBy(article, "votes", 1l);//投票数加1
}
}





/**
* 文章列表查询(分页)
* @param page key
* @return redis查询结果
*/
@Override
public List<Map<String, String>> getArticles(int page, String key) {
int start = (page - 1) * Constants.ARTICLES_PER_PAGE;
int end = start + Constants.ARTICLES_PER_PAGE - 1;
//倒序查询出投票数最高的文章,zset有序集合,分值递减
Set<String> ids = jedis.zrevrange(key, start, end);
List<Map<String,String>> articles = new ArrayList<Map<String,String>>();
for (String id : ids){
Map<String,String> articleData = jedis.hgetAll(id);
articleData.put("id", id);
articles.add(articleData);
}

return articles;
}


@Override
public String hget(String key, String feild) {
return jedis.hget(key,feild);
}

@Override
public Map<String, String> hgetAll(String key) {
return jedis.hgetAll(key);
}

}

测试类:

/**
* Redis业务测试用例
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class TestRedisArticle {

@Resource(name = "redisArticleServiceImpl")
private RedisArticleService redisArticleService;

/**
* 测试用例:用户发布文章
*/
@Test
public void apostArticle() {
String userId = "001"; //用户ID 001
String title = "The road to west";
String content = "About body and mental health";
String link = "www.miguo.com";
//发布文章,返回文章ID
String articleId = redisArticleService.postArticle(title, content, link, userId);
System.out.println("刚发布了一篇文章,文章ID为: " + articleId);
System.out.println("文章所有属性值内容如下:");
Map<String,String> articleData = redisArticleService.hgetAll("article:" + articleId);
for (Map.Entry<String,String> entry : articleData.entrySet()){
System.out.println(" " + entry.getKey() + ": " + entry.getValue());
}
System.out.println();
}
/**
* 测试用例:用户对文章投票
*/
@Test
public void barticleVote(){
String userId = "002";
String articleId = "1";

System.out.println("开始对文章"+"article:" + articleId+"进行投票啦~~~~~");
//cang用户给James的文章投票
redisArticleService.articleVote(userId, "article:" + articleId);//article:1
//投票完后,查询当前文章的投票数votes
String votes = redisArticleService.hget("article:" + articleId, "votes");

System.out.println("article:" + articleId+"这篇文章的投票数从redis查出来结果为: " + votes);
}
/**
* 测试用例:获取文章列表并打印出来
*/
@Test
public void cgetArticles(){
int page = 1;
String key = "score:info";
System.out.println("查询当前的文章列表集合为:");
List<Map<String,String>> articles = redisArticleService.getArticles(page,key);
for (Map<String,String> article : articles){
System.out.println(" id: " + article.get("id"));
for (Map.Entry<String,String> entry : article.entrySet()){
if (entry.getKey().equals("id")){
continue;
}
System.out.println(" " + entry.getKey() + ": " + entry.getValue());
}
}
}
}