目录

1. 项目介绍

2.各个功能模块

2.1  登录模块

2.1.1 实现短信登录

2.1.2 编写拦截器

2.2 查询商户模块

2.2.1 主页面查询商户类型

2.2.3 按距离查询商户

2.3 优惠券秒杀模块

2.4 博客模块

2.4.1 点赞

2.5 订阅模块

2.6 签到模块

2.6.1 签到功能

2.6.2 获取当月连续签到天数

3.项目学习收获


1. 项目介绍

黑马点评项目是一个前后端分离项目,类似于大众点评,实现了发布查看商家,达人探店,点赞,关注等功能,业务可以帮助商家引流,增加曝光度,也可以为用户提供查看提供附近消费场所,主要。用来配合学习Redis的知识。

1.1 项目使用的技术栈

SpringBoot+MySql+Lombok+MyBatis-Plus+Hutool+Redis

1.2项目架构

采用单体架构

后端部署在Tomcat上,前端部分部署在Nginx 。

2.各个功能模块

2.1  登录模块

2.1.1 实现短信登录

编写一个工具类校验手机号格式,例如

public class RegexUtils {/*** 是否是无效手机格式* @param phone 要校验的手机号* @return true:符合,false:不符合*/public static boolean isPhoneInvalid(String phone){return mismatch(phone, RegexPatterns.PHONE_REGEX);}/*** 是否是无效邮箱格式* @param email 要校验的邮箱* @return true:符合,false:不符合*/public static boolean isEmailInvalid(String email){return mismatch(email, RegexPatterns.EMAIL_REGEX);}/*** 是否是无效验证码格式* @param code 要校验的验证码* @return true:符合,false:不符合*/public static boolean isCodeInvalid(String code){return mismatch(code, RegexPatterns.VERIFY_CODE_REGEX);}// 校验是否不符合正则格式private static boolean mismatch(String str, String regex){if (StrUtil.isBlank(str)) {return true;}return !str.matches(regex);}
}

手机号码格式无误后生成验证码发送至手机,并将验证码内容写入到Redis。设置过期时间;

系统根据输入的手机号验证码进行与Redis中写入的验证码比对一致,即可登录成功,从MySQL中获取用户信息并生成Token,以Token为key将用户信息写入Redis中(hash),新用户则会注册信息并登录;

2.1.2 编写拦截器

登录拦截器,一些功能需要登录后才能使用

public class LoginInterceptor implements HandlerInterceptor {/**** @description: 登录拦截方法* @param: [javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, java.lang.Object]* @return: boolean* @date: 2022/10/25 17:27*/@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//        1.判断是否要做拦截if(UserHolder.getUser()==null){response.setStatus(401);return false;}
//        2.有用户则放行return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {UserHolder.removeUser();}
}

刷新Token拦截器,用户长时间没有操作会使Token过期,每次用户点击可以刷新Token过期时间

public class RefreshTokenInterceptor implements HandlerInterceptor {private StringRedisTemplate stringRedisTemplate;public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate){this.stringRedisTemplate=stringRedisTemplate;}/**** @description: 登录拦截方法* @param: [javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, java.lang.Object]* @return: boolean* @date: 2022/10/25 17:27*/@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//        1.获取tokenString token = request.getHeader("authorization");
//        2.判断token是否为空if(StrUtil.isBlank(token)){return true;}
//        4.基于token获取redis中的用户Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(LOGIN_USER_KEY + token);
//        3.判断用户是否为空if(userMap.isEmpty()){return true;}
//        5.将查询到的hash数据转为UserDTO对象UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(),false);
//        6.存在则保存用户信息到ThreadLocalUserHolder.saveUser(userDTO);
//        7.刷新token有效期stringRedisTemplate.expire(LOGIN_USER_KEY + token,LOGIN_USER_TTL, TimeUnit.SECONDS);
//        8.放行return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {UserHolder.removeUser();}
}

在注册中心添加这两个拦截器

@Configuration
public class MVCConfig implements WebMvcConfigurer {@Resourceprivate StringRedisTemplate stringRedisTemplate;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new LoginInterceptor())//登录拦截器.excludePathPatterns("/user/code","/user/login","/blog/hot","/shop/**","/shop-type/**","/voucher/**").order(1);registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).order(0);//刷新Token有效期    }
}

