#赞踩功能的实现
##1.LikeController
@Controller
public class LikeController {
@Autowired
HostHolder hostHolder;
@Autowired
LikeService likeService;
@Autowired
NewsService newsService;
@Autowired
EventProducer eventProducer;
@RequestMapping(path = {"/like"}, method = {RequestMethod.GET, RequestMethod.POST})
@ResponseBody
public String like(@Param("newId") int newsId) {
//在likeService中进行点赞具体操作
long likeCount = likeService.like(hostHolder.getUser().getId(), EntityType.ENTITY_NEWS, newsId);
// 更新new的被点赞数,其实就是调用DAO层更新数据库里的likeCount
newsService.updateLikeCount(newsId, (int) likeCount);
return ToutiaoUtil.getJSONString(0, String.valueOf(likeCount));
}
@RequestMapping(value = "/dislike",method = {RequestMethod.POST,RequestMethod.GET})
@ResponseBody
public String dislike(@RequestParam("newsId") int newsId){
int userId=hostHolder.getUser().getId();
long likeCount=likeService.disLike(userId, EntityType.ENTITY_NEWS,newsId);
//更新数据库里的likecount值
newsService.updateLikeCount(newsId,(int)likeCount);
return ToutiaoUtil.getJSONString(0,String.valueOf(likeCount));
}
}
##2.前端:
###home.html
<div class="post">
<div class="votebar">
有关点赞的操作
#if ($vo.like > 0)
<button class="click-like up pressed" data-id="$!{vo.news.id}" title="赞同"><i class="vote-arrow"></i><span class="count">$!{vo.news.likeCount}</span></button>
#else
<button class="click-like up" data-id="$!{vo.news.id}" title="赞同"><i class="vote-arrow"></i><span class="count">$!{vo.news.likeCount}</span></button>
#end
#有关灭赞的操作
#if($vo.like < 0)
<button class="click-dislike down pressed" data-id="$!{vo.news.id}" title="反对"><i class="vote-arrow"></i></button>
#else
<button class="click-dislike down" data-id="$!{vo.news.id}" title="反对"><i class="vote-arrow"></i></button>
#end
可以看到点赞和撤赞操作都有对应的button
###static/scripts/main/site/home.js
(function (window, undefined) {
var PopupLogin = Base.getClass('main.component.PopupLogin');
var PopupUpload = Base.getClass('main.component.PopupUpload');
var ActionUtil = Base.getClass('main.util.Action');
Base.ready({
initialize: fInitialize,
binds: {
//.表示class #表示id
'click .js-login': fClickLogin,
'click .js-share': fClickShare
},
events: {
//一个点赞操作,一个撤赞操作
'click button.click-like': fClickLike,
'click button.click-dislike': fClickDisLike
}
});
//其他方法略过
function fClickLike(oEvent) {
var that = this;
var oEl = $(oEvent.currentTarget);
var sId = $.trim(oEl.attr('data-id'));
// 已经操作过 || 不存在Id || 正在提交 ,则忽略
if (oEl.hasClass('pressed') || !sId || that.actioning) {
return;
}
that.actioning = true;
ActionUtil.like({
newsId: sId,
call: function (oResult) {
oEl.find('span.count').html(oResult.msg);
oEl.addClass('pressed');
oEl.parent().find('.click-dislike').removeClass('pressed');
},
error: function () {
alert('出现错误,请重试');
},
always: function () {
that.actioning = false;
}
});
}
function fClickDisLike(oEvent) {
var that = this;
var oEl = $(oEvent.currentTarget);
var sId = $.trim(oEl.attr('data-id'));
// 已经操作过 || 不存在Id || 正在提交 ,则忽略
if (oEl.hasClass('pressed') || !sId || that.actioning) {
return;
}
that.actioning = true;
ActionUtil.dislike({
newsId: sId,
call: function (oResult) {
oEl.addClass('pressed');
var oLikeBtn = oEl.parent().find('.click-like');
oLikeBtn.removeClass('pressed');
oLikeBtn.find('span.count').html(oResult.msg);
},
error: function () {
alert('出现错误,请重试');
},
always: function () {
that.actioning = false;
}
});
}
})(window);
###static/scripts/main/util/action.js中
/**
* 喜欢
* @param {Object} oConf
* @param {String} oConf.newsId 对象id
* @param {Function} oConf.call 成功回调
* @param {Function} oConf.error 失败回调
* @param {Function} oConf.always 操作的回调
*/
function fLike(oConf) {
var that = this;
that.post({
url: '/like',
data: {newsId: oConf.newsId},
call: oConf.call,
error: oConf.error,
always: oConf.always
});
}
/**
* 不喜欢
* @param {Object} oConf
* @param {String} oConf.newsId 对象id
* @param {Function} oConf.call 成功回调
* @param {Function} oConf.error 失败回调
* @param {Function} oConf.always 操作的回调
*/
function fDislike(oConf) {
var that = this;
that.post({
url: '/dislike',
data: {newsId: oConf.newsId},
call: oConf.call,
error: oConf.error,
always: oConf.always
});
}
简单的说就是当我们点赞时,会激发url:”/like”也就是会调用LikeController中与/like绑定的方法
##3.LikeService
###com.nowcoder.service.LikeService
@Service
public class LikeService {
@Autowired
JedisAdapter jedisAdapter;
//判断某个用户对某一项元素是否喜欢,思路就是在redis中找likeKey对应的set里是否有这个点赞人的userid
public int getLikeStatus(int userId,int entityType,int entityId){
String likeKey= RedisKeyUtil.getLikeKey(entityType,entityId);
if (jedisAdapter.sismember(likeKey,String.valueOf(userId))){
return 1;
}
String dislikeKey= RedisKeyUtil.getDisLikeKey(entityType,entityId);
return jedisAdapter.sismember(dislikeKey,String.valueOf(userId))?-1:0;
}
/**
* 喜欢点赞操作
* 返回 喜欢的数量
*/
public long like(int userId,int entityType,int entityId){
String likeKey=RedisKeyUtil.getLikeKey(entityType,entityId);
//把用户加到喜欢的likeKey的KV中,就是说这条咨询有多少人喜欢
//也就是说redis里面不同的likeKey作为k,每个likeKey代表的是不同类型(entityType)的特定ID的新闻有多少人喜欢
//V是一个set集合,里面就是为这条新闻点赞的userID们
jedisAdapter.sadd(likeKey,String.valueOf(userId));
//删除不喜欢中的userId
String disLikeKey=RedisKeyUtil.getDisLikeKey(entityType,entityId);
jedisAdapter.srem(disLikeKey,String.valueOf(userId));
//在返回有多少人喜欢
return jedisAdapter.scard(likeKey);
}
/**
* 不喜欢点赞操作
* 返回 喜欢的数量
*/
public long disLike(int userId,int entityType,int entityId){
String disLikeKey=RedisKeyUtil.getDisLikeKey(entityType,entityId);
//把用户加到不喜欢的disLikeKey的KV中
jedisAdapter.sadd(disLikeKey,String.valueOf(userId));
//删除喜欢中的userId
String likeKey=RedisKeyUtil.getLikeKey(entityType,entityId);
jedisAdapter.srem(likeKey,String.valueOf(userId));
//在返回有多少人喜欢
return jedisAdapter.scard(likeKey);
}
}
###com.nowcoder.util.ToutiaoUtil
public class RedisKeyUtil {
public static String SPILT=":";
public static String BIZ_LIKE="LIKE";
public static String BIZ_DISLIKE="DISLIKE";
private static String BIZ_EVENT = "EVENT";
public static String getEventQueueKey() {
return BIZ_EVENT;
}
public static String getLikeKey(int entityId,int entityType){
return BIZ_LIKE+SPILT+String.valueOf(entityType)+SPILT+String.valueOf(entityId);
}
public static String getDisLikeKey(int entityId,int entityType){
return BIZ_DISLIKE+SPILT+String.valueOf(entityType)+SPILT+String.valueOf(entityId);
}
}
在redis中点赞事件存储的kv键值对的名:
点赞:LIKE:int( entityType):int(entityId)
撤赞:DISLIKE:int( entityType):int(entityId)
redis 常见数据结构以及使用场景分析
##1.String
常用命令: set,get,decr,incr,mget 等。 String数据结构是简单的key-value类型,value其实不仅可以是String,也可以是数字。 常规key-value缓存应用;
常规计数:微博数,粉丝数等。
###举例:
jedis.set("hello", "world");//设置指定 key 的值
print(1, jedis.get("hello"));//world
jedis.rename("hello", "newhello");//重命名
print(1, jedis.get("newhello"));//world
jedis.setex("hello2", 15, "world");//为指定的 key 设置值及其过期时间
// 数值操作
jedis.set("pv", "100");
jedis.incr("pv");//默认加1
print(2, jedis.get("pv"));//101
jedis.decrBy("pv", 5);//减5
print(2, jedis.get("pv"));//96
print(3, jedis.keys("*"));//[newhello, hello2, pv]
##2.Hash
常用命令: hget,hset,hgetall 等。
Hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象,后续操作的时候,你可以直接仅 仅修改这个对象中的某个字段的值。 比如我们可以Hash数据结构来存储用户信息,商品信息等等。
###举例:
// hash, 可变字段
String userKey = "userxx";
jedis.hset(userKey, "name", "jim");//hash中添键值对
jedis.hset(userKey, "age", "12");
jedis.hset(userKey, "phone", "18666666666");
print(12, jedis.hget(userKey, "name"));//jim
print(13, jedis.hgetAll(userKey));//{phone=18666666666, name=jim, age=12}
jedis.hdel(userKey, "phone");
print(14, jedis.hgetAll(userKey));//{name=jim, age=12},删除了个phone
print(15, jedis.hexists(userKey, "email"));//false
print(16, jedis.hexists(userKey, "age"));//true
print(17, jedis.hkeys(userKey));//[name, age]
print(18, jedis.hvals(userKey));//[jim, 12]
jedis.hsetnx(userKey, "school", "zju");//不存在就添加
jedis.hsetnx(userKey, "name", "yxy");//存在就不添加
print(19, jedis.hgetAll(userKey));//{school=zju, name=jim, age=12}
##3.List
常用命令: lpush,rpush,lpop,rpop,lrange等
list 就是链表,Redis list 的应用场景非常多,也是Redis最重要的数据结构之一,比如微博的关注列表,粉丝列表, 消息列表等功能都可以用Redis的 list 结构来实现。
Redis list 的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。 另外可以通过 lrange 命令,就是从某个元素开始读取多少个元素,可以基于 list 实现分页查询,这个很棒的一个功能,基于 redis 实现简单的高性能分页,可以做类似微博那种下拉不断分页的东西(一页一页的往下走),性能高。
###举例:
String listName = "list";
jedis.del(listName);
for (int i = 0; i < 10; ++i) {
jedis.lpush(listName, "a" + String.valueOf(i));
}
print(4, jedis.lrange(listName, 0, 12)); // 最近来访10个id:[a9, a8, a7, a6, a5, a4, a3, a2, a1, a0]
print(5, jedis.llen(listName));//10
print(6, jedis.lpop(listName));//a9
print(7, jedis.llen(listName));//9,出去了个a9
print(8, jedis.lrange(listName, 2, 6)); //[a6, a5, a4, a3, a2]
print(9, jedis.lindex(listName, 3));//a5
##4.Set
常用命令: sadd,spop,smembers,sunion 等
set 对外提供的功能与list类似是一个列表的功能,特殊之处在于 set 是可以自动排重的。
当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在 一个set集合内的重要接口,这个也是list所不能提供的。可以基于 set 轻易实现交集、并集、差集的操作。
比如:在微博应用中,可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis可以非常 方便的实现如共同关注、共同粉丝、共同喜好等功能。
###项目中用到的:
/**
* 喜欢点赞操作
* 返回 喜欢的数量
*/
public long like(int userId,int entityType,int entityId){
String likeKey=RedisKeyUtil.getLikeKey(entityType,entityId);
//把用户加到喜欢的likeKey的KV中,就是说这条咨询有多少人喜欢
jedisAdapter.sadd(likeKey,String.valueOf(userId));
//删除不喜欢中的userId
String disLikeKey=RedisKeyUtil.getDisLikeKey(entityType,entityId);
jedisAdapter.srem(disLikeKey,String.valueOf(userId));
//在返回有多少人喜欢
return jedisAdapter.scard(likeKey);
}
补充:redis中的sadd对应redis中的sadd
SADD KEY_NAME VALUE1..VALUEN
Redis Sadd 命令将一个或多个成员元素加入到集合中,==已经存在于集合的成员元素将被忽略==。
假如集合 key 不存在,则创建一个只包含添加的元素作成员的集合。
当集合 key 不是集合类型时,返回一个错误。
实例:
redis 127.0.0.1:6379> SADD myset "hello"
(integer) 1
redis 127.0.0.1:6379> SADD myset "foo"
(integer) 1
redis 127.0.0.1:6379> SADD myset "hello"
(integer) 0
redis 127.0.0.1:6379> SMEMBERS myset
1) "hello"
2) "foo"
SISMEMBER KEY VALUE
==返回值==:
如果成员元素是集合的成员,返回 1 。 如果成员元素不是集合的成员,或 key 不存在,返回 0 。
实例:
redis 127.0.0.1:6379> SADD myset1 "hello"
(integer) 1
redis 127.0.0.1:6379> SISMEMBER myset1 "hello"
(integer) 1
redis 127.0.0.1:6379> SISMEMBER myset1 "world"
(integer) 0
###举例:
// 集合,点赞用户群, 共同好友
String likeKey1 = "newsLike1";
String likeKey2 = "newsLike2";
for (int i = 0; i < 10; ++i) {
jedis.sadd(likeKey1, String.valueOf(i));
jedis.sadd(likeKey2, String.valueOf(i * 2));
}
print(20, jedis.smembers(likeKey1));//[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(21, jedis.smembers(likeKey2));//[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
print(22, jedis.sunion(likeKey1, likeKey2));//取俩个set并集 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 14, 16, 18]
print(23, jedis.sdiff(likeKey1, likeKey2));//求不同 [1, 9, 3, 5, 7]
print(24, jedis.sinter(likeKey1, likeKey2));//求交集 [0, 2, 4, 6, 8]
print(25, jedis.sismember(likeKey1, "12"));//false
print(26, jedis.sismember(likeKey2, "12"));//true
jedis.srem(likeKey1, "5");//删除操作
print(27, jedis.smembers(likeKey1));//[0, 1, 2, 3, 4, 6, 7, 8, 9]
// 从1移动到2
jedis.smove(likeKey2, likeKey1, "14");//将前一个集合中的指定元素移到后一个集合中
print(28, jedis.smembers(likeKey1));//[0, 1, 2, 3, 4, 6, 7, 8, 9, 14]
print(29, jedis.scard(likeKey1));//求集合中元素的总个数 10
##5.Sorted Set
常用命令: zadd,zrange,zrem,zcard等
和set相比,sorted set增加了一个权重参数score,使得集合中的元素能够按score进行有序排列。
举例: 在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,弹幕消息(可以理解为按消息维 度的消息排行榜)等信息,适合使用 Redis 中的 SortedSet 结构进行存储。
###举例:
// 排序集合,有限队列,排行榜
String rankKey = "rankKey";
jedis.zadd(rankKey, 15, "Jim");
jedis.zadd(rankKey, 60, "Ben");
jedis.zadd(rankKey, 90, "Lee");
jedis.zadd(rankKey, 75, "Lucy");
jedis.zadd(rankKey, 80, "Mei");
print(30, jedis.zcard(rankKey));//集合中元素的总个数 5
print(31, jedis.zcount(rankKey, 61, 100));//指定范围内元素的个数 3
// 改错卷了
print(32, jedis.zscore(rankKey, "Lucy"));//75.0
jedis.zincrby(rankKey, 2, "Lucy");//增加指定数字
print(33, jedis.zscore(rankKey, "Lucy"));//77.0
jedis.zincrby(rankKey, 2, "Luc");//增加指定数字,没有就创建
print(34, jedis.zscore(rankKey, "Luc"));//2.0
print(35, jedis.zcount(rankKey, 0, 100));//指定范围内元素的个数 5
// 1-4 名 Luc
print(36, jedis.zrange(rankKey, 0, 10));//在指定范围内根据value排key,默认升序 [Luc, Jim, Ben, Lucy, Mei, Lee]
print(36, jedis.zrange(rankKey, 1, 3));//[Jim, Ben, Lucy],默认从头到尾数
print(36, jedis.zrevrange(rankKey, 1, 3));//[Mei, Lucy, Ben] 从尾到头数
for (Tuple tuple : jedis.zrangeByScoreWithScores(rankKey, "60", "100")) {
print(37, tuple.getElement() + ":" + String.valueOf(tuple.getScore()));
}
print(38, jedis.zrank(rankKey, "Ben"));//从头到尾排第几 2
print(39, jedis.zrevrank(rankKey, "Ben"));//从尾到头排第几 3
点赞流程分析:
@Controller
public class HomeController {
@Autowired
NewsService newsService;
@Autowired
UserService userService;
@Autowired
HostHolder hostHolder;
@Autowired
LikeService likeService;
private List<ViewObject> getNews(int userId, int offset, int limit) {
List<News> newsList = newsService.getLatestNews(userId, offset, limit);
int localUserId = hostHolder.getUser() != null ? hostHolder.getUser().getId() : 0;
List<ViewObject> vos = new ArrayList<>();
for (News news : newsList) {
ViewObject vo = new ViewObject();
vo.set("news", news);
vo.set("user", userService.getUser(news.getUserId()));
if(localUserId!=0){
//从这在redis中取值
vo.set("like", likeService.getLikeStatus(localUserId, EntityType.ENTITY_NEWS, news.getId()));
}else{
vo.set("like",0);
}
vos.add(vo);
}
return vos;
}
@RequestMapping(path = {"/", "/index"}, method = {RequestMethod.GET, RequestMethod.POST})
public String index(Model model,@RequestParam(value = "pop", defaultValue = "0") int pop) {
model.addAttribute("vos", getNews(0, 0, 10));
if (hostHolder.getUser() != null) {
pop = 0;
}
model.addAttribute("pop", pop);
return "home";
}
}
从上述代码我们能知道:
项目首页,也就是home.html,通过Model对象接受到从后端传来的vos链表对象。取到的key对应的value是从redis中去的,这就是真正使用redis的原因,存储在内存中,取值速度快。