文章目录
- [1. session管理](#1. session管理)
-
- [1.1 手机号登录流程](#1.1 手机号登录流程)
- [1.2 session的数据结构设计](#1.2 session的数据结构设计)
-
- [1.2.1 一些概念](#1.2.1 一些概念)
- [1.2.2 session数据结构例子](#1.2.2 session数据结构例子)
- [1.3 引入redis来实现共享session](#1.3 引入redis来实现共享session)
-
- [1.3.1 流程](#1.3.1 流程)
- [1.3.2 要考虑的问题](#1.3.2 要考虑的问题)
- [2. 限时抢购代金券](#2. 限时抢购代金券)
-
- [2.1 业务流程](#2.1 业务流程)
- [2.2 代金券表设计](#2.2 代金券表设计)
- [2.3 高并发场景面临的问题](#2.3 高并发场景面临的问题)
-
- [2.3.1 问题](#2.3.1 问题)
- [2.3.2 工具](#2.3.2 工具)
- [2.3.3 解决方案](#2.3.3 解决方案)
1. session管理
1.1 手机号登录流程
-
发送短信验证码流程:输入手机号 --> 验证手机号合法(否则回到"输入手机号") --> 生成验证码 --> 保存验证码到session --> 发送验证码短信。
-
验证码登录流程:输入手机号、验证码 --> 校验验证码(session中的有没有用户提交的验证码) --> 校验通过,以手机号找用户 --> 用户存在,下一步;用户不存在,则创建用户,再下一步 --> 保存用户信息到session。
-
登录检查流程:请求携带cookie ,从cookie中拿session id --> 根据session id拿session对象,判断用户是否在session中(用户登录过则在seesion中有记录) --> 用户不存在终止请求响应,或者重定向到登录页;用户存在,继续响应,注意要缓存用户信息,方便后续使用。
如果采用单体服务-单线程-多协程的方式来处理请求,那么session是线程的数据,协程共享它。如果考虑负载均衡而使用多个server实例,那么session要在这些服务实例之间共享,此时只能使用redis(在内存,kv,server实例之间共享)。即session是多个server实例之间共享的状态。
1.2 session的数据结构设计
1.2.1 一些概念
什么是web应用的状态:客户端与服务器在多次交互过程中产生的上下文信息,例如用户登录凭证、购物车内容、操作历史等。
为什么需要状态:判断请求之间的关联性。如判断两次请求是否来自同一个用户,判断两次请求是否来自同一客户端。
session的作用:HTTP是无状态协议,因此需要使用session(服务端维护)跟踪用户状态,常见的状态化机制还有Cookie(服务端分发,客户端携带),Token(服务端分发,客户端携带)。
1.2.2 session数据结构例子
python
session_obj1 = {
'user_id': 'abc',
'phone': '13112341234',
'expire_time': 1745738823
}
session_obj2 = {
'user_id': 'ABC',
'phone': '13112341234',
'expire_time': 1745738823
}
session = {
'001': session_obj1,
'002': session_obj2
}
1.3 引入redis来实现共享session
redis中保存两类数据:每个手机号的申请的验证码;登录成功的用户信息
1.3.1 流程
发送验证码短信的流程
输入手机号 --> 验证手机号合法(否则回到"输入手机号") --> 生成验证码 --> 保存验证码到redis,注意加过期时间 ( eg, key=phone:13212341234, val=31245 ) --> 发送验证码短信。
验证码登录流程
输入手机号、验证码 --> 校验验证码(redis中,verify_code = GET phone:13212341234, if verify_code == in_verify_code) --> 校验通过,以手机号找用户 --> 用户存在,下一步;用户不存在,则创建用户,再下一步 --> 保存用户信息到redis( 方式一:用户信息序列化为string形式保存,方式二:直接使用hash,key的格式:token:uuid )。
登录检查流程
请求携带cookie ,从cookie中拿token --> 根据token从redis拿用户信息 --> 拿到用户信息,继续响应,注意要缓存用户信息,方便后续使用同时刷新token过期时间,保证只要用户有请求,登录状态就一直保持;没拿到用户信息,终止请求响应,或者重定向到登录页。
1.3.2 要考虑的问题
问题:如果用户登录后,相当长的一段时间访问那些不需要登录认证的页面,则token一直不会被刷新而自动掉线,这是不合理的。
解决:所有路径都执行刷新token的动作(根据请求cookie中的token访问redis,获取用户信息,用线程变量缓存用户信息,刷新token过期时间),在需要登录认证的路径做额外的登录状态检查工作(检查缓存中是否有用户信息,如果没有则终止继续响应或者重定向到登录页;如果有则继续响应)。
2. 限时抢购代金券
2.1 业务流程
基本流程:查询代金券券(by id) --> 检查时效 --> 检查库存 --> 扣减库存 --> 创建代金券订单并返回订单id
一人只能购买一个单限制下的流程:查询代金券券(by id) --> 检查时效,在时效内继续 --> 检查库存,库存充足继续 --> 根据用户id和代金券di检查代金券订单,订单不存在继续 --> 扣减库存 --> 创建代金券订单并返回订单id
2.2 代金券表设计
对象:普通代金券,限时抢购代金券,代金券订单
表设计:
voucher(voucher_id, shop_id, title, sub_title, rules, pay_value, actual_value, type, status, create_time, update_time)
time_lmt_voucher(voucher_id, stock, start_time, end_time, create_time, update_time)
voucher_order(id, user_id, voucher_id, pay_type, status, pay_time, use_time, refund_time, create_time, update_time)
说明:其中,voucher和time_lmt_voucher是一对一关系,time_lmt_voucher是voucher的扩展。
2.3 高并发场景面临的问题
2.3.1 问题
- 超卖(库存为负数):扣减库存逻包含两个操作:读库存并判断库存是否充足,假设初始情况下库存为1(记为op1);更新库存为0(记为op2)。线程1执行op1后线程2也执行op1,然后线程1执行op2,库存为0,线程2再执行op2,库存为-1。
- 一人一单限制失效:业务逻辑包含两个操作:根据用户id和代金券id查询代金券订单是否存在,假设初始情况下用户没有代金券订单(记为op1);创建新订单(记为op2)。线程1执行op1后,线程2也执行op1,然后线程1执行op2,创建一个订单,线程2再执行op2,又创建了一个订单。
- 使用数据库的自增id作为订单id存在风险(如根据订单id差值推测出订单量等),且订单数量可能受限。
2.3.2 工具
乐观锁:线程不安全不一定发生,不加锁,只有在更新数据时才检查数据是否被已经被修改。使用版本号控制,先访问数据得到版本好,更新时比较版本号,版本号一致则更新数据同时更新版本号,版本号不一致则拒绝更新。
悲观锁:线程不安全一定发生,在访问数据前加锁。
2.3.3 解决方案
- 超卖(库存为负数):使用CAS(Compare And Set)原则,限制库存更新操作条件,用op1查询的库存结果来限制op2的更新操作,set time_lmt_voucher into stock=stock-1 where stock=1;
- 一人一单限制失效:使用互斥锁构建临界区,把op1和op2放进去。单体服务用进程锁,线程锁,协程锁。分布式服务使用分布式锁。
- 使用redis做一个全局id生成器(唯一性,高可用,高性能,递增,安全),id(64bits): 时间戳(32bist) + 序列号(32bits)。