2.2 查询商户模块

2.2.1 主页面查询商户类型

进入主页,先从Redis中读出商户分类信息,若Redis中为空则向MySQL中读取,并写入Redis中。主页店铺分类信息为常用信息,应使用Redis避免频繁读取数据库。

2.2.2 商户详情页

该功能的实现分别应对Redis缓存容易出现的三种给出了三个不同的解决方案:

1)缓存穿透(用户对不存在的数据进行大量请求,在Redis中为未中便会请求MySQL数据库,造成数据库崩溃)

解决措施(缓存空对象,布隆过滤器)

这里采用设置默认值的方式应对穿透,当请求像MySQL中也未命中数据时,会返回一个默认值并写入Redis缓存。

2)缓存击穿(热点数据在Redis中的缓存失效,大量同时访问MySQL造成崩溃)

解决措施(设置逻辑过期,互斥锁)

这里采用给热点数据在Redis中的缓存设置逻辑过期+互斥锁

3)缓存雪崩(Redis中大量缓存同时失效或Redis宕机,大量请求同时访问数据库,造成数据库崩溃)

解决措施(设置多级缓存,采用Redis集群服务,给缓存过期时间加上一个随机值,在业务中添加限流)

这里采取给缓存过期时间加随机数的方式改进

解决方法封装成一个工具类了

