部署:
1. 导入sql
开发:
Session登录:
session的原理是cookie,每个session都有个唯一的sessionId, 在每次访问tomcat的时候sessionId就会自动的写在cookie当中, 携带着sessionId就能找到session, 所以不需要返回用户凭证
每一个进入tomcat的请求都是有一个独立的线程来处理的
ThreadLocal:
每个线程都拥有一个ThreadLocalMap, 健是ThreadLocal对象, 值是所存储的变量副本
ThreadLocal为每个线程独立的提供了变量副本, 使得每个线程可以独立的操作自己的变量
对于那些只需要在单个线程中保持状态, 而不需要在多个线程之间共享的变量,使用ThreadLocal化非常合适, 可以避免使用锁带来的性能损耗. 因为每个线程上都有自己的变量副本 ,不需要进行同步操作.
注意: 如果线程结束后没有手动的删除ThreadLocal变量, 存储在线程本地变量表中的ThreadLocal对象不会自动删除, 可能导致他们不会被垃圾回收机制回收, 这样可能造成内存泄漏.
所以在使用之后应该及时的调用remove删除变量副本
配置拦截器步骤:
1. 编写实现了HandlerInterceptor接口的类, 有三个方法:
preHandler: 对应Controller方法执行之前, 返回为true则放行, 若返回false则中断执行
postHandler: 对应Controller方法执行之后, DispatcherServlet进行视图的渲染之前
afterHandler: 对应的是DispatcherServlet进行视图的渲染之后, 多用于统一的资源清理, 日志和异常的处理.
集群的session共享问题:
当服务需求比较大的时候, 一台tomcat服务器是不能满足需求的, 可能需要多条tomcat共同服务, 这时同一个用户的不同请求可能会分配到不同的tomcat上, 这时就需要tomcat之间共享session数据 来保证用户不需要重复地登录验证
前端在登陆成功后会接收token, 使用浏览器存储或者使用pinia存储, 在axios配置请求头,每次氢气都携带返回的token.
为什么使用随机生成的token作为存储用户信息的key:
1. 确保唯一性
2. 如果和存储code(验证码)一样使用手机号作为key, 那么在做登录验证的时候,就需要将手机号在登陆成功时返回前端, 这样前端在请求头中携带手机号来做登录验证, 就有比较大的数据泄露的风险,使用随机数既能确保唯一性,又能避免数据泄露.
在存储用户信息时,采用的存储结构是Hash结构,相比于使用String有两点好处:
1. 能够修改里面的某个元素
2.内存利用率比较高
登录拦截器的优化:
在使用原来的拦截器刷新用户信息存储时间时,并不是所有的路径都被拦截, 这样就可能会使得用户在浏览一些不需要做登录验证的页面时时间过长导致需要重新登录, 为了解决这个问题, 采用如下结构
拦截器的执行顺序默认是添加顺序, 但是有一个order属性可以控制的更加严谨, order的值越小(最小为0)执行优先级越高
商户查询缓存:
redis缓存工作图:
这是将主页面的常用的商店列表的数据写入Redis
缓存更新策略:
先删缓存后更新数据库的出现缓存和数据库不一致的概率高于先更新数据库后删除缓存的概率
注意: 缓存的操作速度高于数据库的操作
数据的查和读操作速度高于写操作
先删除缓存后更新数据库情况下造成缓存和数据库不一致的发生情况为: 线程1删除缓存->线程2查询缓存未命中,查询数据库->线程2将数据写入缓存->线程1更新数据库
先更新数据库后删除缓存情况下缓存和数据库不一致的发生情况为: 缓存失效->线程1查询缓存未命中,查询数据库->线程2更新数据库->线程2删除缓存->线程1将数据写入缓存
因为对数据库的写操作相较于对数据库的查和读操作以及对缓存的操作来说速度是比较慢的, 所以先啥删除缓存后更新数据库的情况下造成缓存和数据库中的数据不一致的概率是比较大的
缓存穿透:
缓存雪崩:
缓存击穿:
基于互斥锁解决缓存击穿的问题:
redis中有一个setnx指令, 当key不存在的时候才会执行, 存在则返回为null.
利用这个特性来做为锁
整个流程:
先从redis中查询, 如果未查到, 则尝试获取锁, 如果获得锁则查询数据库并写入缓存; 如果没有获得锁, 则等待一段时间后迭代, 即先去缓存中获取,如果缓存中没有则去数据库中查找, 所以这个锁的释放一定要在写入缓存之后
基于逻辑过期的思想来解决缓存击穿的问题:
这是应对已经存储在缓存中可能会更新的数据, 首先去缓存中查找数据(不会为null, 因为前提就要把用对的数据写入缓存中), 查到数据后进行逻辑判断, 如果没有过期,则直接返回数据, 如果过期, 则返回旧数据, 并尝试获取针对该数据的锁, 如果获取锁是失败, 则直接返回数据, 如果获取锁成功, 则返回旧数据的同时另起一个线程用于去数据库查询数据, 并将其写入缓存来更新旧数据.
秒杀模块:
全局ID生成器:
线程池和CountDownLatch:
private ExecutorService executor = Executors.newFixedThreadPool(300);
这是声明了一个管理300个线程的线程池, 使用它的submit方法来提交实现了Runnable接口的类,或者在类使用lambda表达式.
CountDownLatch countDownLatch = new CountDownLatch(60);
初始化值为60, 它有两个常用的方法, countDown和await方法
countDown方法会将值减一, await会阻塞当前线程
当值减到0时, 当前线程(调用await方法的线程)会继续执行
注意, 比如说, 这个函数会启动100个线程, 但是初始值设为60, 那么主线程(调用这个方法的线程,也可以是测试方法)只会等待60个线程执行完毕, 对于剩余的40个线程不会等待(但是也会启动)
同样的如果初始值设为200(比线程数大), 那么值永远都不会减为0 ,那么主线程将会一直等待
这个初始值的设定要考虑多方面因素.一般和连接池的初始化数量保持一致.
超卖问题的解决:
版本号法: 每次库存量减一的时候版本号加一
这两种方法的解决思路是一样的, 只不过CAS是用数据(库存)来替代version(版本号)
库存比较特殊, 在使用乐观锁时不一定是库存量有没有变化, 也可以是只判断库存量是否大于0
如果是判断库存量有没有变化的话, 失败率是比较高的, 因为100->99也是变化, 而99也是可以继续减少的, 所以当一个线程对库存量减一时, 其他的线程发现库存量被修改时就放弃修改, 这样就会加大请求的失败. 而将判断更改为库存量是否大于0则能够有效的避免这个问题.
一人一单问题的解决:
这个问题的解决使用到的编程技巧太带劲了.包括悲观锁的使用, 代理对象的使用, String.toString()的方法会生成一个新的对象, 所以使用 intern()方法, 这个方法的作用是确保所有具有相同字符序列的字符串字面量都被存储在一个唯一的字符串池中。当调用 intern()
方法时,如果字符串池(字符串常量池)中已经存在等于此 String
对象的字符串,则返回代表池中这个字符串的 String
对象的引用;否则,将此 String
对象添加到池中,并返回此 String
对象的引用。
对方法添加事务注解时, 如果在该类的某个方法中调用了这个方法,即this.method()
这时事务的注解是不生效的, 因为事务的实现是Spring获取类的代理对象, 由代理对象做的事务处理
所以应该使用
AopContext.currentProxy();
来生成所在类的代理对象,由该代理对象调用方法
synchronized (userId.toString().intern()){
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
}
使用该方法需要添加aspectjweaver依赖, 并且在启动类中添加@EnableAspectJAutoProxy(exposeProxy = true)的注解,来暴露代理对象
在集群模式下的一人一单的并发问题:
启动了两个进程, 就存在两个JVM,即两个锁监视器, 所以锁在另一个启动的项目中无效
分布式锁:
要做的就是让多个进程都从一个锁监视器中获取锁
判断和释放是两步, 如果判断完成后发生了阻塞, 这时可能锁已经释放了, 然后其他的线程获取了锁,并存储了自己的值, 这是阻塞结束,就会删除现存的锁,发生并发安全问题, 为了解决这个问题, 就需要将这两步合成为一步, 方法就是利用Lua脚本
Lua脚本:
java调用Lua脚本:
Redisson:
@Bean修饰方法spring会在启动时自动调用这个方法, 将返回值注册为spring管理的Bean
Redisson可重入锁的实现原理:
分布式锁的总结:
优化秒杀:
对于数据库的修改操作是比较耗时的, 所以将库存和一人一单的校验利用redis优化, 然后对于数据库的写操作另起线程做, 因为此时对于写的时效性没有要求, 这样就能提高整体的效率.
基于jvm的阻塞队列存在内存限制问题
每当重启或出现宕机时阻塞队列中的数据就会消失, 或者当从阻塞队列中拿到数据, 但是在做处理时出现异常也会造成数据安全问题
Redis提供的消息队列:
基于list的消息队列:
基于PubSub的消息队列:
基于Stream的消息队列:
消费者组:
达人探店:
点赞:
每个人只能对一个blog点一个赞, 所以考虑使用set(具有唯一性)
点赞排行榜:
按照时间顺序, 显示前五名, 这就要求存储的数据结构具有顺序, 综合下来, SortedSet满足要求.
set有ismember来判断给定元素是否存在, 而SortedSet没有这个方法, 但是可以通过score来查分数, 如果给定元素不存在则返回null, 可以根据此来判断是否已经点赞. SortedSet是根据score(分数)来自动排序的, 所以可以在里面存时间戳, 然后获取0-4的元素即为前五个.
select返回的数据默认是按照id大小的, 但是这个给定的id顺序不一致, 而给定的id顺序是点赞顺序, 所以后面要加上order by filed (id, 5, 1), 这样才会按照给定的id顺序返回
这段代码是处理获取SortedSet中前五个数据, 重点是看将从数据库中返回的List<User>转换为List<UserDTO>的过程, 先将其转换为stream<User>, 然后用其提供的map()方法转换为Stream<UserDTO>, 最后用其提供了collect()方法将其转化为List<UserDTO>
还有一个就是将List<Long>转换为用","间隔的字符串的方法:
StrUtil.join(",", ids)
关注:
在求共同关注时, 可以借助set集合的求交集的功能:SINTER, 将每个用户的的关注对象都存储在set集合中, 当前端发送求共同关注的请求时要携带访问主页的id号, 然后去到redis中求这两个id对应的set的交集.
关注推送:
拉模式: 只有在粉丝打开收件箱的时候才会去读取关注人发布的信息,然后根据发布时间去排序, 并且不做保存, 优点就是减少内存, 缺点是每次读取都要去发送请求重新获取, 并且对获取到的数据进行处理, 比较耗时.
推模式: 没有发件箱, 博主发布后直接发送到粉丝的收件箱, 能够极大程度的减少延迟, 缺点就是对内存的占用比较高.
推拉结合: 针对普通博主(粉丝比较少的)将发布的内容直接推送到粉丝; 针对大V, 将粉丝群体分为活跃粉丝和普通粉丝, 对于活跃粉丝直接将发布的内容发到其收件箱, 而对于普通粉丝则采用拉模式.
Feed流中的分页问题:

