>  기사  >  데이터 베이스  >  Redis+SpringBoot 사례 분석

Redis+SpringBoot 사례 분석

WBOY
WBOY앞으로
2023-06-02 21:09:061348검색

1. 프로젝트 환경

  1. 프런트엔드 기술 스택: Vue-Cli

  2. 프런트엔드 소프트웨어: WebStorm 2020.3

  3. 프런트엔드 스타일: Bootstrap

  4. 백엔드 기술 스택: SpringBoot

  5. 백엔드 소프트웨어: IntelliJ IEDA2019

  6. JavaJDK: 1.8

  7. 서버: Alibaba Cloud Centos 7

  8. 기타: MyBatis, Redis, MySql, Docker, Shiro

2. 프로젝트 시연

ShoppingProject01_Pub : Project05; Bad Person_Axios; 사용자가 이메일을 통해 등록하고 제출을 클릭하면 웹사이트에서 사용자에게 활성화 코드 링크가 포함된 이메일을 보냅니다. 사용자는 링크를 클릭하여 계정 활성화 포털을 실현합니다.
    2) SMS 등록 및 로그인:
  1. 사용자가 자신의 휴대폰 번호로 등록할 때 "인증 코드 받기" 버튼을 클릭하면 웹사이트에서 보낸 인증 코드가 포함된 문자 메시지가 휴대폰으로 전송됩니다. Redis 기반으로 인증코드는 5분 동안 유효하며, 각 휴대폰 번호는 SMS 인증코드 포털을 3번만 받을 수 있습니다.

    3) Alipay 결제:

    Android 버전의 Alipay 샌드박스 앱을 다운로드하면 사용자는 QR 코드를 스캔하여 alipay를 통해 웹사이트에서 상품을 구매할 수 있습니다. 배경 MySql은 주문 행동 포털을 기록하고 웹페이지는 다음과 같이 표시됩니다. 그림 1에 나와 있습니다.
  2. 그림 1 제품 표시 페이지


    4) 사용자 분류:
    사용자가 연간 VIP 멤버십을 구매하기 위해 QR 코드를 스캔하면 웹 사이트의 제품이 절반 가격으로 구매되고 배경 MySql 기록 사용자의 역할이 변경됩니다.
    5) 사용자 포인트 순위 목록:
    상품을 구매하는 사용자는 포인트가 증가합니다. 웹페이지는 그림 2와 같습니다.


    Redis+SpringBoot 사례 분석그림 2 순위 표시 페이지

프로젝트에서 겪은 큰 함정:
1) 이메일 전송 기능의 로컬 테스트를 통과했으며 서버 측 테스트 버그가 자주 나타났습니다.

2) 프로젝트가 서버에 배포된 후에는 서버의 Redis에 연결할 수 없습니다. 해결 방법: (1) Docker 대신 Redis를 서버에 배포합니다. (2) Redis 포트를 7000으로 변경합니다. (3) 활성 방화벽 상태에서 서버 및 Alibaba Cloud의 7000 포트를 해제합니다. conf 파일.
3) git이 로컬 소스 코드를 gitee에 업로드했을 때 잘못된 조작으로 인해 gitee의 이전 코드가 로컬 소스 코드를 덮어쓰게 되었는데, 이는 다음날 발견되었습니다. 해결책: 소스 코드 jar 패키지가 서버에 남아 있기 때문에 디컴파일 도구 jd_gui를 사용하면 수명을 절반으로 줄일 수 있습니다. 또한 git 업로드 파일은 포털을 참조합니다.


Redis+SpringBoot 사례 분석3. 메인 모듈 설명

1
Vue-Cli 모듈 설명:
  1. 1.1 Vue-Cli 개요:

    1) 프론트엔드와 백엔드 분리 및 단일 페이지 웹 애플리케이션이 특징( SPA), Vue-Cli는 스캐폴딩 사양이 있는 Vue 프로젝트를 생성할 수 있습니다. Vue-Cli의 장점은 다음과 같습니다.
    (1) 스캐폴딩 사양을 기반으로 한 개발이 매우 유연해집니다.

    (2) Vue-Cli는 webpack을 기반으로 구축되었으며 합리적인 기본 구성을 제공합니다. 패키징 도구 webpack은 단일 페이지와 다양한 개발 구성 요소를 집계할 수 있습니다.
  2. (3) Vue-Cli는 프런트 엔드 생태계에서 최고의 도구를 물려받은 풍부한 공식 플러그인 컬렉션입니다.

