likes
comments
collection
share

秒杀P8-分布式状态管理

作者站长头像
站长
· 阅读数 11

Redis

单机,不涉及哨兵、集群

  • 服务器安装redis
  • vim /etc/redis.conf
  • 修改配置
  • (70行左右)注释掉bind 127.0.0.1,允许公网ip访问
  • 130左右daemonize yes 允许后台允许
  • 500左右requirepass nowcoder123设置redismm
  • 启动 redis-server /etc/redis.conf
  • redis安装后自带命令行客户端,可以用命令操作
  • redis-cli -a nowcoder123进入redis
  • select 9 进入9号库 默认0,自带16个库,索引是0-15
  • select 0

项目中作缓存用,存字符串

  • set key value

  • get key

  • incr key

  • decr key

  • 自增或者自减,比如自动刷新浏览量

秒杀P8-分布式状态管理

作缓存,set后加自动销毁时间。ex 数字。 ex秒,px毫秒

ttl cache 查看剩余时间

秒杀P8-分布式状态管理

flushdb删库。生产环境下,禁止!

秒杀P8-分布式状态管理

代码

导包+配置

秒杀P8-分布式状态管理

guava本地缓存,后面令牌桶会用

秒杀P8-分布式状态管理

  • 连redis中哪个库(0-15)
  • IP地址?
  • redis的端口
  • redis密码

秒杀P8-分布式状态管理

看redis的自动配置类

注解:

  • @ConditionalOnClass(RedisOperations.class)项目有括号里这个类,该配置类就起作用。导包以后自动生成
  • @EnableConfigurationProperties (RedisProperties.class)启用redis资源文件。dev.properities中配置的redis内容,被RedisProperties类读取到。
  • @Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })启动两个客户端的链接配置