所谓滚动分页就是按照上一次查询的最后一条数据查找, 不在按照角标查询, 而List只能按照角标查询, 所以只能使用SortedSet, 因为SortedSet不仅能按照角标查询,即ZRANGE, 也能按照socre查询.
注意offset的设置
这是获取收件箱中内容的函数, 注意: 1. List<Long>用于存储收件箱中用户的id, 可以在初始化的时候大小设定为返回的个数, 这样就避免了扩容时带来的消耗. 2. os(最小时间戳的相同个数)的计算方法.
附近商户:
GEO:
地理坐标, 太牛逼了
这是将店铺的id和坐标按照店铺的类型导入到不同的geo中的测试方法
注意: 1. 将店铺List<Shop>分组成Map<Long, List<Shop>>的方式, 使用了stream流提供的按照指定内容分组的函数, 如果不知道这个方法,可能就需要来迭代一遍数组进行分组了.
用户签到:
BitMap:
用BitMap中的01来映射是否签到, 比如某个用户一个月的签到情况就可以使用一个31bit位的BitMap来表示, 第一位代表第一天,以此类推, 这样就能大大减少存储的数据量
用于签到的函数:
注意: 日期的格式化使用的函数, 获取日期的当前所在月的天数
签到统计:
UV统计:
第三个命令是合并, 可以合并多个HL