秒杀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 })启动两个客户端的链接配置
@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接口
-
序列化和反序列化方法
序列化方法:
- 传入object对象,如果为空返回null。
- 不为空,调用导入的fastJson的JSON对象(fastJson工具包的API),把对象转换成json串。
- 加
SerializerFeature.WriteClassName
是转换成json时,在json串开头添加最底层的classname类名信息。
{classname:xxx, id:1,username:zhangsan}
- 因为基于扩展性,传入object,实际可能是任何对象,所以用这个记录一下是xx什么类。方便后面反序列化。
- 因为返回的是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方法。
-
作缓存功能
-
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()方法执行完吗,事务提交以后才能得到
@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
@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
- 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放到请求头里
转载自:https://juejin.cn/post/7232520169481502781