赞
踩
1. 项目介绍
黑马点评项目是一个前后端分离项目,类似于大众点评,实现了发布查看商家,达人探店,点赞,关注等功能,业务可以帮助商家引流,增加曝光度,也可以为用户提供查看提供附近消费场所,主要。用来配合学习Redis的知识。
1.1 项目使用的技术栈
SpringBoot+MySql+Lombok+MyBatis-Plus+Hutool+Redis
1.2项目架构
采用单体架构
1.3项目地址
黑马点评: 基于springboot +mybatis+redis实现的多功能探店APP,涵盖发博客,评价,定位,关注,共同关注,秒杀,消息推送等多个功能
2.功能模块
2.1用户登录模块
手机号创建用户
- private User createUserWithPhone(String phone) {
- User user = new User();
- user.setPhone(phone);
- user.setNickName(USER_NICK_NAME_PREFIX+RandomUtil.randomString(10));
- save(user);
- return user;
-
- }
登录验证
- @Override
- public Result login(LoginFormDTO loginForm, HttpSession session) {
- String phone = loginForm.getPhone();
- String code = loginForm.getCode();
- if (RegexUtils.isPhoneInvalid(phone)){
- return Result.fail("手机号格式错误!");
- }
-
- String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY+phone);
- if (cacheCode ==null || !cacheCode.equals(code)){
- return Result.fail("验证码错误");
- }
- User user = query().eq("phone", phone).one();
- if (user ==null){
- user=createUserWithPhone(phone);
- }
- String token = UUID.randomUUID().toString(true);
- UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
- Map<String, Object> userMap = BeanUtil.beanToMap(userDTO,new HashMap<>(),
- CopyOptions.create().setIgnoreNullValue(true)
- .setFieldValueEditor((fieldName,fieldValue)->fieldValue.toString()));
- String tokenKey = LOGIN_USER_KEY + token;
- stringRedisTemplate.opsForHash().putAll(tokenKey,userMap);
- stringRedisTemplate.expire(tokenKey,LOGIN_USER_TTL,TimeUnit.MINUTES);
- return Result.ok(token);
- }
发送验证码
- @Resource
- private StringRedisTemplate stringRedisTemplate;
- @Override
- public Result sendCode(String phone, HttpSession session) {
- if (RegexUtils.isPhoneInvalid(phone)){
- return Result.fail("手机号格式错误!");
- }
-
-
- String code = RandomUtil.randomNumbers(6);
-
- stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY+phone,code,LOGIN_CODE_TTL, TimeUnit.MINUTES);
-
-
- log.debug("你的手机验证码为:{},时长为2分钟,请尽快使用",code);
- return Result.ok();
- }
登出(代码量太小直接在controll实现)
- @PostMapping("/logout")
- public Result logout(){
- UserHolder.removeUser();
- return Result.ok();
- }
拦截器
登录拦截器
- package com.hmdp.utils;
-
- import cn.hutool.core.bean.BeanUtil;
- import cn.hutool.core.util.StrUtil;
- import com.hmdp.dto.UserDTO;
- import org.springframework.data.redis.core.StringRedisTemplate;
- import org.springframework.web.servlet.HandlerInterceptor;
-
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpServletResponse;
- import javax.servlet.http.HttpSession;
- import java.util.Map;
- import java.util.concurrent.TimeUnit;
-
-
- public class LoginInterceptor implements HandlerInterceptor {
-
-
- @Override
- public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
- if (UserHolder.getUser()==null){
- response.setStatus(401);
- return false;
- }
- return true;
- }
-
- }
刷新token拦截器,长时间不操作用户token过期
- package com.hmdp.utils;
-
- import cn.hutool.core.bean.BeanUtil;
- import cn.hutool.core.util.StrUtil;
- import com.hmdp.dto.UserDTO;
- import org.springframework.data.redis.core.StringRedisTemplate;
- import org.springframework.web.servlet.HandlerInterceptor;
-
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpServletResponse;
- import javax.servlet.http.HttpSession;
- import java.util.Map;
- import java.util.concurrent.TimeUnit;
-
-
- public class RefreshTokenInterceptor implements HandlerInterceptor {
- private StringRedisTemplate stringRedisTemplate;
-
- public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate){
- this.stringRedisTemplate=stringRedisTemplate;
- }
-
- @Override
- public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
- String token = request.getHeader("authorization");
- if (StrUtil.isBlank(token)){
-
- return true;
- }
- String key = RedisConstants.LOGIN_USER_KEY + token;
- Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
- if (userMap.isEmpty()){
- return true;
- }
- UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
- UserHolder.saveUser(userDTO);
- stringRedisTemplate.expire(key,RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
- return true;
- }
-
- @Override
- public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
- UserHolder.removeUser();
- }
- }
在注册中心加入这两个拦截器并配置路径
- package com.hmdp.config;
-
- import com.hmdp.utils.LoginInterceptor;
- import com.hmdp.utils.RefreshTokenInterceptor;
- import org.springframework.context.annotation.Configuration;
- import org.springframework.data.redis.core.StringRedisTemplate;
- import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
- import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
-
- import javax.annotation.Resource;
-
- @Configuration
- public class MvcConfig implements WebMvcConfigurer {
-
- @Resource
- private StringRedisTemplate stringRedisTemplate;
-
- @Override
- public void addInterceptors(InterceptorRegistry registry) {
- registry.addInterceptor(new LoginInterceptor())
- .excludePathPatterns(
- "/blog/hot",
- "/voucher/**",
- "/upload/**",
- "/shop/**",
- "/shop-type/**",
- "/user/code",
- "/user/login"
- ).order(1);
- registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
- }
- }
2.2查询商户模块
进入主页,先从Redis中读出商户分类信息,若Redis中为空则向MySQL中读取,并写入Redis中。主页店铺分类信息为常用信息,应使用Redis避免频繁读取数据库。 该功能的实现分别应对Redis缓存容易出现的三种给出了三个不同的解决方案:
1)缓存穿透(用户对不存在的数据进行大量请求,在Redis中为未中便会请求MySQL数据库,造成数据库崩溃)
解决措施(缓存空对象,布隆过滤器)
这里采用设置默认值的方式应对穿透,当请求像MySQL中也未命中数据时,会返回一个默认值并写入Redis缓存。
2)缓存击穿(热点数据在Redis中的缓存失效,大量同时访问MySQL造成崩溃)
解决措施(设置逻辑过期,互斥锁)
这里采用给热点数据在Redis中的缓存设置逻辑过期+互斥锁
3)缓存雪崩(Redis中大量缓存同时失效或Redis宕机,大量请求同时访问数据库,造成数据库崩溃)
解决措施(设置多级缓存,采用Redis集群服务,给缓存过期时间加上一个随机值,在业务中添加限流)
- package com.hmdp.utils;
-
-
- import cn.hutool.core.util.BooleanUtil;
- import cn.hutool.core.util.StrUtil;
- import cn.hutool.json.JSONObject;
- import cn.hutool.json.JSONUtil;
- import com.hmdp.entity.Shop;
- import lombok.extern.slf4j.Slf4j;
- import org.springframework.data.redis.core.StringRedisTemplate;
- import org.springframework.stereotype.Component;
-
- import java.time.LocalDateTime;
- import java.util.concurrent.ExecutorService;
- import java.util.concurrent.Executors;
- import java.util.concurrent.TimeUnit;
- import java.util.function.Function;
-
- import static com.hmdp.utils.RedisConstants.*;
-
- @Slf4j
- @Component
- public class CacheClient {
-
- private final StringRedisTemplate stringRedisTemplate;
-
-
- public CacheClient(StringRedisTemplate stringRedisTemplate) {
- this.stringRedisTemplate = stringRedisTemplate;
- }
-
- public void set(String key, Object value, Long time, TimeUnit unit){
- stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),time,unit);
- }
-
- public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit){
-
- RedisData redisData = new RedisData();
- redisData.setData(value);
- redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
- stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
- }
-
- public <R,ID> R queryWithPassThrougn(
- String keyPrefix, ID id, Class<R> type, Function<ID,R> dbFallback,Long time, TimeUnit unit){
- String key = keyPrefix + id;
- String json = stringRedisTemplate.opsForValue().get(key);
- if (StrUtil.isNotBlank(json)){
-
- return JSONUtil.toBean(json, type);
- }
- if (json!=null){
- return null;
- }
- R r = dbFallback.apply(id);
- if (r==null){
- stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
- return null;
- }
- this.set(key,r,time,unit);
-
- return r;
- }
-
- private static final ExecutorService CACHE_REBUILD_EXECUTOR= Executors.newFixedThreadPool(10);
-
- public <R,ID> R queryWithLogicalExpire(
- String keyPrefix, ID id, Class<R> type, Function<ID,R> dbFallback,Long time, TimeUnit unit){
- String key = CACHE_SHOP_KEY + id;
- String shopJson = stringRedisTemplate.opsForValue().get(key);
- if (StrUtil.isBlank(shopJson)){
-
- return null;
- }
- RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
- JSONObject data = (JSONObject) redisData.getData();
- R r = JSONUtil.toBean(data, type);
- LocalDateTime expireTime = redisData.getExpireTime();
- if (expireTime.isAfter(LocalDateTime.now())){
- return r;
- }
- String lockKey = LOCK_SHOP_KEY + id;
- boolean isLock = tryLock(lockKey);
- if (isLock){
- CACHE_REBUILD_EXECUTOR.submit(()->{
- try {
- R r1 = dbFallback.apply(id);
- this.setWithLogicalExpire(key,r1,time,unit);
- } catch (Exception e) {
- throw new RuntimeException(e);
- } finally {
- unlock(lockKey);
- }
- });
- }
-
-
- return r;
- }
-
- private boolean tryLock(String key){
- Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
- return BooleanUtil.isTrue(flag);
- }
-
- private void unlock(String key){
- stringRedisTemplate.delete(key);
- }
-
- }
2.3优惠券秒杀模块
采用异步下单的方式,先运行Lua脚本,判断是否下过单,若未下过单,则扣减Redis库存,脚本运行成功,有购买资格,则生成一个全局Id作为订单id,生成订单信息,把订单保存到一个阻塞队列,阻塞队列收到订单后,获取分布式锁后再把订单信息和库存信息同步到MySQL,然后释放锁。该模块利用分布式锁实现一人一单功能,利用Lua确保库存不会变负数。
- package com.hmdp.service.impl;
-
- import com.hmdp.dto.Result;
- import com.hmdp.entity.VoucherOrder;
- import com.hmdp.mapper.VoucherOrderMapper;
- import com.hmdp.service.ISeckillVoucherService;
- import com.hmdp.service.IVoucherOrderService;
- import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
- import com.hmdp.utils.RedisIdWorker;
- import com.hmdp.utils.UserHolder;
- import lombok.extern.slf4j.Slf4j;
- import org.redisson.api.RLock;
- import org.redisson.api.RedissonClient;
- import org.springframework.aop.framework.AopContext;
- import org.springframework.core.io.ClassPathResource;
- import org.springframework.data.redis.core.StringRedisTemplate;
- import org.springframework.data.redis.core.script.DefaultRedisScript;
- import org.springframework.stereotype.Service;
- import org.springframework.transaction.annotation.Transactional;
-
- import javax.annotation.PostConstruct;
- import javax.annotation.Resource;
- import java.util.Collections;
- import java.util.concurrent.ArrayBlockingQueue;
- import java.util.concurrent.BlockingQueue;
- import java.util.concurrent.ExecutorService;
- import java.util.concurrent.Executors;
-
- /**
- * <p>
- * 服务实现类
- * </p>
- *
- * @author 青云
- * @since 2021-12-22
- */
- @Slf4j
- @Service
- public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
-
- @Resource
- private ISeckillVoucherService seckillVoucherService;
-
- @Resource
- private RedisIdWorker redisIdWorker;
-
- @Resource
- private StringRedisTemplate stringRedisTemplate;
-
- @Resource
- private RedissonClient redissonClient;
- private static final DefaultRedisScript<Long> SECKILL_SCRIPT;
-
- static {
- SECKILL_SCRIPT=new DefaultRedisScript<>();
- SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));
- SECKILL_SCRIPT.setResultType(Long.class);
- }
-
- private BlockingQueue<VoucherOrder> orderTasks=new ArrayBlockingQueue<>(1024*1024);
-
- private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();
-
- @PostConstruct
- private void init(){
- SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandle());
- }
- private class VoucherOrderHandle implements Runnable{
-
- @Override
- public void run() {
- while (true){
- try {
- VoucherOrder voucherOrder = orderTasks.take();
- handleVoucherOrder(voucherOrder);
- } catch (InterruptedException e) {
- log.error("处理订单异常",e);
- }
- }
- }
- }
- private IVoucherOrderService proxy;
-
- private void handleVoucherOrder(VoucherOrder voucherOrder) {
- Long userId = voucherOrder.getUserId();
- RLock lock= redissonClient.getLock("lock:order:" + userId);
-
- boolean isLock = lock.tryLock();
- if (!isLock){
- log.error("不允许重复下单");
- return ;
- }
- //获取代理对象
- try {
- proxy.createVoucherOrder(voucherOrder);
- } finally {
- lock.unlock();
- }
- }
-
- @Override
- public Result seckillVoucher(Long voucherId) {
- Long userId = UserHolder.getUser().getId();
- //执行lua脚本
- Long result = stringRedisTemplate.execute(
- SECKILL_SCRIPT,
- Collections.emptyList(),
- voucherId.toString(), userId.toString()
- );
- //判断结果是否为0
- int r = result.intValue();
- if (r!=0){
- //不为0,代表没有购买资格
- return Result.fail(r==1?"库存不足":"不能重复下单");
- }
- //为0,有购买资格,把下单信息保存到阻塞队列
- VoucherOrder voucherOrder = new VoucherOrder();
-
- long orderId = redisIdWorker.nextId("order");
-
- voucherOrder.setId(orderId);
- voucherOrder.setUserId(userId);
- voucherOrder.setVoucherId(voucherId);
- orderTasks.add(voucherOrder);
-
- proxy = (IVoucherOrderService) AopContext.currentProxy();
-
- //返回订单id
- return Result.ok(orderId);
- }
-
- // @Override
- // public Result seckillVoucher(Long voucherId) {
- // //查询优惠券
- // SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
- //
- // //判断秒杀是否开始
- // if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
- // return Result.fail("秒杀尚未开始!");
- // }
- // //判断秒杀是否结束
- // if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
- // return Result.fail("秒杀已结束!");
- // }
- // //判断库存是否充足
- // if (voucher.getStock()<1){
- // return Result.fail("库存不足!");
- // }
- //
- // Long userId = UserHolder.getUser().getId();
- synchronized (UserHolder.getUser().getId().toString().intern()) {
- SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
- //
- // RLock lock= redissonClient.getLock("lock:order:" + userId);
- //
- // boolean isLock = lock.tryLock();
- // if (!isLock){
- // return Result.fail("不允许重复下单");
- // }
- // //获取代理对象
- // try {
- // IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
- // return proxy.createVoucherOrder(voucherId);
- // } finally {
- // lock.unlock();
- // }
- //
- // }
-
- @Transactional
- public void createVoucherOrder(VoucherOrder voucherOrder) {
- Long userId = voucherOrder.getId();
-
- int count = query().eq("user_id", userId).eq("voucher_id", voucherOrder.getVoucherId()).count();
- if (count>0){
- log.error("获取次数已上限");
- return ;
- }
- //扣减库存
- boolean success = seckillVoucherService.update()
- .setSql("stock=stock-1")
- .eq("voucher_id", voucherOrder.getVoucherId())
- // .eq("stock",voucher.getStock())
- .gt("stock",0)
- .update();
- if (!success){
- log.error("库存不足");
- return ;
- }
- //创建订单
- // VoucherOrder voucherOrder = new VoucherOrder();
- // long orderId = redisIdWorker.nextId("order");
- // voucherOrder.setId(orderId);
- // voucherOrder.setUserId(userId);
- // voucherOrder.setVoucherId(voucherOrder);
- save(voucherOrder);
- //返回订单id
- // return Result.ok(orderId);
- }
- }
lua脚本
- -- 1.参数列表
- -- 1.1 优惠券id
- local voucherId = ARGV[1]
- -- 1.2 用户id
- local userId = ARGV[2]
-
- -- 2.数据key
- -- 2.1 库存key key 是优惠的业务名称加优惠券id value 是优惠券的库存数
- local stockKey = 'seckill:stock:' .. voucherId
- -- 2.2 订单key key 也是拼接的业务名称加优惠权id 而value是用户id, 这是一个set集合,凡购买该优惠券的用户都会将其id存入集合中
- local orderKey = 'seckill:order:' .. voucherId
-
- -- 3.脚本业务
- -- 3.1 判断库存是否充足 get stockKey
- if (tonumber(redis.call('get', stockKey)) <= 0) then --将get的value先转为数字类型才能判断比较
- -- 3.2 库存不足,返回1
- return 1
- end
- -- 3.3 判断用户是否下单 sismember orderKey userId命令,判断当前key集合中,是否存在该value;返回1存在,0不存在
- if (redis.call('sismember', orderKey, userId) == 1) then
- --3.4 存在说明是重复下单,返回2
- return 2
- end
- -- 3.5 扣库存
- redis.call('incrby', stockKey, -1)
- -- 3.6 下单(保存用户)
- redis.call('sadd', orderKey, userId)
- return 0
2.4博客模块
点赞:用户浏览博客时,可以对博客进行点赞,点赞过的用户id,写入,Redis缓存中(zset:博客id,用户ID,时间)博客页并展示点赞次数和点赞列表头像,展示点赞列表时,注意点赞列表按时间排序,点赞时间早的排在前面,SQL语句应拼接order By 。
- package com.hmdp.service.impl;
-
- import cn.hutool.core.bean.BeanUtil;
- import cn.hutool.core.util.BooleanUtil;
- import cn.hutool.core.util.StrUtil;
- import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
- import com.hmdp.dto.Result;
- import com.hmdp.dto.ScrollResult;
- import com.hmdp.dto.UserDTO;
- import com.hmdp.entity.Blog;
- import com.hmdp.entity.Follow;
- import com.hmdp.entity.User;
- import com.hmdp.mapper.BlogMapper;
- import com.hmdp.service.IBlogService;
- import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
- import com.hmdp.service.IFollowService;
- import com.hmdp.service.IUserService;
- import com.hmdp.utils.SystemConstants;
- import com.hmdp.utils.UserHolder;
- import org.springframework.data.redis.core.StringRedisTemplate;
- import org.springframework.data.redis.core.ZSetOperations;
- import org.springframework.stereotype.Service;
-
- import javax.annotation.Resource;
- import java.util.ArrayList;
- import java.util.Collections;
- import java.util.List;
- import java.util.Set;
- import java.util.stream.Collectors;
-
- import static com.hmdp.utils.RedisConstants.BLOG_LIKED_KEY;
- import static com.hmdp.utils.RedisConstants.FEED_KEY;
-
- /**
- * <p>
- * 服务实现类
- * </p>
- *
- * @author 青云
- * @since 2021-12-22
- */
- @Service
- public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {
-
- @Resource
- private IUserService userService;
-
- @Resource
- private StringRedisTemplate stringRedisTemplate;
-
- @Resource
- private IFollowService followService;
- @Override
- public Result queryHotBlog(Integer current) {
- // 根据用户查询
- Page<Blog> page = query()
- .orderByDesc("liked")
- .page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE));
- // 获取当前页数据
- List<Blog> records = page.getRecords();
- // 查询用户
- records.forEach(blog -> {
- this.queryBlogUser(blog);
- this.isBlogLiked(blog);
- });
- return Result.ok(records);
- }
-
- private void queryBlogUser(Blog blog) {
- Long userId = blog.getUserId();
- User user = userService.getById(userId);
- blog.setName(user.getNickName());
- blog.setIcon(user.getIcon());
- }
-
- @Override
- public Result queryBlogById(Long id) {
- Blog blog = getById(id);
- if (blog ==null){
- return Result.fail("博客不存在");
- }
- queryBlogUser(blog);
-
- isBlogLiked(blog);
-
- return Result.ok(blog);
- }
-
- private void isBlogLiked(Blog blog) {
- UserDTO user = UserHolder.getUser();
- if (user == null){
- return;
- }
- Long userId = UserHolder.getUser().getId();
- //判断当前用户是否点赞
- String key = BLOG_LIKED_KEY + blog.getId();
- Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());
- blog.setIsLike(score !=null);
- }
-
- @Override
- public Result likeBlog(Long id) {
- Long userId = UserHolder.getUser().getId();
- //判断当前用户是否点赞
- String key = BLOG_LIKED_KEY + id;
- Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());
- if (score==null){
- boolean isSuccess = update().setSql("liked=liked+1").eq("id", id).update();
- if (isSuccess){
- stringRedisTemplate.opsForZSet().add(key,userId.toString(),System.currentTimeMillis());
- }
- }else {
- boolean isSuccess = update().setSql("liked = liked - 1").eq("id", id).update();
-
- if (isSuccess){
- stringRedisTemplate.opsForZSet().remove(key,userId.toString());
- }
- }
- return Result.ok();
- }
-
- @Override
- public Result queryBlogLikes(Long id) {
- String key = BLOG_LIKED_KEY + id;
- //查询top点赞用户
- Set<String> top5 = stringRedisTemplate.opsForZSet().range(key, 0, 4);
- if (top5==null||top5.isEmpty()){
- return Result.ok(Collections.emptyList());
- }
- List<Long> ids = top5.stream().map(Long::valueOf).collect(Collectors.toList());
- String idStr = StrUtil.join(",", ids);
- List<UserDTO> userDTOS = userService.query().in("id",ids)
- .last("ORDER BY FIELD(id,"+idStr+")").list()
- .stream()
- .map(user -> BeanUtil.copyProperties(user, UserDTO.class))
- .collect(Collectors.toList());
- return Result.ok(userDTOS);
- }
-
- @Override
- public Result saveBlog(Blog blog) {
- // 获取登录用户
- UserDTO user = UserHolder.getUser();
- blog.setUserId(user.getId());
- // 保存探店博文
- boolean isSuccess = save(blog);
- if (!isSuccess){
- return Result.fail("新增笔记失败");
- }
- //查询所有粉丝
- List<Follow> follows = followService.query().eq("follow_user_id", user.getId()).list();
- for (Follow follow : follows) {
- Long userId = follow.getUserId();
- String key = FEED_KEY + userId;
- stringRedisTemplate.opsForZSet().add(key,blog.getId().toString(),System.currentTimeMillis());
-
- }
- // 返回id
- return Result.ok(blog.getId());
- }
-
- @Override
- public Result queryBlogOfFollow(Long max, Integer offset) {
- Long userId = UserHolder.getUser().getId();
- String key = FEED_KEY + userId;
- Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet()
- .reverseRangeByScoreWithScores(key, 0, max, offset, 2);
- if (typedTuples==null||typedTuples.isEmpty()){
- return Result.ok();
- }
- ArrayList<Long> ids = new ArrayList<>(typedTuples.size());
- long minTime=0;
- int os=1;
- for (ZSetOperations.TypedTuple<String> tuple : typedTuples) {
- String idStr = tuple.getValue();
- ids.add(Long.valueOf(idStr));
- long time = tuple.getScore().longValue();
- if (time==minTime){
- os++;
- }else {
- minTime = time;
- os=1;
- }
-
- }
- String idStr = StrUtil.join(",", ids);
- List<Blog> blogs = query()
- .in("id",ids)
- .last("ORDER BY FIELD(id,"+idStr+")").list();
- for (Blog blog : blogs) {
- queryBlogUser(blog);
-
- isBlogLiked(blog);
-
- }
-
- ScrollResult r = new ScrollResult();
- r.setList(blogs);
- r.setOffset(os);
- r.setMinTime(minTime);
- return Result.ok(r);
- }
- }
2.5关注模块
- package com.hmdp.service.impl;
-
- import cn.hutool.core.bean.BeanUtil;
- import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
- import com.hmdp.dto.Result;
- import com.hmdp.dto.UserDTO;
- import com.hmdp.entity.Follow;
- import com.hmdp.mapper.FollowMapper;
- import com.hmdp.service.IFollowService;
- import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
- import com.hmdp.service.IUserService;
- import com.hmdp.utils.UserHolder;
- import org.springframework.data.redis.core.StringRedisTemplate;
- import org.springframework.stereotype.Service;
-
- import javax.annotation.Resource;
- import java.util.Collections;
- import java.util.List;
- import java.util.Set;
- import java.util.stream.Collectors;
-
- /**
- * <p>
- * 服务实现类
- * </p>
- *
- * @author 青云
- * @since 2021-12-22
- */
- @Service
- public class FollowServiceImpl extends ServiceImpl<FollowMapper, Follow> implements IFollowService {
-
- @Resource
- private StringRedisTemplate stringRedisTemplate;
-
- @Resource
- private IUserService userService;
- @Override
- public Result follow(Long followUserId, Boolean isFollow) {
- Long userId = UserHolder.getUser().getId();
- String key = "follows" + userId;
-
- if (isFollow){
- Follow follow=new Follow();
- follow.setFollowUserId(followUserId);
- follow.setUserId(userId);
- boolean isSuccess = save(follow);
- if (isSuccess){
- stringRedisTemplate.opsForSet().add(key,followUserId.toString());
- }
- }else {
- boolean isSuccess = remove(new QueryWrapper<Follow>()
- .eq("user_id", userId)
- .eq("follow_user_id", followUserId)
- );
- if (isSuccess){
- stringRedisTemplate.opsForSet().remove(key,followUserId.toString());
-
- }
- }
- return Result.ok();
- }
-
- @Override
- public Result isFollow(Long followUserId) {
- Long userId = UserHolder.getUser().getId();
- Integer count = query().eq("user_id", userId)
- .eq("follow_user_id", followUserId)
- .count();
- return Result.ok(count>0);
- }
-
- @Override
- public Result followCommons(Long id) {
- Long userId = UserHolder.getUser().getId();
- String key = "follows" + userId;
- String key2 = "follows" + id;
- Set<String> intersect = stringRedisTemplate.opsForSet().intersect(key, key2);
- if (intersect==null||intersect.isEmpty()){
- return Result.ok(Collections.emptyList());
- }
- List<Long> ids = intersect.stream().map(Long::valueOf).collect(Collectors.toList());
- List<UserDTO> userDTOS = userService.listByIds(ids).stream()
- .map(user -> BeanUtil.copyProperties(user, UserDTO.class))
- .collect(Collectors.toList());
- return Result.ok(userDTOS);
- }
- }
2.6订阅模块
用户发布的内容推送给粉丝,实现策略有三种模式:拉取模式,推模式,推拉结合模式
该处实现了推模式,发布博客时,把博客推送给粉丝,会向粉丝的信箱(ZSet:粉丝id,博客id,时间)中存入博客id,用户查看订阅时,即根据信箱滚动分页查询最新的博客
- @Override
- public Result queryBlogOfFollow(Long max, Integer offset) {
- Long userId = UserHolder.getUser().getId();
- String key = FEED_KEY + userId;
- Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet()
- .reverseRangeByScoreWithScores(key, 0, max, offset, 2);
- if (typedTuples==null||typedTuples.isEmpty()){
- return Result.ok();
- }
- ArrayList<Long> ids = new ArrayList<>(typedTuples.size());
- long minTime=0;
- int os=1;
- for (ZSetOperations.TypedTuple<String> tuple : typedTuples) {
- String idStr = tuple.getValue();
- ids.add(Long.valueOf(idStr));
- long time = tuple.getScore().longValue();
- if (time==minTime){
- os++;
- }else {
- minTime = time;
- os=1;
- }
-
- }
- String idStr = StrUtil.join(",", ids);
- List<Blog> blogs = query()
- .in("id",ids)
- .last("ORDER BY FIELD(id,"+idStr+")").list();
- for (Blog blog : blogs) {
- queryBlogUser(blog);
-
- isBlogLiked(blog);
-
- }
-
- ScrollResult r = new ScrollResult();
- r.setList(blogs);
- r.setOffset(os);
- r.setMinTime(minTime);
- return Result.ok(r);
- }
2.7签到模块
使用时间bitMap,打卡取1,为打卡取0,从第0位开始,n日的打卡数据在n-1位
把当月签到数据和1做与运算,得到最近一天是否打卡,为0则直接返回,为1则把签到数据右移一位和1做与运算,循环,直到与运算结果为0,循环次数为连续签到天数。
- @Override
- public Result sign() {
- Long userId = UserHolder.getUser().getId();
- LocalDateTime now = LocalDateTime.now();
- String keySuffix= now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
- String key = USER_SIGN_KEY + userId + keySuffix;
- int dayOfMonth = now.getDayOfMonth();
- stringRedisTemplate.opsForValue().setBit(key,dayOfMonth-1,true);
- return Result.ok();
- }
-
- @Override
- public Result signCount() {
- Long userId = UserHolder.getUser().getId();
- LocalDateTime now = LocalDateTime.now();
- String keySuffix= now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
- String key = USER_SIGN_KEY + userId + keySuffix;
- int dayOfMonth = now.getDayOfMonth();
-
- List<Long> result = stringRedisTemplate.opsForValue().bitField(key, BitFieldSubCommands.create()
- .get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth))
- .valueAt(0)
- );
- if (result==null||result.isEmpty()){
- return Result.ok(0);
- }
- Long num = result.get(0);
- if (num==null||num==0){
- return Result.ok(0);
- }
- int count=0;
- while (true){
- if ((num&1)==0){
- break;
- }else {
- count++;
- }
- num >>>=1;
- }
- return Result.ok(count);
- }
3.心得体会
黑马点评是一个非常适合我们学习的项目,尽管功能大部分是Redis实现,但是可以学到很多新知识,例如秒杀的各种情况如何解决等,希望经过以后的学习能轻松的独立写出类似项目
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。