@Configuration (proxyBeanMethods = false)
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties (RedisProperties.class)
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {
  • @ConditionalOnMissingBean (name= "redisTemplate")没有这个bean,实例化下面的bean

  • redisTemplate,外面访问redis的接口/类,需要提前实例化。

  • 实例化时(运行方法),传入redisConnectionFactory(方法的参数)。

  • @Import的内容就是构建Factory的。

  • 因为redisConnectionFactory持有redis的链接,set进redisTemplate,redisTemplate才能调用链接访问redis

  • 实例化的方式太简单,只new了一下,这样在服务器redis中存入的数据(字符串,json格式字符串等)默认序列化为了二进制数据,很不方便。

  • 要重写

秒杀P8-分布式状态管理

自己写一个配置类,实例化好,覆盖掉自带的。

自己重写的configuration/RedisConfiguration

  • @Configuration注解
  • @Bean注解,表示一定会初始化这个方法,把返回的bean对象加载到容器——覆盖原配置类
  • new实例化
  • factory被set进Template
  • 定义key和value序列化的方法
  • key的序列化工具:string字符串
  • 哈希的key序列化工具:string字符串
  • key都调用的是redis序列化器自带的方法
  • 注意value和哈希value的序列化——自定义FastJsonSerializer类
  • 调用template的方法,在对象初始化好之后,其解析一些东西让一些配置生效

秒杀P8-分布式状态管理

注:redis的kv结构,如果v是存哈希,哈希也有kv,都要序列化 秒杀P8-分布式状态管理

common/FastJsonSerializer——自定义的value和哈希value序列化工具

  • 实现RedisSerializer接口

  • 序列化和反序列化方法

序列化方法:

  1. 传入object对象,如果为空返回null。
  2. 不为空,调用导入的fastJson的JSON对象(fastJson工具包的API),把对象转换成json串。
  3. SerializerFeature.WriteClassName是转换成json时,在json串开头添加最底层的classname类名信息。
{classname:xxx, id:1,username:zhangsan}
  1. 因为基于扩展性,传入object,实际可能是任何对象,所以用这个记录一下是xx什么类。方便后面反序列化。
  2. 因为返回的是byte字节,转换json的二进制编码

反序列化: 传入之前序列化的字节,为空返回空 不为空,把字节转换成json字符串 再用fastJson工具解析字符串成对象,得到的是object对象,Feature.SupportAutoType底层是转换成之前存的时候标记的类

注:调用fastjson的API,非自己写

public class FastJsonSerializer implements RedisSerializer<Object> {

    public static final Charset UTF_8 = Charset.forName("UTF-8");

    @Override
    public byte[] serialize(Object obj) throws SerializationException {
        if (obj == null) {
            return null;
        }

        String json = JSON.toJSONString(obj, SerializerFeature.WriteClassName);
        return json.getBytes(UTF_8);
    }

    @Override
    public Object deserialize(byte[] bytes) throws SerializationException {
        if (bytes == null || bytes.length <= 0) {
            return null;
        }

        String json = new String(bytes, UTF_8);
        return JSON.parseObject(json, Object.class, Feature.SupportAutoType);
    }
}

=====================================================以上Redis配置完成

Redis使用

代码解析

  • 哪个bean要用redis,就注入redistemplate。

  • opsFor___()方法,处理对应格式数据

  • string字符串是opForvalue方法。

秒杀P8-分布式状态管理

  • 作缓存功能

  • redis里面一般存很多kv,所以key存的时候要分级、用冒号分割,要结合语义。

  • opsFor___().set/get/increment

秒杀P8-分布式状态管理

  • 持久化对象
  • key,value的value存的是user对象转成的json字符串
  • 获取对象是,get(key),然后强转成user对象

秒杀P8-分布式状态管理

redis里面看

秒杀P8-分布式状态管理

秒杀P8-分布式状态管理

redis支持简单的事务

格式固定

  • 调用redisTemplate.execute方法
  • 方法指定一个接口SessionCallback()
  • 这里直接new实现接口SessionCallback()
  • 接口中实现一个方法execute(),返回boject,
  • execute()方法传入RedisOperations对象,是执行各种命令的父接口。如是前面opsFor__()方法的父接口
  • 先声明key,语义表示要干什么
  • 开启事务
  • 事务过程,用operations对象的数据操作方法
  • 提交事务

:在事务未提交前,中间得不到kv的value值。execute()方法执行完吗,事务提交以后才能得到

秒杀P8-分布式状态管理

@Test
public void testTransactional() {
    Object result = redisTemplate.execute(new SessionCallback() {
        @Override
        public Object execute(RedisOperations operations) throws DataAccessException {
            String redisKey = "test:tx";

            operations.multi();
            operations.opsForSet().add(redisKey, "zhangsan");
            operations.opsForSet().add(redisKey, "lisi");
            operations.opsForSet().add(redisKey, "wangwu");

            System.out.println(operations.opsForSet().members(redisKey));

            return operations.exec();
        }
    });
    System.out.println(result);
}

获取类的类名?object的操作

秒杀P8-分布式状态管理

(类)反射

反射的API

从库里读到字符串,包含类名,怎么返回处理成对象。如下字符串

秒杀P8-分布式状态管理

所有框架底层都用到类反射。

框架的配置,类名等都是字符串,获取字符串,来实例化对应的对象。就是通过类反射。

管理用户状态

用redis代替session

用redis实现全局分布式的session。

session只在controller层使用,一般都在servicecontrollor。

session是基于http协议,跟request请求有关,所有在controller层(前后端沟通的接口)。

项目中的controllor有session的,都要替换成redis

Ctrl+shift+f:在所有路径下搜索

秒杀P8-分布式状态管理

三个地方都有session,

UserController

  • 注入redis
@Autowired
private RedisTemplate redisTemplate;
  • 获取验证码
    • 原本手机号+对应的验证码存到session中,现在存到redis里
    • getOTP(参数:手机号)方法获取验证码,其内部调用generateOTP()方法通过随机数生成验证码,获取到后,redisTemplate.opsForValue().set(phone, otp, 5, TimeUnit.MINUTES),key是手机号,value是验证码,存到redis里
    • 此处作缓存用,存的同时设置过期时间,数字+单位
private String generateOTP() {
        StringBuilder sb = new StringBuilder();
        Random random = new Random();
        for (int i = 0; i < 4; i++) {
            sb.append(random.nextInt(10));
        }
        return sb.toString();
    }

    @RequestMapping(path = "/otp/{phone}", method = RequestMethod.GET)
    @ResponseBody
    public ResponseModel getOTP(@PathVariable("phone") String phone/*, HttpSession session*/) {
        // 生成OTP
        String otp = this.generateOTP();
        // 绑定OTP
//        session.setAttribute(phone, otp);
        redisTemplate.opsForValue().set(phone, otp, 5, TimeUnit.MINUTES);
        // 发送OTP
        logger.info("[牛客网] 尊敬的{}您好, 您的注册验证码是{}, 请注意查收!", phone, otp);

        return new ResponseModel();
    }
  • 注册
    • 之前验证码是放到session中取,现在改成从redis中取
    • register(参数:输入的验证码、用户对象)方法,用redisTemplate.opsForValue().get(user.getPhone());由用户对象get手机号,再从redis中get到手机号真实的验证码
    • 如果输入的验证码为空、真实的验证码为空、输入的与真实的验证码不同,报错
@RequestMapping(path = "/register", method = RequestMethod.POST)
    @ResponseBody
    public ResponseModel register(String otp, User user/*, HttpSession session*/) {
        // 验证OTP
//        String realOTP = (String) session.getAttribute(user.getPhone());
        String realOTP = (String) redisTemplate.opsForValue().get(user.getPhone());
        if (StringUtils.isEmpty(otp)
                || StringUtils.isEmpty(realOTP)
                || !StringUtils.equals(otp, realOTP)) {
            throw new BusinessException(PARAMETER_ERROR, "验证码不正确!");
        }

        // 加密处理
        user.setPassword(Toolbox.md5(user.getPassword()));

        // 注册用户
        userService.register(user);

        return new ResponseModel();
    }
  • 登录+状态管理
    • 用session时,登录以后记录登录状态,把用户和登录状态"loginUser(已登录)"存到session中。验证时在session查询"loginUser“查到了就是登录状态
    • login(参数:电话、密码)方法,判断非空,密码经过md5组件加密,通过调用user =userService.login(phone, md5pwd);把账号密码放到user对象中
    • 换用redis后,声明给客户端返回token(相当于sessionid),身份令牌,是字符串形式
    • token通过UUID生成,String token = UUID.randomUUID().toString().replace("-", "");生成,同时去掉横线
    • 把k=tocken,v=user,存入redis中,user会序列化成json存,设置过期时间
@RequestMapping(path = "/login", method = RequestMethod.POST)
    @ResponseBody
    public ResponseModel login(String phone, String password/*, HttpSession session*/) {
        if (StringUtils.isEmpty(phone)
                || StringUtils.isEmpty(password)) {
            throw new BusinessException(PARAMETER_ERROR, "参数不合法!");
        }

        String md5pwd = Toolbox.md5(password);
        User user = userService.login(phone, md5pwd);
//        session.setAttribute("loginUser", user);
        String token = UUID.randomUUID().toString().replace("-", "");
        redisTemplate.opsForValue().set(token, user, 1, TimeUnit.DAYS);

        return new ResponseModel(token);
    }
  • 登出
    • 原来是传入session,让session过期来登出。现用redis替换
    • logout(参数:token),判断如果客户端传入tocken不为空,直接在redis中删除tocken这个key【底层linux操作: del key】
@RequestMapping(path = "/logout", method = RequestMethod.GET)
    @ResponseBody
    public ResponseModel logout(/*HttpSession session*/String token) {
//        session.invalidate();

        if (StringUtils.isNotEmpty(token)) {
            redisTemplate.delete(token);
        }
        return new ResponseModel();
    }
  • 获取用户状态/信息
    • 原本直接在session里get查询"longinUser",查到了就是已登录
    • getUser(参数:tocken)方法,判断令牌不为空,user = (User) redisTemplate.opsForValue().get(token);,从redis中get到tocken对应的value(对象序列化后的json串),转换成user对象
@RequestMapping(path = "/status", method = RequestMethod.GET)
    @ResponseBody
    public ResponseModel getUser(/*HttpSession session*/String token) {
//  User user = (User) session.getAttribute("loginUser");
        User user = null;
        if (StringUtils.isNotEmpty(token)) {
            user = (User) redisTemplate.opsForValue().get(token);
        }
        return new ResponseModel(user);
    }
  • 登录时存状态,给前端返回tocken,
  • 登录后取状态,传入tocken,给前端返回用户信息

OrderController

  • 原创建订单时,需要存入当前登录的用户的id,所以是在session中查询已登录的user对象,get其id,user = (User) session.getAttribute("loginUser");。用redis后
  • create(参数:itemId商品id、amount购买数量、promotionId活动、token用户令牌)方法,根据tocken在redis中获取userid,user = (User)redisTemplate.opsForValue().get(token);,get到token对应user,后面获取userid
  • 调用orderService的创建订单方法,传入参数
@Controller
@RequestMapping("/order")
@CrossOrigin(origins = "${nowcoder.web.path}", allowedHeaders = "*", allowCredentials = "true")
public class OrderController implements ErrorCode {

    @Autowired
    private OrderService orderService;

    @Autowired
    private RedisTemplate redisTemplate;

    @RequestMapping(path = "/create", method = RequestMethod.POST)
    @ResponseBody
    public ResponseModel create(/*HttpSession session, */
            int itemId, int amount, Integer promotionId, String token) {
//        User user = (User) session.getAttribute("loginUser");
        User user = (User) redisTemplate.opsForValue().get(token);
        orderService.createOrder(user.getId(), itemId, amount, promotionId);
        return new ResponseModel();
    }

}

LoginChecklnterceptor

  • 在指定请求之前拦截,判断是否登录
    • 注意cookie、session、sessionid的关系【回顾一下】
    • session会给客户端(前端)返回一个cookie,里面存的sessionid。cookie浏览器会自动保存到内存或者硬盘上
  • preHandle(参数:request、response、handler)方法,格式固定,返回布尔类型。
  • 通过request参数获取tocken。controller的方法中的参数其实底层也是从request中获得,只不过spring帮我们封装了。
  • 现在handler是做公用的事情,spring不知道要干嘛,所以直接把底层对象给我们,我们自己取参数。
  • 获取请求对象中的tocken参数 request.getParameter("token")
  • 判断token是否为空、redis中叫“token”的key是否存在redisTemplate.hasKey(token)【底层调用的Linux命令:exists key。1存在0不存在】。
  • 都空,说明没有登陆过。返回false,并给客户端返回一个提示信息。

返回的提示信息,代码可以细看一下

@Component
public class LoginCheckInterceptor implements HandlerInterceptor, ErrorCode {
    @Autowired
    private RedisTemplate redisTemplate;
    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response, Object handler) throws Exception {
//        HttpSession session = request.getSession();
//        User user = (User) session.getAttribute("loginUser");
        String token = request.getParameter("token");
        if (token == null || !redisTemplate.hasKey(token)) {
            response.setContentType("application/json");
            response.setCharacterEncoding("utf-8");
            PrintWriter writer = response.getWriter();
            Map<Object, Object> data = new HashMap<>();
            data.put("code", USER_NOT_LOGIN);
            data.put("message", "请先登录!");
            ResponseModel model = new ResponseModel(ResponseModel.STATUS_FAILURE, data);
            writer.write(JSONObject.toJSONString(model));
            return false;
        }
        return true;
    }

}

