MybatisPlus session技术会把jsessionid自动写到cookie里
ThreadLocal保证线程安全 用springmvc的自定义拦截器把登出用户 查看详情 获取当前用户并且返回 上传操作 登录查看详情
以下是不拦截的路径
在tomcat负载均衡时 如果不使用redis 直接用相互拷贝 1 浪费空间 2 如果此时前端再次请求就会导致数据不一致 3 相互直接copy会有延迟 影响用户体验 使用redis可以高效解决问题 1 速度快 2 避免数据不一致 保证操作原子性 3 集中存储 :Redis将数据集中存储在一个或多个节点上 节省存储空间 4 数据共享:所有Tomcat实例共享Redis中的数据,无需担心数据冗余问题。
基于Redis实现共享session登录 redis用hash存储数据
验证码有效期 用opsForValue存储key 用户手机号和value 验证码 设置有效时间
需要手动构建redisTemplate对象 用构造函数注入
不能加Compotent,拦截器是一个非常轻量级的组件,只有在需要时才会被调用,并且不需要像控制器或服务一样在整个应用程序中可用。因此,将拦截器声明为一个Spring Bean可能会引导致性能下降。
要把map里的 long类型的id变为string类型 不然会报错
转化下map值类型 userMap.forEach((key,value)->{if(null!=value) userMap.put(key, String.valueOf(value)); });
采用两个拦截器 保证未登录状态下也能实现部分功能
缓存 缓存 缓存
ttc?
缓存穿透
商铺信息不存在时 返回404响应码变成把空值插入redis 同时在判断缓存命中时需要判断命中是否为空值 是则结束 不是空则返回商铺信息
缓存null值 简单迅速 但是产生额外空间 布隆过滤则是通过概率性来判断当前数据是否存在 通过把数据库里的数据计算为哈希值 再解码为字节码进行判断
缓存雪崩
缓存击穿
第一个是过期了,redis数据没了,必须查询数据库重建缓存数据,再设置ttl。第二个是逻辑过期,redis数据还能命中旧数据,单拎一个线程重建,没啥影响。
p46最好自己敲 如何把逻辑过期和互斥锁封装成工具类 这几集都懵了 确实知识点太多 泛型和构造化函数欠缺
我在juc方面还是欠缺
优惠券秒杀
优惠券和普通券表
出现超卖!!!!! 并发安全问题
多个线程操作共享资源
版本号法
Compare And Switch CAS法
乐观锁方案成功率太低 库存这里的条件可以改为是否大于0 只要大于0当前线程就可以执行减操作
但是对于常规方案 需要保证数据安全的情况下 可以分段加锁 可以把资源分多表
concurrent hashmap 用到过 分段锁 再优化!
虽然做了一人一单 但是还是一个用户买了十单 为什么? 代码逻辑是先判断 后执行
多线程操作下 多次判断得到值都是100 相当于同时进行10次判断 后执行 之后第二次判断当前userId已有秒杀券
解决这种问题 需要悲观锁 synchronized 不能放在方法上 这样就会让多线程串行 效率过低
需要用代码块把逻辑包起来 同时把userId的转成字符串 而不是new一个字符串对象
这是解决方案 userId.toString().intern()
intern()方法,如果创建的字符串对象在常量池中有,那么就不会在堆中创建新的String对象
加在方法上锁住的是所有对象;同步代码块里面传对象,锁的是同一个对象
但是这样的方法 一旦锁释放 但是事务未提交 数据库可能还未被写入数据 再次查询时当前userId依旧没有秒杀券 根据判断又可以进下一个线程 所以这个时候可能出现并发安全问题
于是 可以把锁直接套在方法外 内部事务提交后再释放锁
可以用Resource注入自身,这样就有代理对象了,然后用注入的对象去调用这个方法
在事务传播时 Spring会对当前类进行动态代理来实现事务Transactional
获取当前类动态代理对象后 再调用方法
集群模式下的并发安全问题
在集群模式(分布式系统下)下 多个jvm会有多个锁监视器 每个jvm都有自己的锁 导致每个锁都可以有一个线程获取 于是就出现并行运行 引出 跨jvm 跨进程锁
分布式锁
分布式锁
分布式锁就是多个jvm的共同锁监视器 在某个线程得到所后 其他线程一直等待当前线程完成操作并释放锁后再执行业务操作
mysql支持主从模式 redis支持主从和集群模式
zookeeper 严格遵循主从 高一致性 在查询上会耗费时间
阻塞等待耗费cpu性能 非阻塞只尝试一次
Boolean和boolean 此时会自动拆箱 一旦Boolean对象为null boolean会报空指针异常
实现简单分布式锁
极端情况 超时时间小于业务完成时间 当线程1未完成业务时超时已经释放锁 线程2不再业务阻塞直接得到锁 但是在线程2占用锁时线程1完成业务 释放锁 导致线程3可以继续获取锁 一直循环
所以 需要在线程1完成前 其他线程需要判断线程1的锁是不是当前本线程的锁
判断锁的标识是否一致
代码实现
判断线程标识防止锁误删
可能因为jvm本身会产生阻塞 Garbage Collection gc 垃圾回收机制
因为已经判断过标识了,线程1正准备手动释放锁的时候阻塞了,线程2就拿到了锁,锁标识也变成线程2的了,这时候线程1阻塞结束后,因为已经判断过了锁标识,线程1误以为琐是自己的,就释放了2的锁
判断锁和释放锁是两个操作 可能会不一致 怎么才能把两个操作合在一起具有原子性
批处理 一次性 可以用redis乐观锁进行判断 确保释放时无线程修改
Redis事务拿不到中间结果,这有点坑。所以才有了Lua
因为事务里面只能执行redis指令 而redis的指令没法直接判断value 然后通过value结果删除指令
redis的lua脚本 基于c语言
简化版
提前读取文件
调用lua脚本 在脚本中运行满足原子性
可重入锁 在同一线程上 一个方法内线获取锁 再调用方法2 方法2内获取锁 这时需要用hash的value+1 代表重入一次 可以实现重入锁 但是也会引出线程安全问题 所以不能在方法2释放锁 需要在调用完方法2后对value-1 代表出锁
完整逻辑是 先判断锁是否存在 如果存在就获取锁并添加线程标识 同时设置锁有效期 再执行业务 判断锁是否是本身的锁 如果是 就对锁计数-1 此时再判断当前锁计数是否为0 不是则返回其调用者 是0则释放锁
在判断锁是否存在这个逻辑上 如果当前锁已经存在 判断当前锁是不是自己的锁 不是则获取锁失败 是则把锁的计数加一 同样设置有效期 再次执行以上操作
要用lua脚本保证操作前后一致性(原子性)
获取锁脚本
释放锁脚本
重试机制 消息订阅和信号量的机制 并非无休止的等待重试策略 这里采用等锁释放后再尝试 对cpu不再是一种浪费
有ttl和time(ttl 锁有效时间 time获取锁的等待时间) 需要确保业务正常运行释放锁 不能因为阻塞释放锁
引出看门狗机制
这里的ee对象封装了当前线程id与当前定时的任务(该定时任务里用lua脚本对redis有效期定时更新 在看门狗机制30s的情况下 每30/3 = 10秒后会重启任务 通过内部递归重复调用方法执行)
重置锁有效期脚本
释放锁逻辑源码
主从机制
哨兵会让从节点变为主节点 但是原锁就会失效 可以在新主节点进新锁 会引发线程安全问题
只要有一个节点有锁存活 则其他redis都不能获取新锁 multi lock 连锁
Redis优化秒杀功能
加上分布式锁 以及从数据库中直接进行写操作
用set 唯一性且多值 把优惠券id 用户id 订单id存入阻塞队列 用另外线程完成减少优惠券的操作
加入阻塞队列 实现异步下单
这里线程池只给了单线程
用spring注解PostConstruct 完成类加载后先执行该方法
ordertasks.take 当前有userId才会往下运行
子线程拿不到ThreadLocal里的数据 因为这个操作是其子线程执行
将其作为成员变量
使用的阻塞队列是jdk里自带的阻塞队列 使用的是jvm的内存 当订单量过大时 同时不限制内存大小可能会导致内存溢出 2 如果redis突然宕机 阻塞队列里的内存全部丢失 导致前后端数据不一致
Redis消息队列实现异步秒杀
Redis消息队列实现异步秒杀
Redis消息队列实现异步秒杀
Redis消息队列实现异步秒杀
Redis消息队列实现异步秒杀
Redis消息队列实现异步秒杀
Redis消息队列实现异步秒杀
替代jdk阻塞队列 独立于jvm 也保证数据安全 存进数据需要做持久化
生产环境中如果对消息的可靠性有十分高的要求(比如订单支付的消费消息),请使用专业的消息队列(例如:rmq,amq等),对消息的丢失有一定容忍度的程序完全可以使用redis,例如我们的日志收集程序
mq 可以在服务挂机重启后继续执行未完成的服务
Rabbitmq Rocketmq Kafka
这里可以 用 mq优化
消息队列是什么?
消息队列(Message Queue)是一种用于在应用程序之间传递消息的通信机制。它允许不同的系统或组件通过发送和接收消息来解耦,从而实现异步通信。消息队列通常由以下核心组件组成:
-
生产者(Producer):负责生成并发送消息到队列。
-
队列(Queue):存储消息的缓冲区,消息按照先进先出(FIFO)的顺序处理。
-
消费者(Consumer):从队列中获取消息并进行处理。
常见的消息队列系统包括 RabbitMQ、Kafka、RocketMQ、ActiveMQ 等。
消息队列能实现什么功能?
-
解耦:
-
生产者和消费者不需要直接通信,通过消息队列解耦,降低系统间的依赖性。
-
-
异步处理:
-
生产者发送消息后无需等待消费者处理,可以继续执行其他任务,提高系统的响应速度。
-
-
流量削峰:
-
在高并发场景下,消息队列可以缓冲大量请求,避免系统过载。
-
-
可靠性:
-
消息队列通常支持持久化,确保消息不会丢失,即使系统崩溃也能恢复。
-
-
扩展性:
-
可以通过增加消费者来横向扩展系统的处理能力。
-
-
顺序保证:
-
某些消息队列(如 Kafka)可以保证消息的顺序性。
-
如何使用消息队列优化阻塞队列实现异步下单秒杀功能?
在秒杀场景中,高并发请求可能导致系统崩溃或响应缓慢。使用消息队列可以优化这一过程,具体实现步骤如下:
1. 传统阻塞队列的问题
-
在高并发场景下,阻塞队列可能会导致线程阻塞,系统资源耗尽。
-
如果队列满了,新的请求会被拒绝,影响用户体验。
2. 使用消息队列优化秒杀功能
架构设计
-
生产者:
-
用户发起秒杀请求后,请求被发送到消息队列中。
-
生产者不需要等待下单结果,直接返回“请求已接收”的响应。
-
-
消息队列:
-
消息队列作为缓冲区,接收并存储所有的秒杀请求。
-
队列可以根据系统处理能力动态调整消息的消费速度。
-
-
消费者:
-
消费者从队列中获取消息,处理下单逻辑(如库存检查、订单创建等)。
-
消费者可以横向扩展,提高处理能力。
-
具体实现步骤
-
用户发起请求:
-
用户点击秒杀按钮,前端发送请求到后端。
-
后端将请求封装为消息,发送到消息队列中。
-
-
消息队列缓冲:
-
消息队列接收并存储所有秒杀请求。
-
如果队列满了,可以返回“秒杀活动火爆,请稍后再试”的提示。
-
-
消费者处理请求:
-
消费者从队列中获取消息,检查库存。
-
如果库存充足,创建订单并减少库存。
-
如果库存不足,返回“秒杀失败”的结果。
-
-
结果通知:
-
处理完成后,消费者可以通过 WebSocket、短信或站内信通知用户秒杀结果。
-
逐渐偏离主题 我感觉这个redis自带队列的stream好难受 看不懂
-
初始化监听:
-
消费者进入一个无限循环(
while(true)
),持续监听消息队列。 -
使用 Redis 的
XREADGROUP
命令从指定的 Stream(s1
)中读取消息。 -
设置
BLOCK 2000
参数,表示最长等待 2000 毫秒,如果队列中没有消息,消费者会阻塞等待。
-
-
消息处理:
-
如果读取到消息(
msg != null
),消费者会调用handleMessage(msg)
函数处理消息。 -
处理完成后,消费者需要发送 ACK(确认)以告知消息队列该消息已被成功处理。
-
-
异常处理:
-
如果在处理消息时发生异常(
catch(Exception e)
),消费者会进入一个内部循环,尝试重新处理未确认的消息。 -
使用
XREADGROUP
命令从 Stream 中读取未确认的消息(0
表示未确认的消息)。 -
如果重新处理成功,消费者会继续处理下一条消息;如果再次失败,会记录日志并继续循环。
-
-
无消息时的处理:
-
如果
XREADGROUP
返回null
,表示没有新消息,消费者会继续下一次循环。
-
如果未确认 会到peddinglist里 从peddinglist取出来再做处理
可以设置休眠 避免操作过于频繁
查详情的时候 查首页的时候
点赞功能
判断是否点过赞
查询当前用户 查询当前帖子是否被点赞
top5点赞用户 zset命令 给id上前缀 查询出结果放在set集合里 top5 解析出id 返回在ids集合里
-
查询top5的点赞用户:使用
stringRedisTemplate.opsForZSet().range(key, 0, 4)
从Redis中获取点赞用户的ID。 -
检查是否为空:如果
top5
为空或null,直接返回一个空列表。 -
解析用户ID:将获取到的点赞用户ID转换为
Long
类型,并收集到列表中。 -
查询用户信息:根据用户ID查询用户信息,并将用户信息转换为
UserDTO
对象。 -
返回结果:返回包含用户信息的列表。
省去stream流
java">public Result queryBlogLikes(Long id) {
// 1. 查询top5的点赞用户
String key = BLOG_LIKED_KEY + id;
Set<String> top5 = stringRedisTemplate.opsForZSet().range(key, 0, 4);
// 如果top5为空或null,返回空列表
if (top5 == null || top5.isEmpty()) {
return Result.ok(Collections.emptyList());
}
// 2. 解析出其中的用户的id
List<Long> ids = new ArrayList<>();
for (String userId : top5) {
ids.add(Long.valueOf(userId));
}
// 3. 根据用户id查询用户
List<User> users = userService.listByIds(ids);
List<UserDTO> userDTOS = new ArrayList<>();
for (User user : users) {
userDTOS.add(BeanUtil.copyProperties(user, UserDTO.class));
}
// 4. 返回
return Result.ok(userDTOS);
}
出现问题 点赞排行反了 用户5 和用户1 先展示用户1 数据库在用in 时 会优先从小到大排
java">@Override
public Result follow(Long followUserId, Boolean isFollow) {
// 1. 获取登录用户
Long userId = UserHolder.getUser().getId();
// 2. 判断是关注还是取关
if (isFollow) {
// 3. 关注,新增数据
Follow follow = new Follow();
follow.setUserId(userId);
follow.setFollowUserId(followUserId);
followMapper.insert(follow);
} else {
// 4. 取关,删除数据
followMapper.deleteByUserIdAndFollowUserId(userId, followUserId);
}
return Result.ok();
}
public interface FollowMapper {
// 插入关注关系
int insert(Follow follow);
// 根据用户ID和关注用户ID删除关注关系
int deleteByUserIdAndFollowUserId(@Param("userId") Long userId, @Param("followUserId") Long followUserId);
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper.FollowMapper">
<insert id="insert" parameterType="com.example.entity.Follow">
INSERT INTO tb_follow (user_id, follow_user_id)
VALUES (#{userId}, #{followUserId})
</insert>
<delete id="deleteByUserIdAndFollowUserId" parameterType="map">
DELETE FROM tb_follow
WHERE user_id = #{userId} AND follow_user_id = #{followUserId}
</delete>
</mapper>
java">@Override
public Result isFollow(Long followUserId) {
// 1. 获取登录用户
Long userId = UserHolder.getUser().getId();
// 2. 查询是否关注
Integer count = followMapper.countByUserIdAndFollowUserId(userId, followUserId);
// 3. 判断
return Result.ok(count > 0);
}
Mapper
public interface FollowMapper {
// 根据用户ID和关注用户ID查询关注关系数量
int countByUserIdAndFollowUserId(@Param("userId") Long userId, @Param("followUserId") Long followUserId);
}
xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper.FollowMapper">
<select id="countByUserIdAndFollowUserId" parameterType="map" resultType="int">
SELECT COUNT(*)
FROM tb_follow
WHERE user_id = #{userId} AND follow_user_id = #{followUserId}
</select>
</mapper>
如果关注 把关注用户的id放入redis的set集合 把当前登录用户和被关注者放在redis里
java">@Override
public Result follow(Long followUserId, Boolean isFollow) {
// 1. 获取登录用户
Long userId = UserHolder.getUser().getId();
String key = "user:" + userId + ":follow";
// 2. 判断是关注还是取关
if (isFollow) {
// 3. 关注,新增数据
Follow follow = new Follow();
follow.setUserId(userId);
follow.setFollowUserId(followUserId);
boolean isSuccess = save(follow);
if (isSuccess) {
// 4. 把关注用户的id,放入redis的set集合
stringRedisTemplate.opsForSet().add(key, followUserId.toString());
}
} else {
// 5. 取关,删除数据
boolean isSuccess = remove(new QueryWrapper<Follow>()
.eq("user_id", userId)
.eq("follow_user_id", followUserId));
if (isSuccess) {
// 6. 把关注用户的id从Redis集合中移除
stringRedisTemplate.opsForSet().remove(key, followUserId.toString());
}
}
return Result.ok();
}
使用zset 用最后一个score值排序作为滚动分页标准 zrevrangebyscore 从max 1000 到 0 展示出 小于等于1000 的三个数 7 6 5
从5开始到0 输出小于5的三个数 4 3 2
8 7 6 改为 8 6 6 limit后跟从条件是几个数字相同 比如说 这里有两个6 此时limit应该为2 像上面的0 就是不在意此时是否相同 输入0则小于等于当前最小值
Controller控制层