2) 설치 프로세스:

(1) WebStorm 설치(개발용), node.js 설치, vue-cli 설치, axios 설치(도메인 간 요청 시작용) 및 부트스트랩 스타일 도입.

3) 배포 프로세스:

npm run build               # 在WebStorm终端执行,生成dist文件夹
docker pull nginx:1.19.10   # 不建议Vue-cli项目部署到tomcat,因为tomcat属于动态服务器,启动需要java环境,是为了解析动态语言jsp的;像纯静态的就部署到静态服务器nginx上。
mkdir html                  # 为了做docker容器内外的数据卷映射
mv dist/ html/
docker run -p 80:80 --name nginx01 -d -v /root/html/dist/:/usr/share/nginx/html nginx:1.19.10  # 数据卷映射
# 此时可访问  http://120.79.133.235:80/index.html

4) Vue-Cli 개발 포인트:

(1) WebStorm에서는 그림 3과 같이 개발 프로세스가 주로 src 파일을 중심으로 이루어집니다.



그림 3 WebStorm 디렉터리

[1] 먼저 라우팅(라우터) 및 구성 요소(구성 요소[공용 구성 요소], 보기[비공개 구성 요소])를 마스터합니다. 구성 요소가 생성된 후에는 경로에 등록해야 합니다. ] 어설션은 부트스트랩 스타일을 캡슐화하고 main.js에서 가져옵니다. [3] 도메인 간 요청을 보내기 위해 axios 인스턴스는 utils에 캡슐화되며 코드는 다음과 같습니다.

import axios from 'axios'

// 创建默认实例
const instance = axios.create({
  baseURL: 'http://120.79.133.235:8989/eb',
  // timeout: 10000,
});

// 请求拦截器
instance.interceptors.request.use(config=>{
  console.log("请求拦截器");
  return config;
})
// 响应拦截器
instance.interceptors.response.use(response=>{
  console.log("响应拦截器");
  return response;
}, err=>{
  console.log("响应出现错误时进入的拦截器");
});

// 暴露instance实例对象
export default instance;

각 구성 요소에서 get 백엔드에 대한 post 요청 방법은 다음과 같습니다.

// Get请求
// 向后端接口发当前页码,获取当前页面的商品List
instance.get("/item/findAllItem?page="+this.page).then(res=>{
        that.items = res.data.items;
        that.totalPage = res.data.totalPage;
        that.page = res.data.page;
      });

// Post请求
// 向后端接口发送当前商品id和用户id,获取商品购买状态
instance.post("/order/alipay/callback",{itemId:this.itemid,userId:this.user.id}).then(res=>{
        if ( res.data.code == 20000 ) {
          alert("提示:您已购买该商品");
        } else {
          alert("提示:您未购买该商品");
        }
      });
    }

[4] 구성 요소 간 점프 및 값 전달 방법은 다음과 같습니다.

// 跳转到MailReg组件
this.$router.push({name:"MailReg"});

// 跳转到item组件,并传递当前商品的id
this.$router.push({path:"/item",query:{ItemId:myid}});
// item组件接收方法:
this.itemid = this.$route.query.ItemId;

// 另外不同组件可以依据token获取登录用户信息,需要用到redis,详见下文

用户积分排行榜模块说明:
1.1 Reids概述:
1) Redis是一种基于内存的数据存储NoSql;
2) Redis支持丰富的数据类型(String, List, Set, ZSet, Hash);
3) Redis有两种持久化方法: (1)快照(snapshot)存储,也叫rdb持久化,保存当前时刻的数据状态;(2) AOF(append only file)存储,将所有redis的写命令记录到日志文件中,Redis支持持久化间隔最快也是一秒,所以它是事务不安全的,即是可能丢失数据的。
4)Redis的应用场景:
(1) 利用Redis字符串完成项目中手机验证码存储的实现。------本项目采用
(2) 利用Redis字符串类型完成具有时效性的业务功能,如订单还有40分钟即将关闭。
(3) 利用Redis实现分布式集群系统中的Session共享。
(4) 利用Redis的ZSet数据类型(可排序set类型:元素+分数)实现排行榜功能。 ------本项目采用
(5) 利用Redis完成分布式缓存。 ------本项目实现MySql中数据的缓存
(6) 利用Redis存储认证之后的token信息。 ------非常方便,本项目采用。
(7) 利用Redis解决分布式集群系统中分布式锁问题。

