当前位置:   article > 正文

秒杀P8-分布式状态管理

分布式状态管理

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

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

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

ttl cache 查看剩余时间

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

代码

导包+配置

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

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

看redis的自动配置类

注解:

  • @ConditionalOnClass(RedisOperations.class)项目有括号里这个类,该配置类就起作用。导包以后自动生成
  • @EnableConfigurationProperties (RedisProperties.class)启用redis资源文件。dev.properities中配置的redis内容,被RedisProperties类读取到。
  • @Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })启动两个客户端的链接配置
 

less

复制代码

@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格式字符串等)默认序列化为了二进制数据,很不方便。

  • 要重写

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

自己重写的configuration/RedisConfiguration

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

注:redis的kv结构,如果v是存哈希,哈希也有kv,都要序列化

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

  • 实现RedisSerializer接口

  • 序列化和反序列化方法

序列化方法:

  1. 传入object对象,如果为空返回null。
  2. 不为空,调用导入的fastJson的JSON对象(fastJson工具包的API),把对象转换成json串。
  3. SerializerFeature.WriteClassName是转换成json时,在json串开头添加最底层的classname类名信息。
 

bash

复制代码

{classname:xxx, id:1,username:zhangsan}

  1. 因为基于扩展性,传入object,实际可能是任何对象,所以用这个记录一下是xx什么类。方便后面反序列化。
  2. 因为返回的是byte字节,转换json的二进制编码

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

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

 

java

复制代码

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方法。

  • 作缓存功能

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

  • opsFor___().set/get/increment

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

redis里面看

redis支持简单的事务

格式固定

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

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

 

csharp

复制代码

@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的操作

(类)反射

反射的API

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

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

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

管理用户状态

用redis代替session

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

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

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

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

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

三个地方都有session,

UserController

  • 注入redis
 

java

复制代码

@Autowired private RedisTemplate redisTemplate;

  • 获取验证码
    • 原本手机号+对应的验证码存到session中,现在存到redis里
    • getOTP(参数:手机号)方法获取验证码,其内部调用generateOTP()方法通过随机数生成验证码,获取到后,redisTemplate.opsForValue().set(phone, otp, 5, TimeUnit.MINUTES),key是手机号,value是验证码,存到redis里
    • 此处作缓存用,存的同时设置过期时间,数字+单位
 

typescript

复制代码

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到手机号真实的验证码
    • 如果输入的验证码为空、真实的验证码为空、输入的与真实的验证码不同,报错
 

less

复制代码

@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存,设置过期时间
 

less

复制代码

@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】
 

less

复制代码

@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对象
 

sql

复制代码

@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的创建订单方法,传入参数
 

less

复制代码

@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,并给客户端返回一个提示信息。

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

 

typescript

复制代码

@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

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

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

2.user

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

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

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

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

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

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

也可以tocken放到请求头里

作者:用户7288159815883
链接:https://juejin.cn/post/7232520169481502781

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小丑西瓜9/article/detail/331502
推荐阅读
相关标签
  

闽ICP备14008679号