实现登录校验
单台服务器版
使用SpringMVC拦截器Interceptor+Session+ThreadLocal实现,具体如下
- 新建一个工具类
UserHolder
,用于封装ThreadLocal的方法。
public class UserHolder {
private static final ThreadLocal<UserDTO> tl = new ThreadLocal<>();
public static void saveUser(UserDTO user){
tl.set(user);
}
public static UserDTO getUser(){
return tl.get();
}
public static void removeUser(){
tl.remove();
}
}
- 新建一个
UserLoginInterceptor
拦截器实现HandlerInterceptor
接口,在preHandle()
中判断是否拦截当前用户。
/**
* 拦截器,拦截用户未登录的部分请求
*/
@Component
public class UserLoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//1. 从session获取当前用户
HttpSession session = request.getSession();
Object user = session.getAttribute("user");
//2. 当前用户是否存在
if (user == null) {
//3. 不存在,拦截
response.setStatus(401);
return false;
}
//4. 存在,保存到ThreadLocal并放行
UserHolder.saveUser((UserDTO) user);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//释放ThreadLocal,防止内存泄漏
UserHolder.removeUser();
}
}
- 将自定义的拦截器,注册到SpringMvc中;新建一个MvcConfig,实现
WebMvcConfigurer
,并重写addInterceptors()
方法。
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Autowired
private UserLoginInterceptor userLoginInterceptor;
/**
* 用于Controller层的登录权限控制
*
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
//采用排除不需要拦截的请求地址(也可以用addPathPatterns()表示哪些路径需要拦截)
registry.addInterceptor(userLoginInterceptor).excludePathPatterns(
"/shop/**",
"/voucher/**",
"/shop-type/**",
"/upload/**",
"/blog/hot",
"/user/code",
"/user/login"
);
}
}
服务器集群版
注意:如果使用集群部署项目,会出现session不共享的问题(同一个用户发送多次请求,被多台集群接受,这时不是每台服务器都有用户session)。
解决方法:使用Redis缓存Session,Session有的特性Redis都有,Redis还能共享数据。
- 共享数据
- 内存存储
- key-value形式
Redis使用Hash存储对象的优点:节省内存(相比与String存储对象而言)、可以对单个字段(key)进行修改,更加灵活
UserLoginInterceptor
/**
* 拦截器,拦截用户未登录的部分请求
*/
@Component
public class UserLoginInterceptor implements HandlerInterceptor {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//1. 从请求头获取token
String token = request.getHeader("authorization");
if (StrUtil.isBlank(token)) {
// 不存在,拦截
response.setStatus(401);
return false;
}
//2. 使用token从Redis获取用户
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(LOGIN_USER_KEY + token);
//3. 将用户(Map)--> 用户(UserDTO),isIgnoreError:转换出现错误是否忽略
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
//4. 当前用户是否存在
if (userDTO == null) {
// 不存在,拦截
response.setStatus(401);
return false;
}
//5. 存在,将用户存在ThreadLocal
UserHolder.saveUser(userDTO);
//6. 刷新token有效期
stringRedisTemplate.expire(LOGIN_USER_KEY + token, LOGIN_USER_TTL, TimeUnit.MILLISECONDS);
//7. 放行
return true;
}
Redis缓存商户信息
(一)缓存穿透
(二)缓存雪崩
(三)缓存击穿
(四)总结
使用Redis解决秒杀问题
ShopService:缓存商家数据时,缓存穿透、击穿的问题
ShopTypeService:Redis缓存首页店铺分类功能
UserService:使用Redis缓存短信验证码和用户登录成功的token
VoucherOrderService:使用Redis解决秒杀优惠券下单(超卖问题和一人一单)
未实现秒杀前:
//1. 查询优惠券信息
SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
//2. 判断秒杀是否开始
LocalDateTime now = LocalDateTime.now();
LocalDateTime beginTime = seckillVoucher.getBeginTime();
LocalDateTime endTime = seckillVoucher.getEndTime();
if (beginTime.isAfter(now)) {
//3. 否,返回异常结果
return Result.fail("抢购时间未到!");
}
if (endTime.isBefore(now)) {
return Result.fail("抢购时间已结束!");
}
//4. 是,判断库存是否充足
if (seckillVoucher.getStock() < 1) {
//5. 否,返回异常结果
return Result.fail("抢购完了,下次再来吧");
}
//6. 是,扣减库存
// seckillVoucher.setStock(seckillVoucher.getStock() - 1);
// seckillVoucherService.updateById(seckillVoucher);
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1")
.eq("voucher_id", voucherId).update();
if (!success) {
return Result.fail("库存不足!");
}
//7. 创建订单
VoucherOrder voucherOrder = new VoucherOrder();
//7.1 订单id
long orderId = redisWorker.nextId("order");
voucherOrder.setId(orderId);
//7.2 用户id
Long userId = UserHolder.getUser().getId();
voucherOrder.setUserId(userId);
//7.3 优惠券id
voucherOrder.setVoucherId(voucherId);
//8. 返回订单id
return Result.ok(orderId);
以上是未实现秒杀,有诸多问题(Jmeter):
(一)超卖问题(乐观锁与悲观锁)
多个线程同时查询了数据库(查到都是1),查完后又都去修改了数据库,这时就导致了库存为负的问题;
这其实和基础学的多线程一样,是因为在多线程环境下操作了共享数据,所以出现了线程安全问题;
悲观锁的解决方法就是在修改数据库时上锁(如果获取锁失败就重新查询);
乐观锁实现方式1:
乐观锁实现方式2:
CAS(Compare-and-Swap)先比较再替换:就是在上图的基础上,将库存也就是stock去代替version的功能,让被修改的字段本身作为一个判断机制(eg:set stock =stock -1,where id = 10 and stock = 1);
对于这里的秒杀优惠券来说,set stock =stock -1,where id = 10 and stock > 0,更合适,因为只要库存大于0,就可以修改数据库,只要<=0时不能修改数据库就行。
(二)一人一单
问题一:
问题二:
在内部上锁之后,先解锁,解锁的同时其他线程进来的,但是该方法还没执行完毕,也就还没提交事务;导致的情况就是,锁释放了,事务还没提交(也就还没同步数据到数据库中),同样会出现线程安全问题。
问题三:
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
事务问题,
视频讲解
实现秒杀后:
/**
* 秒杀优惠券下单
*
* @param voucherId
* @return
*/
@Override
public Result seckillVoucher(Long voucherId) {
//1. 查询优惠券信息
SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
//2. 判断秒杀是否开始
LocalDateTime now = LocalDateTime.now();
LocalDateTime beginTime = seckillVoucher.getBeginTime();
LocalDateTime endTime = seckillVoucher.getEndTime();
if (beginTime.isAfter(now)) {
//3. 否,返回异常结果
return Result.fail("抢购时间未到!");
}
if (endTime.isBefore(now)) {
return Result.fail("抢购时间已结束!");
}
//4. 是,判断库存是否充足
if (seckillVoucher.getStock() < 1) {
//5. 否,返回异常结果
return Result.fail("抢购完了,下次再来吧");
}
Long userId = UserHolder.getUser().getId();
//上锁,用户id当锁,这样就实现一个用户一把锁
synchronized (userId.toString().intern()) {
//获取代理对象(事务)
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
}
}
/**
* 封装用户下单时的操作
* @param voucherId
* @return
*/
public Result createVoucherOrder(Long voucherId) {
Long userId = UserHolder.getUser().getId();
//根据优惠券和用于id查询订单
int count = this.query().eq("user_id", userId).eq("voucher_id", voucherId).count();
if (count > 0) {
//如果用户已经下过单
return Result.fail("同一用户只能下一单!");
}
//6. 是,扣减库存
// seckillVoucher.setStock(seckillVoucher.getStock() - 1);
// seckillVoucherService.updateById(seckillVoucher);
//采用乐观锁,解决超卖问题
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1")
.eq("voucher_id", voucherId).gt("stock", 0)
.update();
if (!success) {
return Result.fail("库存不足!");
}
//7. 创建订单
VoucherOrder voucherOrder = new VoucherOrder();
//7.1 订单id
long orderId = redisWorker.nextId("order");
voucherOrder.setId(orderId);
//7.2 用户id
voucherOrder.setUserId(userId);
//7.3 优惠券id
voucherOrder.setVoucherId(voucherId);
//8. 返回订单id
return Result.ok(orderId);
}
(三)集群或分布式问题(分布式锁)
问题:将项目部署到集群或分布式,每一个集群都启动秒杀项目,这时有多个JVM的存在,每个JVM都有自己的锁,这就导致了每一个锁都有一个线程可以去操作数据库,导致了线程安全问题(集群中的某台机器对应了一个JVM)。
解决思路:让多个JVM只能使用同一把锁,这就需要用到分布式锁;
(1)基于Redis的分布式锁
V1.0
V2.0
如果线程1获取锁之后的业务阻塞了,锁超时后被释放,释放之后线程业务又醒了并继续执行业务,然后去把锁删除了,但是他本身是没有锁的,删的是线程2的锁,这时就又会出现线程安全问题。
V3.0
解决思路,要让判断锁和释放锁成为一个原子操作(Lua脚本)。
SimpleRedisLock静态代码块加载lua脚本:
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
static {
UNLOCK_SCRIPT = new DefaultRedisScript<>();
UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
UNLOCK_SCRIPT.setResultType(Long.class);
}
SimpleRedisLock重写unlock方法
@Override
public void unLock() {
//调用Lua脚本,解决线程安全问题
stringRedisTemplate.execute(
UNLOCK_SCRIPT,
Collections.singletonList(KEY_PREFIX + name),
ID_PREFIX + Thread.currentThread().getId()
);
}
Resouce下新建Lua脚本:
---
--- Generated by EmmyLua(https://github.com/EmmyLua)
--- Created by 我和寂世.
--- DateTime: 2023/3/3 17:53
---
if (redis.call('get', KEYS[1]) == ARGV[1]) then
-- 释放锁
return redis.call('del', KEYS[1])
end
return 0
(四)Redisson(工具解决分布式锁问题)
(1)快速入门