《Redis实战》第一章文章投票的实现,基本将书中所提到的情况考虑到了,下面是主要代码。
1. 依赖和配置文件
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.13.3</version>
</dependency>
</dependencies>
spring:
redis:
host: ip地址
port: 6379
database: 0
password: ******
server:
port: 8080
2. 统一返回结果
/**
* 统一返回结果格式
*
* @author Caolele
* @since 09-19-2022 周一
*/
@Data
public class Result<T> {
private Integer code;
private String message;
private T data;
public Result(Integer code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
}
public Result(Integer code, T data) {
this.code = code;
this.data = data;
}
/**
* 统一失败返回
*/
public static Result fail() {
return new Result(20001, "获取失败", null);
}
/**
* 自定义失败消息返回
*/
public static Result fail(String message) {
Result result = new Result(20001, null);
result.setMessage(message);
return result;
}
/**
* 统一成功返回
*/
public static <T> Result success(T t) {
Result result = new Result(20000, "获取数据成功", t);
return result;
}
/**
* 自定义成功消息返回
*/
public static <T> Result success(String message, T t) {
return new Result(20000, message, t);
}
/**
* 自定义成功消息无数据返回
*/
public static Result success(String message) {
return new Result(20000, message);
}
/**
* 虽然成功执行操作,但是没有数据
*
* @return
*/
public static Result successWithoutData() {
return new Result(20000, "未查询到数据", null);
}
}
3. 序列化配置
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
redisTemplate.setHashKeySerializer(stringRedisSerializer);
redisTemplate.setHashValueSerializer(genericJackson2JsonRedisSerializer);
redisTemplate.setKeySerializer(stringRedisSerializer);
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
return redisTemplate;
}
}
4. 常量
/**
* @author Caolele
* @since 09-19-2022 周一
*/
public class Article {
/**
* 文章投票过期时间
*/
public static final Long ONE_WEEK_IN_SECONDS = 7*86400L;
/**
* 投票一次所加的分数
*/
public static final Double VOTE_SCORE = 432D;
}
/**
* @author Caolele
* @since 09-19-2022 周一
*/
public class RedisArticleKey {
/**
* 文章详细信息的key的前缀
* e.g. article:articleId
*/
public static final String ARTICLE_DETAIL_KEY_PREFIX = "article:";
/**
* 文章标题
*/
public static final String ARTICLE_DETAIL_TITLE = "title";
/**
* 链接
*/
public static final String ARTICLE_DETAIL_LINK = "link";
/**
* 发布者
* e.g. poster user:userId
*/
public static final String ARTICLE_DETAIL_POSTER = "poster";
/**
* 时间戳
*/
public static final String ARTICLE_DETAIL_TIME = "time";
/**
* 投票数量
*/
public static final String ARTICLE_DETAIL_VOTES = "votes";
/**
* 文章发布时间 key
*/
public static final String TIME_KEY = "time:";
/**
* 按时间排序的ZSet中的key前缀
*/
public static final String TIME_ARTICLE_PREFIX ="article:";
/**
* 文章评分 key
*/
public static final String SCORE_KEY = "score:";
/**
* 按分数排序,每篇文章的前缀
*/
public static final String SCORE_ARTICLE_PREFIX = "article:";
/**
* Set 每篇文章的投票用户列表的key前缀
*/
public static final String VOTED_KEY_PREFIX = "voted:";
/**
* 用户投票列表中的用户前缀
*/
public static final String VOTED_USER_KEY_PREFIX = "user:";
private RedisArticleKey() {
}
}
5. 核心业务类
文章发布
/**
* @author Caolele
* @since 09-19-2022 周一
*/
@Service
public class ArticleServiceImpl implements ArticleService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Override
public void publish(Long articleId, Long userId) {
Long timeStamp = System.currentTimeMillis() / 1000;
// TODO: 2022/9/19 下面三步是一个事务,暂未实现
//1.通过article:articleId 记录每篇文章的详细信息
Map<String, Object> map = getArticleDetailMap(articleId, timeStamp,userId);
redisTemplate.opsForHash().putAll(RedisArticleKey.ARTICLE_DETAIL_KEY_PREFIX + articleId, map);
//2.将发布的文章放入到按时间排序的有序集合
redisTemplate.opsForZSet().add(RedisArticleKey.TIME_KEY,
RedisArticleKey.TIME_ARTICLE_PREFIX + articleId,
timeStamp);
//3.直接用发布时间戳作为评分的初始化值
redisTemplate.opsForZSet().add(RedisArticleKey.SCORE_KEY, RedisArticleKey.ARTICLE_DETAIL_KEY_PREFIX + articleId, timeStamp);
//4.初始化文章的投票列表,仅允许存在7天,7天后自动注销,为了能建立这个set便于设置过期时间,放入发布者的key,这样发布者不可以给自己点赞
redisTemplate.opsForSet().add(RedisArticleKey.VOTED_KEY_PREFIX+articleId,RedisArticleKey.VOTED_USER_KEY_PREFIX+userId);
redisTemplate.expire(RedisArticleKey.VOTED_KEY_PREFIX+articleId, Article.ONE_WEEK_IN_SECONDS, TimeUnit.SECONDS);
}
//构造一篇文章
private Map<String, Object> getArticleDetailMap(Long articleId, Long timeStamp,Long userId) {
//记录发布文章的信息
Map<String, Object> map = new HashMap();
map.put(RedisArticleKey.ARTICLE_DETAIL_LINK, "#");
map.put(RedisArticleKey.ARTICLE_DETAIL_POSTER,RedisArticleKey.VOTED_USER_KEY_PREFIX+userId);
map.put(RedisArticleKey.ARTICLE_DETAIL_TITLE, "漂泊者们");
map.put(RedisArticleKey.ARTICLE_DETAIL_VOTES, 0L);
map.put(RedisArticleKey.ARTICLE_DETAIL_TIME, timeStamp);
return map;
}
/**
* 取出评分最高的数篇文章 zrevrange score: 0 9 取出前10个按分数从高到低
*/
@Override
public Result getTopScoreArticles(Integer size) {
//先获取前size个文章的id,然后去查找指定Id文章的详细信息返回
Set<Object> objects = redisTemplate.opsForZSet().reverseRange(RedisArticleKey.SCORE_KEY, 0, size - 1);
if(objects.isEmpty())
{
return Result.successWithoutData();
}
List<Map<Object,Object>> maps = new LinkedList<>();
objects.forEach(o -> {
Map<Object, Object> entries = redisTemplate.opsForHash().entries(o.toString());
maps.add(entries);
});
return Result.success(maps);
}
}
文章投票
/**
* @author Caolele
* @since 09-19-2022 周一
*/
@Service
public class VoteServiceImpl implements VoteService {
@Autowired
private RedisTemplate redisTemplate;
@Override
public Result articleVote(Long articleId, Long userId) {
if(!redisTemplate.hasKey(RedisArticleKey.ARTICLE_DETAIL_KEY_PREFIX+articleId)){
return Result.fail("文章不存在");
}
if(redisTemplate.
opsForHash().
get(RedisArticleKey.ARTICLE_DETAIL_KEY_PREFIX+articleId,RedisArticleKey.ARTICLE_DETAIL_POSTER).
equals(RedisArticleKey.VOTED_USER_KEY_PREFIX+userId)){ //用户给自己投票
return Result.fail("你不可给自己投票");
}
Double cutoff = System.currentTimeMillis() / 1000 -
redisTemplate.opsForZSet().score(RedisArticleKey.TIME_KEY,
RedisArticleKey.TIME_ARTICLE_PREFIX + articleId);
if (cutoff > Article.ONE_WEEK_IN_SECONDS) { //距离发布时间过去7天,不可以再投票
return Result.fail("发布日期超过7天,不能再投票");
}
boolean flag = redisTemplate.opsForSet().isMember(RedisArticleKey.VOTED_KEY_PREFIX + articleId,
RedisArticleKey.VOTED_USER_KEY_PREFIX + userId);
if (flag == true) {
return Result.fail("不可重复投票");
}
//投票一次加432分
redisTemplate.opsForZSet().incrementScore(RedisArticleKey.SCORE_KEY,
RedisArticleKey.SCORE_ARTICLE_PREFIX + articleId,
Article.VOTE_SCORE);
//追加到文章的投票列表中
redisTemplate.opsForSet().add(RedisArticleKey.VOTED_KEY_PREFIX + articleId,
RedisArticleKey.VOTED_USER_KEY_PREFIX + userId);
//将文章的详细信息中的投票数量加1
Integer votes = (Integer) redisTemplate.opsForHash().get(RedisArticleKey.ARTICLE_DETAIL_KEY_PREFIX + articleId,
RedisArticleKey.ARTICLE_DETAIL_VOTES);
redisTemplate.opsForHash().put(RedisArticleKey.ARTICLE_DETAIL_KEY_PREFIX + articleId,
RedisArticleKey.ARTICLE_DETAIL_VOTES, votes+1L);
return Result.success("投票成功");
}
}