一、基于Redis设计的投票网站实战
1、应用场景实战
文章投票功能模块需求
- 用户可以发表文章,发表时默认给自己的文章投了一票
- 用户在查看网站时可以按评分进行排列查看
- 用户也可以按照文章发布时间进行排序
- 为节约内存,一篇文章发表后,7天内可以投票,7天过后就不能再投票了
- 为防止同一用户多次投票,用户只能给一篇文章投一次票
2、关系数据库设计
文章基本信息表 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
3、应用场景Redis实现
投票网站应用场景会使用到的Redis相关指令如下:
HASH类型命令:
hset hincrBy hgetAll expire
SET集合命令:
sadd smembers
ZSET集合命令:
zadd zscore zincrby zrevrange
4、redis的Key设计思路
Key-Value键值对:比如set key value
key的设计:一般以业务、功能模块或表名开头,后跟主键(或能表示数据唯一性的值)
例子:用户模块,其中用户ID 001,用户名称hankin,那么Key如何设计?
set user:001:name hankin
key为 user:001:name
5、记录已投票用户,防重投
已对002文章投票过的用户,使用SET存储(无序,不能重复)
6、记录文章分值
使用ZSET记录文章投票分数,和按文章发布的时间戳(有序列,不能重复)
7、Redis缓存设计实战
8、缓存数据变化
1)004号用户对文章article:002投了1张票,文章的评分增加了400
2)004号用户对article:002文章投票后,会被追加到已投票用户名单里
9、代码环节
代码参考:redis-vote模块,git地址:https://gitee.com/hankin_chj/redis-platform.git
业务接口方法:
/**
* 业务接口方法
*/
public interface RedisArticleService {
public String postArticle(String title, String content, String link, String userId);
public Map<String, String> hgetAll(String key);
public void articleVote(String userId, String articleId);
public String hget(String key, String votes);
public List<Map<String,String>> getArticles(int page, String order);
}
文章发布代码:
/**
* 文章发布使用redis技术
*/
@Service
public class RedisArticleServiceImpl implements RedisArticleService {
@Resource
private JedisUtils jedis;
/**
* 文章提交发布
* @param title 标题 内容 链接 用户ID
* @return 文章的ID
*/
@Override
public String postArticle(String title, String content, String link, String userId) {
//article:001
String articleId = String.valueOf(jedis.incr("article:")); // articleId=1
//投票键: voted:
String voted = "voted:" + articleId;
jedis.sadd(voted, userId);
jedis.expire(voted, Constants.ONE_WEEK_IN_SECONDS);
long now = System.currentTimeMillis() / 1000;
String article = "article:" + articleId;
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");
jedis.hmset(article, articleData);
jedis.zadd("score:info", now + Constants.VOTE_SCORE, article);
jedis.zadd("time:", now, article);
return articleId;
}
/**
* 文章投票
* @param userId 用户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
if (jedis.sadd("voted:" + articleId, userId) == 1) {
jedis.zincrby("score:info", Constants.VOTE_SCORE, article);//分值加400
jedis.hincrBy(article, "votes", 1l);//投票数加1
}
}
/**
* 文章列表查询(分页)
* @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业务测试用例代码:
/**
* Redis业务测试用例
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class TestRedisArticle {
@Resource(name = "redisArticleServiceImpl")
private RedisArticleService redisArticleService;
/**
* 测试用例:用户发布文章
*/
@Test
pub