controller的方法想要什么参数,就声明什么。如果前端传入一样的参数,就会通过反射机制解析、赋值进来

前端代码

1.登录login

  • 原cookie,前端自动处理。
  • 现在换成token,后端Usercontroller返回的responsemodel的tocken,前端存到sessionstorage中。
  • responsemodel有2个属性:state,value

秒杀P8-分布式状态管理

  • storage安全性比cookie好,可看成本机的kv数据库
  • localstorage一直存,不会删
  • sessionstorage浏览器关闭就删除

秒杀P8-分布式状态管理

window.sessionStorage.setItem("token", result.data);存储token

秒杀P8-分布式状态管理

2.user

换token后,客户端用ajax返回服务器信息,没法自动携带token,所以我们要拼到url上去

url: SERVER_PATH + "/user/status?token=" + window.sessionStorage.getItem("token")

秒杀P8-分布式状态管理

所有需要客户端携带token的地方,都要这样处理

如订单controller,创建订单需要传入token。前端item.js同样拼上去

秒杀P8-分布式状态管理

如果请求很多,jquery有监听器拦截,可以拦截强制拼一个上去。

token存在本地sessionstorage上,地址栏不显示。

也可以tocken放到请求头里

转载自:https://juejin.cn/post/7232520169481502781
评论
请登录