1.2 基于Redis实现前端组件从后端获取用户信息
Step1:前端Login.vue组件中用户输入登录信息提交的接口如下:

// 这里调用了后端/user/login接口,获取当前登录用户的token,存入Session的localStorage中,在后续网页浏览过程中可随时调取这个token
instance.post("/user/login",this.user).then(res=>{
        if ( res.data.state ) {
          alert(res.data.msg+",点击确定进入主页");
          // 前端存储token信息
          localStorage.setItem("token",res.data.token);
          that.$router.push({path:"/itemList"});
        } else {
          alert(res.data.msg);
          that.user = {};
        }
      });

Step2:后端/user/login接口实现如下:

// Controller层
@PostMapping("login")
public Map<String, Object> loginAccount(@RequestBody User user, HttpSession session) {
    return userService.loginAccount(user, session);
}

// Service层
// 情况3:查询到一个用户时
// 获取主体对象
try {
     Subject subject = SecurityUtils.getSubject();
     subject.login(new UsernamePasswordToken(user.getName(), user.getPassword()));
     User userDB = userListDB.get(0);
     String token = session.getId();
     if (userDB.getScore() == null) {
           userDB.setScore(0.0);
           userDAO.updateUserScore(userDB);
     }
     redisTemplate.opsForValue().set("TOKEN_" + token, userDB, 30, TimeUnit.MINUTES);
     redisTemplate.opsForZSet().add("userRank", userDB, userDB.getScore());
     map.put("token", token);
     map.put("state",true);
     map.put("msg","登录成功");
     return map;
     ...

Redis整合SpringBoot有两种Template,即RedisTemplate和StringRedisTemplate。其中StringRedisTemplate是RedisTemplate的子类,两个方法基本一致,不同之处在于操作的数据类型不同,RedisTemplate中的两个泛型都是Object,意味着存储的key和value都可以是一个对象,而StringRedisTemplate的两个泛型都是String,意味着StringRedisTemplate的key和value都只能是字符串。
在Step2中,我将token和数据库中的用户信息userDB绑定在一起存入了Redis中,后续前端组件获取登录用户信息的代码如下:

// 从localStorage获取token
let token = localStorage.getItem("token");
let that = this;
// 发送axios请求,根据token获取用户信息
instance.get("/user/token?token="+token).then(res=>{
that.user = res.data;
console.log(that.user);
})

后端/user/token的接口如下:

@GetMapping({"token"})
public User findUser(String token) {
   System.out.println("接收的token信息:" + token);
   return (User)redisTemplate.opsForValue().get("TOKEN_" + token);
}

Step3:用户退出登录时,应消除浏览器中对应的token,后端接口代码如下:

    // 退出登录
    @DeleteMapping({"logout"})
    public Map<String, Object> logout(String token) {
        Map<String, Object> map = new HashMap<>();
        try {
            redisTemplate.delete("TOKEN_" + token);
            Subject subject = SecurityUtils.getSubject();
            subject.logout();
            map.put("state", true);
            map.put("msg", "提示:退出账户成功");
            return map;
        } catch (Exception e) {
            e.printStackTrace();
            map.put("state", false);
            map.put("msg", "提示:退出账户失败");
            return map;
        }
    }

1.3 基于Redis的用户积分排行榜实现
MySql中的用户信息如图4所示:
Redis+SpringBoot 사례 분석

图4 MySql中的用户信息


Redis中的UserRank如图5所示:

Redis+SpringBoot 사례 분석

图5 Redis中的UserRank

Step1:当用户登录时,他的首要任务是接入UserRank对应的信息,后端代码如下:

if (userDB.getScore() == null) {
    userDB.setScore(0.0);
    userDAO.updateUserScore(userDB);
}
    redisTemplate.opsForValue().set("TOKEN_" + token, userDB, 30, TimeUnit.MINUTES);
    redisTemplate.opsForZSet().add("userRank", userDB, userDB.getScore());

userDB是数据库中当前登录用户的信息(一定是有的,你注册了,对吧?),若用户首次登录我将他的分数在数据库设置为0.0,之后我在Redis的ZSet中加入这个用户,你知道,Set集合不会存储重复key值的元素,因此不会同一个用户出现在UserRank中两次。两个template完成了token绑定User,User绑定UserRank中Score的过程,之后的分数更新过程会反复使用这两个template实现。
Step2:当用户信息更新时,相应的与用户信息有关的两个template都要发生变化,代码如下:

// key值序列化
redisTemplate.setKeySerializer(new StringRedisSerializer());
// 由当前用户的token获取当前用户的信息
User firstUser = (User)redisTemplate.opsForValue().get("TOKEN_" + token);
// 删除zSet中的当前用户
 redisTemplate.opsForZSet().remove("userRank", firstUser);
// 产生新的当前用户(昵称改变)
List<User> userListDB = this.userDAO.selectUserByName(user.getName());
User secondUser = userListDB.get(0);
// 更新token中当前用户的信息
redisTemplate.opsForValue().set("TOKEN_" + token, secondUser, 30, TimeUnit.MINUTES);
// 产生zSet中的当前用户
redisTemplate.opsForZSet().add("userRank", secondUser, secondUser.getScore());

Step3:当用户扫码支付时,首次进入的后端controller如下:

// 支付单件商品
    @GetMapping("payForItem")
    public byte[] alipay(String itemid,String userid, String token) {
        this.token = token;
        log.info("itemid=====>"+itemid);
        log.info("userid=====>"+userid);
        PayVo payVo = new PayVo();
        payVo.setItemId(itemid);
        payVo.setUserId(userid);
        System.out.println(payVo.getUserId());
        return alipayService.alipay(payVo);
    }

在alipayService有一个小型用户分级,即vip用户购物价格减半:

            // 1:支付的用户
            String userId = payVo.getUserId();
            // my 1: 依据用户id查询用户
            User user = userService.selectUserById(Integer.valueOf(userId));
            // 依据商品id查询商品
            Item item = itemService.getItemById(payVo.getItemId());
            // my 1: 依据用户id查询用户
            if ( item == null ) return null;
            // 2: 支付金额
            String tempMoney = item.getPrice().toString();
            String money = "";
            if ( user.getRole().equals("normal") ) {
                money = tempMoney;
            }
            if ( user.getRole().equals("vip") ) {
                Double tempMoney2 = Double.valueOf(tempMoney)*0.5;
                money = String.valueOf(tempMoney2);
            }

在payForItem相同文件下,调用了payCommonService,在这里会实现用户积分更新和用户等级更新:

payCommonService.payUserPublic(bodyJsonObject, userId, user.getName(), orderNumber, tradeno, "alipay", this.token);

将"VIP"这件商品的id设置为“666”,当用户购买该商品时,当前用户更新过程如下:

if ( itemId.equals("666") ) {
            int myuserId = Integer.valueOf(userId);
            User userDB = userService.selectUserById(myuserId);
            // key值序列化
            this.redisTemplate.setKeySerializer(new StringRedisSerializer());
            // 由当前token获取当前用户信息
            User firstUser = (User)redisTemplate.opsForValue().get("TOKEN_" + token);
            // 由当前用户信息删除当前用户zSet
            redisTemplate.opsForZSet().remove("userRank", firstUser);
            // 更新当前用户信息身份
            userDB.setRole("vip");
            // 更新当前用户新身份的分数
            userService.updateUserRole(userDB);
            List<User> userListDB = this.userDAO.selectUserByName(userDB.getName());
            // 获取当前新身份用户的完整信息
            User secondUser = userListDB.get(0);
            // 更新当前token对应的当前用户
            redisTemplate.opsForValue().set("TOKEN_" + token, secondUser, 30, TimeUnit.MINUTES);
            // 设置当前用户的zSet
            redisTemplate.opsForZSet().add("userRank", secondUser, secondUser.getScore().doubleValue());
        }

当前用户积分更新过程如下:

        // 更新当前用户的积分
        double tempScore = Double.valueOf(orderDetail.getPrice()) * 0.3;
        String key1 = "TOKEN_" + token;
        // 由当前token获取当前用户
        User user = (User)redisTemplate.opsForValue().get(key1);
        // 更新当前用户的zSet分数
        redisTemplate.opsForZSet().incrementScore("userRank", user, tempScore);
        // 获取当前用户的zSet分数
        double newScore = redisTemplate.opsForZSet().score("userRank", user);
        // 删除当前用户的zSet(因为要更新当前用户的信息,将当前用户在数据库中的分数进行同步)
        redisTemplate.opsForZSet().remove("userRank", new Object[] { user });
        user.setScore(newScore);
        userDAO.updateUserScore(user);
        // 更新token对应的当前用户的信息
        redisTemplate.opsForValue().set(key1, user);
        // 新增当前用户的zSet
        redisTemplate.opsForZSet().add("userRank", user, newScore);

위 내용은 Redis+SpringBoot 사례 분석의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 yisu.com에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제