{"id":18906172,"url":"https://github.com/haokailau/jg-dianping","last_synced_at":"2026-05-04T13:37:32.156Z","repository":{"id":226193006,"uuid":"767964078","full_name":"HaokaiLau/jg-dianping","owner":"HaokaiLau","description":"Redis的企业应用实战","archived":false,"fork":false,"pushed_at":"2024-03-26T14:18:59.000Z","size":77,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-05-24T12:42:52.382Z","etag":null,"topics":["java","redis","spribg-boot","spring","spring-mvc"],"latest_commit_sha":null,"homepage":"","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/HaokaiLau.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2024-03-06T08:14:46.000Z","updated_at":"2024-03-06T08:23:56.000Z","dependencies_parsed_at":"2025-05-24T12:44:59.789Z","dependency_job_id":null,"html_url":"https://github.com/HaokaiLau/jg-dianping","commit_stats":null,"previous_names":["haokailau/jg-dianping"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/HaokaiLau/jg-dianping","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/HaokaiLau%2Fjg-dianping","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/HaokaiLau%2Fjg-dianping/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/HaokaiLau%2Fjg-dianping/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/HaokaiLau%2Fjg-dianping/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/HaokaiLau","download_url":"https://codeload.github.com/HaokaiLau/jg-dianping/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/HaokaiLau%2Fjg-dianping/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":278698810,"owners_count":26030398,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-06T02:00:05.630Z","response_time":65,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["java","redis","spribg-boot","spring","spring-mvc"],"created_at":"2024-11-08T09:15:16.417Z","updated_at":"2025-10-06T23:56:58.992Z","avatar_url":"https://github.com/HaokaiLau.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Java项目实践——鸡哥点评\r\n\r\n## 1.项目介绍\r\n\r\n### 1.1 项目介绍\r\n\r\n​\t本项目涵盖了Redis的各种数据结构和命令，Redis的各种常见Java客户端的应用和最佳实践。还有Redis在企业中的应用方案，例如共享session、缓存及缓存更新策略、分布式锁、消息队列、秒杀等等场景。另外还有Redis的主从、哨兵、集群等的搭建和原理，使用运维过程中的最佳实践方案。最后还会深入学习Redis底层原理、网络模型、通信模型、内存淘汰策略等内容\r\n\r\n### 1.2 项目内容\r\n\r\n1.共享session登录问题\r\n\r\n- 这部分会使用Redis来处理Session不共享问题。\r\n\r\n2.商户查询缓存\r\n\r\n- 这部分要理解缓存击穿，缓存穿透，缓存雪崩等问题，对于这些概念的理解不仅仅是停留在概念上，更是能在代码中看到对应的内容。\r\n\r\n3.优惠劵秒杀\r\n\r\n- 这部分我们可以学会Redis的计数器功能，结合Lua脚本完成高性能的Redis操作，同时学会Redis分布式锁的原理，包括Redis的三种消息队列。\r\n\r\n- 达人探店\r\n  - 基于List来完成点赞列表的操作，同时基于SortedSet来完成点赞的排行榜功能。\r\n- 好友关注\r\n  - 基于Set集合的关注、取消关注，共同关注等等功能。\r\n- 附近商户\r\n  - 利用Redis的GEOHash结构来完成对于地理坐标的操作。\r\n- 用户签到\r\n  - 使用Redis的BitMap数据实现签到以及统计功能。\r\n- UV统计\r\n  - 主要是使用Redis的HyperLogLog结构来完成统计功能。\r\n\r\n### 1.3 技术选型\r\n\r\n**后端：**\r\n\r\n1. 对Maven工程的结构和特点需要有一定的理解\r\n2. git: 版本控制工具, 在团队协作中, 使用该工具对项目中的代码进行管理。\r\n3. junit：单元测试工具，开发人员功能实现完毕后，需要通过junit对功能进行单元测试。\r\n4. SpringBoot： 快速构建Spring项目, 采用 \"约定优于配置\" 的思想, 简化Spring项目的配置开发。\r\n5. SpringMVC：SpringMVC是spring框架的一个模块，springmvc和spring无需通过中间整合层进行整合，可以无缝集成。\r\n6. MySQL： 关系型数据库, 本项目的核心业务数据都会采用MySQL进行存储。\r\n7. Redis： 基于key-value格式存储的内存数据库, 访问速度快, 经常使用它做缓存。\r\n8. Mybatis： 本项目持久层将会使用Mybatis开发。\r\n9. Mybatis Plus：[MyBatis-Plus (opens new window)](https://github.com/baomidou/mybatis-plus)（简称 MP）是一个 [MyBatis (opens new window)](https://www.mybatis.org/mybatis-3/)的增强工具，在 MyBatis 的基础上只做增强不做改变，为简化开发、提高效率而生。\r\n\r\n**前端：**\r\n\r\n本项目中在构建系统管理后台的前端页面，我们会用到H5、Vue.js等技术。\r\n\r\n## 2.项目重点功能的实现思路以及代码\r\n\r\n### 2.1共享session登录问题\r\n\r\nsession在服务器集群中很难实现session共享的效果，所以我们采取Redis缓存的方法解决这一问题\r\n\r\n#### 2.1.1 完善登录认证功能\r\n\r\n1.发送手机验证码功能\r\n\r\n```java\r\n/**\r\n     * 发送手机验证码\r\n     *\r\n     * @param phone\r\n     * @param session\r\n     * @return\r\n     */\r\n    @Override\r\n    public Result sendCode(String phone, HttpSession session) {\r\n        //1.校验手机号是否合法\r\n        if (RegexUtils.isPhoneInvalid(phone)) {\r\n            //2.如果不符合，返回错误信息\r\n            return Result.fail(\"手机号格式错误!\");\r\n        }\r\n\r\n        //3.如果合法，生成验证码（使用hutool中的工具类生成）\r\n        String code = RandomUtil.randomNumbers(6);\r\n\r\n        //4.保存验证码到session\r\n        //session.setAttribute(\"code\",code);\r\n\r\n        //4.保存验证码到redis\r\n        stringRedisTemplate.opsForValue().set(RedisConstants.LOGIN_CODE_KEY + phone, code, RedisConstants.LOGIN_CODE_TTL, TimeUnit.MINUTES);\r\n\r\n        //5.发送验证码（这里暂时没有调用其他api进行验证码的发送）\r\n        log.debug(\"发送短信验证码成功!验证码为:{}\", code);\r\n\r\n        //6.返回成功信息\r\n        return Result.ok();\r\n    }\r\n```\r\n\r\n2.登录功能\r\n\r\n```java\r\n/**\r\n     * 登录功能\r\n     *\r\n     * @param loginForm\r\n     * @param session\r\n     * @return\r\n     */\r\n    @Override\r\n    public Result login(LoginFormDTO loginForm, HttpSession session) {\r\n        //1.校验手机号是否合法\r\n        String phone = loginForm.getPhone();\r\n        if (RegexUtils.isPhoneInvalid(phone)) {\r\n            //2.如果不符合，返回错误信息\r\n            return Result.fail(\"手机号格式错误!\");\r\n        }\r\n\r\n        //3.校验验证码是否正确\r\n        // 获取redis中保存的验证码和前端发送来的验证码进行比对\r\n        //Object cacheCode = session.getAttribute(\"code\");\r\n        Object cacheCode = stringRedisTemplate.opsForValue().get(RedisConstants.LOGIN_CODE_KEY + phone);\r\n        String code = loginForm.getCode();\r\n        if (cacheCode == null || !cacheCode.toString().equals(code)) {\r\n            //4.验证码过期或者不正确，返回错误信息\r\n            return Result.fail(\"验证码错误!\");\r\n        }\r\n\r\n        //5.根据手机号查询数据库 select * from tb_user where phone = ?\r\n        //使用mp提供的api实现查询功能\r\n        User user = query().eq(\"phone\", phone).one();\r\n\r\n        //6.判断用户是否存在\r\n        if (user == null) {\r\n            //7.不存在，创建新用户，保存到数据库\r\n            user = createWithPhone(phone);\r\n        }\r\n\r\n        //8.保存用户信息到session中\r\n        //session.setAttribute(\"user\", BeanUtil.copyProperties(user, UserDTO.class));\r\n        //注:与jwt令牌不同的是，基于session实现的登录功能无需返回登录凭证\r\n        //8.保存用户信息到redis中\r\n        //8.1 随机生成token，作为登录令牌\r\n        String token = UUID.randomUUID().toString(true);\r\n\r\n        //8.2 将User对象转成Hash存储\r\n        Map\u003cString, Object\u003e userMap = BeanUtil.beanToMap(user, new HashMap\u003c\u003e(),\r\n                CopyOptions.create().setIgnoreNullValue(true)\r\n                        .setFieldValueEditor((fieldName, fieldValue) -\u003e fieldValue.toString()));\r\n\r\n        //8.3 存储\r\n        String tokenKey = RedisConstants.LOGIN_USER_KEY + token;\r\n        stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);\r\n\r\n        //8.4设置token有效期\r\n        stringRedisTemplate.expire(tokenKey, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);\r\n\r\n        //9.返回成功信息\r\n        return Result.ok(token);\r\n    }\r\n\r\n    private User createWithPhone(String phone) {\r\n        //1.创建新用户\r\n        User user = new User();\r\n        //2.为对象属性赋值\r\n        user.setPhone(phone);\r\n        user.setNickName(SystemConstants.USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));\r\n        //3.保存用户\r\n        save(user);\r\n        return user;\r\n    }\r\n```\r\n\r\n4.自定义令牌刷新拦截器\r\n\r\n```java\r\n/**\r\n * @author 喜欢悠然独自在\r\n * @version 1.0\r\n * 令牌刷新拦截器（实现 HandlerInterceptor接口）\r\n * 令用户在请求无需登录的页面时也能刷新令牌保持登录状态\r\n */\r\n@Slf4j\r\n@Component\r\npublic class RefreshTokenInterceptor implements HandlerInterceptor {\r\n\r\n    @Resource\r\n    private StringRedisTemplate stringRedisTemplate;\r\n\r\n    /**\r\n     * 前置拦截\r\n     * @param request\r\n     * @param response\r\n     * @param handler\r\n     * @return\r\n     * @throws Exception\r\n     */\r\n    @Override\r\n    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {\r\n\r\n        /*\r\n        用户未登录/token过期（判断是否有token或者redis中的登录信息是否过期）\r\n         */\r\n\r\n        //1.获取请求头中的token\r\n        String token = request.getHeader(\"authorization\");\r\n        if (StrUtil.isBlank(token)) {\r\n            return true;\r\n        }\r\n\r\n        //2.基于token从redis中获取数据\r\n        String tokenKey = RedisConstants.LOGIN_USER_KEY + token;\r\n        Map\u003cObject, Object\u003e userMap = stringRedisTemplate.opsForHash().entries(tokenKey);\r\n        //3.判断用户是否存在\r\n        if (userMap.isEmpty()) {\r\n            return true;\r\n        }\r\n\r\n        /*\r\n        以下是token未过期才会执行的逻辑（用户是已登录的状态，正在浏览无需登录校验的页面，刷新令牌有效期）\r\n         */\r\n\r\n        //5.把Map转成UserDTO对象\r\n        UserDTO user = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);\r\n\r\n        //6.存在，把用户信息保存到ThreadLocal\r\n        UserHolder.saveUser(user);\r\n\r\n        //7.刷新token有效期\r\n        stringRedisTemplate.expire(tokenKey, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);\r\n\r\n        //8.放行\r\n        return true;\r\n    }\r\n\r\n    /**\r\n     * controller执行后\r\n     * @param request\r\n     * @param response\r\n     * @param handler\r\n     * @param modelAndView\r\n     * @throws Exception\r\n     */\r\n    @Override\r\n    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {\r\n        //销毁ThreadLocal中存储的信息，防止内存泄露\r\n        UserHolder.removeUser();\r\n    }\r\n}\r\n```\r\n\r\n5.自定义登录校验拦截器\r\n\r\n```java\r\n/**\r\n * @author 喜欢悠然独自在\r\n * @version 1.0\r\n * 登录校验拦截器（实现 HandlerInterceptor接口）\r\n */\r\n@Slf4j\r\n@Component\r\npublic class LoginInterceptor implements HandlerInterceptor {\r\n\r\n    /**\r\n     * 前置拦截\r\n     * @param request\r\n     * @param response\r\n     * @param handler\r\n     * @return\r\n     * @throws Exception\r\n     */\r\n    @Override\r\n    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {\r\n        //1.判断是否需要拦截，依据就是ThreadLocal中是否有用户\r\n        if (UserHolder.getUser() == null) {\r\n            //ThreadLocal中没有用户，说明没登录，拦截\r\n            log.info(\"用户未登录!\");\r\n            response.setStatus(401);\r\n            return false;\r\n        }\r\n        //有用户，说明已登录，放行\r\n        return true;\r\n    }\r\n\r\n}\r\n```\r\n\r\n6.定义WebMVC配置类，注册拦截器\r\n\r\n```java\r\n/**\r\n * @author 喜欢悠然独自在\r\n * @version 1.0\r\n * 定义WebMvc配置类\r\n * 注册web层相关组件\r\n * （实现 WebMvcConfiguration接口/继承 WebMvcConfigurationSupport）\r\n */\r\n@Slf4j\r\n@Configuration\r\npublic class WebMvcConfiguration extends WebMvcConfigurationSupport {\r\n\r\n    @Autowired\r\n    private LoginInterceptor loginInterceptor;\r\n\r\n    @Autowired\r\n    private RefreshTokenInterceptor refreshTokenInterceptor;\r\n\r\n    /**\r\n     * 注册登陆校验拦截器\r\n     *\r\n     * @param registry\r\n     */\r\n    @Override\r\n    protected void addInterceptors(InterceptorRegistry registry) {\r\n\r\n        log.info(\"开始注册令牌刷新拦截器...\");\r\n        registry.addInterceptor(refreshTokenInterceptor)\r\n                .addPathPatterns(\"/**\")\r\n                .order(0);\r\n\r\n        log.info(\"开始注册登录校验拦截器...\");\r\n        registry.addInterceptor(loginInterceptor)\r\n                .addPathPatterns(\r\n                        \"/user/**\",\r\n                        \"/blog/**\"\r\n                )\r\n                .excludePathPatterns(\r\n                        \"/user/code\",\r\n                        \"/user/login\",\r\n                        \"/blog/hot\"\r\n                )\r\n                .order(1);\r\n    }\r\n}\r\n```\r\n\r\n实现步骤：\r\n\r\n1. 前端发送获取验证码请求，后端对手机号进行合法性校验后生成验证码，以用户手机号为key，验证码为value存入redis中，并且设置过期时间，把验证码响应给前端。\r\n2. 前端发起登录请求，后端对手机号进行合法性校验后，从redis中基于手机号（key）取出验证码（value），用前端传来的验证码和redis中的验证码进行校验后，根据手机号查询数据库，获取用户数据，判断用户是否为新用户，是则注册（新增用户），然后将用户对象转成Map，使用uuid随机生成一个token，token为key，Map为value，以hash数据类型缓存到redis，并设置过期时间，响应token给前端。\r\n3. 为了使用户有更好的使用体验，减少用户在浏览无需登陆校验页面时token频繁过期的问题，我们使用了两层拦截器，外层拦截器为令牌拦截器，内层拦截器为登录校验拦截器，并且在令牌拦截器中添加了token刷新的逻辑。\r\n4. 令牌拦截器：拦截全路径，当用户未登录浏览无登陆校验页面时，直接放行；如果用户是已登录的状态就基于token从redis中取出用户数据，存入ThreadLocal中，然后刷新token有效期，再放行。\r\n5. 登录校验拦截器：拦截需要登录校验的请求路径，从令牌拦截器中放行的请求，只有两种结果，一种是没有往ThreadLocal中存数据的，一种是存入了用户数据的，所以只需判断ThreadLocal中是否有用户数据即可知道发起该请求的用户是否已经登录。\r\n\r\n### 3.项目总结\r\n\r\n参考视频：[黑马点评](https://www.bilibili.com/video/BV1NV411u7GE?p=1\u0026vd_source=2efbbaa09c22a07508c7c52b78951722)\r\n\r\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhaokailau%2Fjg-dianping","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhaokailau%2Fjg-dianping","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhaokailau%2Fjg-dianping/lists"}