@Component
@Slf4j
public class CacheClient {private final StringRedisTemplate stringRedisTemplate;public CacheClient(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}/*** * @description: 插入缓存 * @param: [java.lang.String, java.lang.Object, java.lang.Long] * @return: void * @date: 2022/10/29 21:06*/ public void set(String key, Object value, Long time, TimeUnit unit){stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),time,TimeUnit.MINUTES);}/*** * @description: 设置逻辑过期 * @param: [java.lang.String, java.lang.Object, java.lang.Long, java.util.concurrent.TimeUnit] * @return: void * @date: 2022/10/29 21:08*/ public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit){
//        1.设置逻辑过期RedisData redisData = new RedisData();redisData.setData(value);redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
//        2.写入RedisstringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(redisData));}/**** @description: 缓存穿透策略之设置默认值* @param: [java.lang.String, ID, java.lang.Class<R>]* @return: R* @date: 2022/10/29 23:18*/public <R,ID> R queryWithPassThrough(String keyPrefix,ID id,Class<R> type,Function<ID,R> dbFallBack, Long time, TimeUnit unit){String key = keyPrefix + id;
//        1.从Redis中查询缓存String json = stringRedisTemplate.opsForValue().get(key);
//        2.判断缓存是否存在if(StrUtil.isNotBlank(json)){
//        3.存在,直接返回return JSONUtil.toBean(json,type);}
//        4.不存在,判断是否是空字符串if(json!=null){
//        5.是空字符串return null;}
//        6.不是空字符串,则向数据库中查找R r = dbFallBack.apply(id);
//        7.数据库中未找到,设置值为空字符串并插入缓存if (r==null) {stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);return null;}
//        8.找到数据源this.set(key,r,time,unit);return r;}public <R,ID> R queryWithLogicalExpire(String keyPrefix,ID id,Class<R> type,Function<ID,R> dbFallBack,Long time,TimeUnit unit){String key = keyPrefix + id;
//        1.从redis查询缓存String json = stringRedisTemplate.opsForValue().get(key);if(StrUtil.isBlank(json)){
//        2.如果缓存未命中return null;}
//        3.如果命中,把json字符反序列化为对象RedisData redisData = JSONUtil.toBean(json, RedisData.class);R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);LocalDateTime expireTime = redisData.getExpireTime();System.out.println(expireTime);
//        5.判断是否过期if(expireTime.isAfter(LocalDateTime.now())){
//            5.1.未过期,直接返回对象return r;}
//        5.2已过期,缓存重建
//        6.缓存重建
//        6.1获取互斥锁String lockKey = LOCK_SHOP_KEY + id;boolean isLock = tryLock(lockKey);
//        6.2判断是否获取锁成功if(isLock){
//            6.3成功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;}/*** @description: 线程池* @param:* @return:* @date: 2022/10/30 14:27*/private static final ExecutorService CACHE_REBUILD_EXECUTOR= Executors.newFixedThreadPool(10);/*** @description: 获取锁* @param:* @return:* @date: 2022/10/30 14:23*/private boolean tryLock(String key){Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);return BooleanUtil.isTrue(flag);}/*** @description: 释放锁* @param: [java.lang.String]* @return: void* @date: 2022/10/30 14:24*/private void unLock(String key){stringRedisTemplate.delete("key");}}

2.2.3 按距离查询商户

第一步需要将商铺坐标按分类写入Redis(Geo),关键代码如下

@Testvoid loadShopData(){
//        1.查询店铺信息List<Shop> shopList = shopService.list();
//        2.把店铺分组,按照typeId分组,typeId一致发到一个集合Map<Long, List<Shop>> map = shopList.stream().collect(Collectors.groupingBy(Shop::getTypeId));
//        3.分批完成写入Redisfor (Map.Entry<Long, List<Shop>> entry : map.entrySet()) {
//            获取类型idLong typeTd = entry.getKey();String key = SHOP_GEO_KEY + typeTd;
//            获取通类型的店铺集合List<Shop> shops = entry.getValue();List<RedisGeoCommands.GeoLocation<String>> locations = new ArrayList<>(shops.size());
//            写入Redis GEOADD key 经度 纬度 memberfor (Shop shop : shops) {locations.add(new RedisGeoCommands.GeoLocation<>(shop.getId().toString(),new Point(shop.getX(), shop.getY())));}stringRedisTemplate.opsForGeo().add(key,locations);}}

请求参数中需要包含坐标,分页页码信息,类别ID,先向Redis中读取该类别的直到改页最后一个商铺商铺信息,并以距离排序,关键代码如下

GeoResults<RedisGeoCommands.GeoLocation<String>> results = stringRedisTemplate.opsForGeo().search(key,GeoReference.fromCoordinate(x, y),new Distance(5000),RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeDistance().limit(end));

再将数据进行解析,并把该页第一个商铺前面的商铺信息都跳过得到想要商铺的id和对应distance的键值对集合

List<GeoResult<RedisGeoCommands.GeoLocation<String>>> list=results.getContent();if(list.size()<=from){return Result.ok();}
//        5.截取from-end的部分List<Long> ids= new ArrayList<>(list.size());HashMap<String, Distance> distanceMap = new HashMap<>(list.size());
//        截取掉from之前的部分,不重复查询list.stream().skip(from).forEach(result->{
//            获取店铺idString shopId = result.getContent().getName();ids.add(Long.valueOf(shopId));
//            获取距离Distance distance = result.getDistance();distanceMap.put(shopId,distance);});

最后根据id查出商铺信息并将设置distance属性,返回商铺信息集合。

补充:如果不按距离排序则直接按页码和页面尺寸查询店铺信息

Page<Shop> page=query.eq("type_id",typeId).page(new Page<>(current,SystemConstants.DEFEAUT_PAGE_SIZE));

2.3 优惠券秒杀模块

采用异步下单的方式,先运行Lua脚本,判断是否下过单,若未下过单,则扣减Redis库存,脚本运行成功,有购买资格,则生成一个全局Id作为订单id,生成订单信息,把订单保存到一个阻塞队列,阻塞队列收到订单后,获取分布式锁后再把订单信息和库存信息同步到MySQL,然后释放锁。该模块利用分布式锁实现一人一单功能,利用Lua确保库存不会变负数。

@Slf4j
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Resourceprivate ISeckillVoucherService iSeckillVoucherService;@Resourceprivate RedissonClient redissonClient;@Resourceprivate RedisIdWorker redisIdWorker;@Resourceprivate StringRedisTemplate stringRedisTemplate;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);
//    创建单线程化线程池,用来运行实现Runnable的类private static final ExecutorService SECKILL_ORDER_EXCUTOR= Executors.newSingleThreadExecutor();
//    等依赖加载完再全部执行@PostConstructprivate void init(){SECKILL_ORDER_EXCUTOR.submit(new VoucherOrderHandler());}private class VoucherOrderHandler implements Runnable{@Overridepublic void run() {while (true) {try {//            获取队列中的订单信息VoucherOrder voucherOrder = orderTasks.take();//            创建订单handleVoucherOrder(voucherOrder);} catch (InterruptedException e) {log.error("订单处理异常",e);}}}}private void handleVoucherOrder(VoucherOrder voucherOrder){
//        1.获取用户Long userId = voucherOrder.getUserId();
//        2.创建锁对象RLock lock = redissonClient.getLock("lock:order:" + userId);
//        3.判断是否获取锁成功boolean isLock = lock.tryLock();if(!isLock){
//            获取锁失败log.error("不允许重复下单");return;}try {proxy.createVoucherOrder(voucherOrder);} finally {lock.unlock();}}/*** @description: 购买优惠券* @param: [java.lang.Long]* @return: com.hmdp.dto.Result* @date: 2022/11/2 21:14*/private IVoucherOrderService proxy;@Overridepublic Result seckillVoucher(Long voucherId) {
//        1.执行lua脚本
//        获取userIDLong userId = UserHolder.getUser().getId();Long result = stringRedisTemplate.execute(SECKILL_SCRIPT,Collections.emptyList(),voucherId.toString(),userId.toString());
//        2.判断结果为0int i = result.intValue();if(i!=0)
//        2.1不为0,代表没有购买资格{return Result.fail(i==1 ? "库存不足" : "不能重复下单");}
//        2.2为0,有购买资格,把下单信息保存到阻塞队列long orderId = redisIdWorker.nextId("order");VoucherOrder voucherOrder = new VoucherOrder();voucherOrder.setId(orderId);voucherOrder.setUserId(userId);voucherOrder.setVoucherId(voucherId);
//        TODO保存阻塞队列orderTasks.add(voucherOrder);
//        获取代理对象proxy=(IVoucherOrderService) AopContext.currentProxy();
//        3.返回订单idreturn Result.ok(orderId);}/*** @description: 创建订单* @param: [java.lang.Long]* @date: 2022/11/3 20:56* @param voucherOrder*/@Transactionalpublic void createVoucherOrder(VoucherOrder voucherOrder) {Long userId = voucherOrder.getUserId();
//        查询订单Integer count = query().eq("voucher_id", voucherOrder.getVoucherId()).eq("user_id", userId).count();if(count>0){log.error("用户已经购买过一次");return ;}
//        5.扣减库存boolean result = iSeckillVoucherService.update().setSql("stock=stock-1").eq("voucher_id", voucherOrder.getVoucherId()).gt("stock",0).update();//where stock >0if(!result){
//            扣减失败log.error("库存不足!");return;}save(voucherOrder);
//        返回订单idreturn;}}

2.4 博客模块

2.4.1 点赞

用户浏览博客时,可以对博客进行点赞,点赞过的用户id,写入,Redis缓存中(zset:博客id,用户ID,时间)博客页并展示点赞次数和点赞列表头像,展示点赞列表时,注意点赞列表按时间排序,点赞时间早的排在前面,SQL语句应拼接order By  。

点赞功能:

 public Result addLike(Long id) {
//        1.获取当前用户Long userId = UserHolder.getUser().getId();Blog blog = query().eq("id", id).one();
//        2.判断当前用户是否已经点赞String key = BLOG_LIKED_KEY + id;Double isLike = stringRedisTemplate.opsForZSet().score(key, userId.toString());
//        3.如果未点赞,可以点赞if(isLike==null){
//            4.数据库该帖点赞+1boolean isSuccess = update().setSql("liked=liked+1").eq("id", id).update();
//            5.保存用户id到该贴子的Redis的Zset集合,并更新blog的isLike属性if(BooleanUtil.isTrue(isSuccess)){stringRedisTemplate.opsForZSet().add(key,userId.toString(),System.currentTimeMillis());blog.setIsLike(true);}return Result.ok();}
//        6.如果已经点赞
//        7.数据库该贴点赞-1;boolean isSuccess = update().setSql("liked=liked-1").eq("id", id).update();
//        8.把set集合中的用户id移除if(BooleanUtil.isTrue(isSuccess)){stringRedisTemplate.opsForZSet().remove(key,userId.toString());blog.setIsLike(false);}return Result.ok();}

点赞列表:

 public Result queryLikesById(Long id) {
//        1.获取keyString key = BLOG_LIKED_KEY + id;
//        2.查询点赞时间前五的userIdSet<String> userIds = stringRedisTemplate.opsForZSet().range(key, 0, 4);if(userIds==null||userIds.isEmpty()){return Result.ok();}
//        3.根据userId查询UserList<Long> list = userIds.stream().map(Long::valueOf).collect(Collectors.toList());String idStr = StrUtil.join(",", list);
//        4.返回User集List<UserDTO> UserDTOS = userService.query().in("id",list).last("ORDER BY FIELD(id,"+idStr+")").list().stream().map(user -> BeanUtil.copyProperties(user,UserDTO.class)).collect(Collectors.toList());return Result.ok(UserDTOS);}

2.4.2 关注作者

与点赞功能相似,将关注用户写入Redis中(String:用户id,被关注与id)

2.5 订阅模块

用户发布的内容推送给粉丝,实现策略有三种模式:拉取模式,推模式,推拉结合模式

该处实现了推模式,发布博客时,把博客推送给粉丝,会向粉丝的信箱(ZSet:粉丝id,博客id,时间)中存入博客id,用户查看订阅时,即根据信箱滚动分页查询最新的博客

 public Result queryBlogByFollow(Long max, Integer offset) {
//        1.获取当前用户idLong userId = UserHolder.getUser().getId();String key =  FEED_KEY+userId;
//        2.查询信箱Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet().reverseRangeByScoreWithScores(key,0,max,offset,3);System.out.println(typedTuples);if(typedTuples==null||typedTuples.isEmpty()){return Result.ok();}List<Long> ids = new ArrayList<>(typedTuples.size());
//        3.解析数据long minTime=0;Integer os=1;for (ZSetOperations.TypedTuple<String> tuple : typedTuples) {
//            获取blogIdids.add(Long.valueOf(tuple.getValue()));
//            获取分数long score = tuple.getScore().longValue();if(minTime==score){os++;}else {os=1;minTime=score;}}
//        4.根据id查询blogString idStr = StrUtil.join(",", ids);List<Blog> blogs = query().in("id", ids).last("ORDER BY FIELD(id," + idStr + ")").list();for (Blog blog : blogs) {
//            获取点赞信息isLiked(blog);
//            获取用户信息User user = userService.getById(blog.getUserId());blog.setName(user.getNickName());blog.setIcon(user.getIcon());}
//        5.封装并返回ScrollResult scrollResult = new ScrollResult();scrollResult.setList(blogs);scrollResult.setOffset(os);scrollResult.setMinTime(minTime);return Result.ok(scrollResult);}

2.6 签到模块

2.6.1 签到功能

使用时间bitMap,打卡取1,为打卡取0,从第0位开始,n日的打卡数据在n-1位

//        2.获取日期LocalDateTime now = LocalDateTime.now();
//        3.拼接keyString keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));String key = USER_SIGN_KEY + userId + keySuffix;
//        4.获取今天是本月的第几天int dayOfMonth = now.getDayOfMonth();
//        5.写入Redis setBit key offset 1stringRedisTemplate.opsForValue().setBit(key,dayOfMonth-1,true);

2.6.2 获取当月连续签到天数

把当月签到数据和1做与运算,得到最近一天是否打卡,为0则直接返回,为1则把签到数据右移一位和1做与运算,循环,直到与运算结果为0,循环次数为连续签到天数。

//        2.获取用户在本月当前签到数据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 sign = result.get(0);if(sign==0||sign==null){return Result.ok(0);}
//        3.取出和1做与运算int count=0;while (true){if ((sign&1)==0) {//        4.判断是否为0//        4.1为0则返回break;}else {
//        4.2为1则count++,并将sign右移count++;}sign>>>=1;}

3.项目学习收获

项目实战可能碰到的场景,及问题,和解决方案

菜鸟项目练习:黑马点评项目总结相关推荐

  1. 黑马点评项目-短信登录功能

    一.导入黑马点评项目 1.代码下载 视频资源链接:P25 实战篇-02.短信登录-导入黑马点评项目 代码可以直接去黑马微信公众号上搜索,或者从下面的网盘链接中下载:链接: https://pan.ba ...

  2. 黑马点评项目笔记(四)社交、附近人、数据统计功能实现

    目录 达人探店 查看博文 点赞博文 点赞排行榜 好友关注 关注和取关 共同关注 关注推送(Feed流) Feed流的两种模式 Timeline 三种实现模式 基于推模式实现消息推送 滚动分页 附近商户 ...

  3. Redis学习笔记②实战篇_黑马点评项目

    若文章内容或图片失效,请留言反馈.部分素材来自网络,若不小心影响到您的利益,请联系博主删除. 资料链接:https://pan.baidu.com/s/1189u6u4icQYHg_9_7ovWmA( ...

  4. 黑马点评项目全部功能实现及详细笔记--Redis练手项目

    目录 一.项目详情 1.1 项目简介 1.2 数据库表设计 1.3 前端部署 1.4 后端搭建 二.短信登录 2.1 发送验证码 2.2 验证码登录 2.3 登录校验拦截器 2.4 退出登录(补充) ...

  5. 黑马点评项目全面业务总结

    1 黑马点评项目 1.1 短信登陆 1.1.1 短信登陆简介 session共享问题:多台服务器并不共享session存储空间,当请求切换到不同tomcat服务时导致数据丢失的问题. 在进行短信登录时 ...

  6. springboot-redis-mysql-nginx项目:黑马点评开发(更新中)

    springboot-redis-mysql-nginx项目:黑马点评开发 开篇导读 亲爱的小伙伴们大家好,希望通过此博客,小伙伴们就能理解各种redis的使用啦. 短信登录 这一块我们会使用redi ...

  7. 黑马点评项目-达人探店

    一.发布探店笔记 1.1 需求分析 探店笔记类似点评网站的评价,往往是图文结合.对应的表有两个: tb_blog:探店笔记表,包含笔记中标题.文字.图片等 tb_blog_comments:其他用户对 ...

  8. 【Redis企业实战】仿黑马点评项目

    目录 一.短信登陆:基于Redis实现共享session实现登录 1.发送短信验证码 2.短信验证码登录.注册 3.校验登陆状态 二.商户查询缓存 1.添加Redis缓存 2.缓存更新策略: 3.缓存 ...

  9. 黑马Redis——黑马点评项目记录

    这已经是我第三次看这个项目了,第一次看这个是七八个月以前,第二次看是三个月以前,现在为了简历内容,我打算第三次再回顾一遍这个项目,不得不说这个项目对我学习redis真的是很有帮助. 这文章也不是正经的 ...

最新文章

  1. .Net魔法堂:史上最全的ActiveX开发教程——发布篇
  2. IBM发布未来五年五大科技预测
  3. 信息系统项目管理师-论文写作基本介绍笔记
  4. centos网络隔一段时间就断_“路由器隔一段时间就上不了网,断一下电又能用了,这是什么原因...
  5. 虚拟机搭建DHCP服务器
  6. 如何获取网页付费隐藏链接_如何将SEO变成可衡量的收入来源
  7. history 改成 模式_前端路由三种模式
  8. bash error 环境变量错误
  9. 03-02 capabilities 设置
  10. jsp包含html有乱码,jsp include包含html页面产生的乱码问题
  11. 腾讯电脑管家修复代理服务器,腾讯QQ电脑管家LSP修复在哪
  12. 人工智能训练师开始报名了,可评职称、拿补助
  13. 吞下西甲英超中超成体育大胃王,PPTV还有哪些大招?
  14. 微信公众平台自定义菜单
  15. linux命令——查找文件大小大于1M的文件
  16. 基于Ubuntu 多进程基础知识笔记
  17. 使用Spring Integration实现定时任务
  18. 使用Mono.Cecil辅助ASP.NET MVC使用dynamic类型Model
  19. 跟着 伍逸 老师学GDI+ 之Pen属性
  20. 2012年10月27日沈阳WebLogicFans论坛User Group线下活动

热门文章

  1. vba clear清除公式、内容、格式的使用
  2. 命令提示符窗口中的快捷键及其使用说明
  3. 简单网络嗅探器编写--------java
  4. ESP32-C3 wifi 微信配网+按键清除+LED状态
  5. 华为鸿蒙系统正式开始破冰,鸿蒙系统开始“破冰”行动,安卓措不及防,网友:华为动手了!...
  6. 游戏引擎的动画系统及骨骼动画原理
  7. 安卓开发工具汇总,开发人员必备!
  8. ROS1云课→25机器人控制配置
  9. 三菱FX3G/3U RTU方式通讯四台台达vfd-e变频器示例
  10. hana数据库如何使用if()