Redis--黑马点评项目集群模式下的线程安全问题
我们之前在实现一人一单的功能时,为了防止线程安全问题,解决方案为使用synchronized关键字将用户id作为锁对象,保证同一个用户只能有效发送一次请求。
但是这种加锁方式只能在单体项目中有效,对于集群项目,也就是有多台服务器的项目,这种加锁方式会失效。主要原因在于,synchronized的原理其实是依靠JVM中的锁监视器,该监视器会查看是否已经有某个线程占用了锁,如果锁已经被占用,那么其他线程也就无法获取锁。
但在集群模式下,不同的机器拥有不同的JVM,JVM中存储的信息自然也不相同。因此依靠synchronized实现的锁机制自然也会失效。
解决方案为使用分布式锁,实现分布式锁的核心就是让锁监视器的监视范围由一台机器扩展为多台机器。
因此锁监视器必然不能是机器中的部件。目前,主要有3种实现分布式锁的方案:MySql、Redis和Zookeeper
本项目使用Redis实现分布式锁,使用Redis实现锁的机制其实在解决缓存击穿问题时已经使用过,其核心就是利用Redis中的setnx语句,该语句会判断在Redis中是否已经存在目标key,如果存在则操作 ...
JUC--使用Interrupt实现两阶段终止线程
如果我们想要在一个线程中优雅的终止另一个线程,可以使用interrupt方法实现,所谓优雅的终止指的是不是突然终止某个线程,而是告诉被终止的线程你即将被终止,让该线程可以在终止之前做一些工作
假设我们需要实现一个监控类,这个类需要不断取获取系统当前的信息,并且系统可以决定终止该类的执行。
我们的想法肯定是让一个线程不断的轮询,获取系统信息,那么如何终止该线程呢?如果直接调用stop方法,可能会造成死锁,因为有些资源会永远无法释放。
正确的做法是,当想要终止另一个线程时,可以调用对应的线程的interrupt方法,该方法可以将线程的中断标识位置为true,接下来只需要通过isInterrupt来获取中断标识位(还有一个静态方法interruptted也能够获取中断标识位,与isInterrupt不同的是后者在返回标识位后会清除标识位,即如果标识位为true的话会置为false),如果为true直接跳出循环。需要注意的是如果当前需要中断的线程处于sleep、wait、join等状态时,调用它的interrupt会使该线程出现异常并清空中断标识位(置为false),因此我们需 ...
JUC--线程中常用的方法
方法名
作用
setName()
设置线程的名字
getName()
返回线程的名字
sleep(int x)
使线程休眠,线程进入阻塞状态
start()
启动线程
run()
该方法中可以编写线程需要完成的工作
setPriority(int priority)
设置线程的优先级,数字越大,优先级越高
getPriority()
获取线程优先级
interrupt()
打断线程,当打断的线程的状态为阻塞时,相当于唤醒该线程(由阻塞变为就绪),其打断标记位为false;如果打断的是一个正常运行的线程,该线程的打断标记位为true,并且该线程并不会停止运行
yield()
线程的让步,让cpu先执行其他线程,让步后的线程进入就绪状态,不一定成功(因为调度器可能还是执行该线程)
join()
线程插队,插队一旦成功,一定会先执行完插队的线程。一般用于线程间的同步
isInterrupt()
获取线程的打断标记位
123456789101112public void testInterrupt() throws Interrupted ...
滑动窗口
滑动窗口题集LeetCode76
维护一个滑动窗口,需要判断窗口中是否含有目标串所有字符。如何快速判断该窗口中是否含有目标串所有字符呢?可以使用一个哈希表记录目标串中的各个字符数,并使用一个变量记录目标串中的字符种类。当该变量为0时说明当前窗口包含目标串中所有字符。
使用指针i,j分别表示窗口的右指针和左指针,右指针每向右移动一次,都要判断左指针能否向右移动,从而使子串最短。判断方法为:用一个哈希表记录此时窗口中含有的各个字符数,当前左指针表示的字符数是否大于目标串需要的字符数,如果大于说明左指针可以移动。
实现代码:
1234567891011121314151617181920212223242526272829303132333435363738394041class Solution { public String minWindow(String s, String t) { char str[] = s.toCharArray(); char tstr[] = t.toCharArray(); in ...
短链接--用户注册
问题:布隆过滤器是如何解决缓存穿透问题的?它有什么优缺点?
–解答:布隆过滤器本身是由一个很长的二进制位数组和一些无偏哈希函数组成。这些哈希函数会分别对要插入的数据进行散列,会将数组对应下标处的值置为1,相当于把这个数据存入到布隆过滤器。通过布隆过滤器能够判定数据库中指定数据是否不存在,只要布隆过滤器中没有该数据,那么数据库中就一定没有该数据,从而避免访问数据库。
–它的优点就是:时间复杂度低:O(n),n为哈希函数数量;安全,存储的不是数据本身;节省内存,存储空间小
–它的缺点是:存在误判:通过布隆过滤器无法判断某个数据是否一定存在于数据库中;不易取出数据,因为存储的不是数据本身(即不能反序列化);很难删除元素
问题:这样听起来,布隆过滤器和Redis很像,它们都是缓存,都是修改数据库时需要将数据存入,为什么布隆过滤器就能解决缓存穿透而Redis就不行呢,况且前者还存在误判?
–解答:实际上redis以及一些哈希数据结构比如set都具有判断某个key是否在存在的功能,这里redis有缓存穿透是因为此处Redis的是当作缓存使用的,而缓存的使用逻辑就是“如果缓存中没有数 ...
Redis--黑马点评项目秒杀券
秒杀券功能乐观锁解决超卖问题
秒杀系统的难点在于解决高并发、多请求,在以上情况下非常容易出现超卖现象,即卖出去商品的数量大于商品的库存数量。出现这种问题的原因在于,我们判断一个用户能否购买该券的前提是从数据库中查到的券的库存数量是否大于0,某一时刻同时来了大量请求假设为100,此时它们查到的数据都大于0假设为5,于是每个线程都会去更新数据库,导致数据库中库存的值为-95,这就造成了超卖。
解决该问题的常见方法就是加锁,这里介绍两种比较常用的锁。
悲观锁:这种锁的特点就是对于每个请求都需要拿到锁之后才能继续执行,并且同一时刻只能有一个请求拿到锁。其优点就是成功率高,缺点是由于每个线程都需要进行排队,效率低下。
乐观锁:这种锁的特点它认为线程安全问题不一定会发生,只在数据库更新数据时判断此时是否有人修改过数据,如果数据被修改过,则不更新数据。这种锁的优点是支持线程并发,性能高,缺点是请求成功率低——只要数据不对就需要不断重试。
针对乐观锁的设计,一般有两种方案。
版本号法:在数据库表字段中再增加一个版本字段,查询时同时查数据和版本号,更新时判断此时版本号和之前的版本 ...
JUC--线程运行原理
线程运行原理 在java中有三块用于存储数据的内存:栈、堆和方法区,其中方法区用来存储类加载后形成的字节码,堆用来存储引用类型的数据以及成员变量,而栈用来存储线程。
每个线程都有属于自己的线程栈,栈与栈之间互不影响。每个线程栈都有一个个栈帧组成,每个栈帧对应着一个方法,存储着该方法所需要的局部变量。
123456789public class TestFrame{ public static void main(String args[]){ method1(10); } public void method1(int x){ int y = x + 1; }}
通过debug可以看到以上代码对应的栈结构
线程上下文切换 所谓线程的上下文切换指的是某个线程由占有CPU的状态变为不占有CPU的状态。发生线程上下文切换时需要保存当前线程运行的状态,会使用PC(程序计数器)保存当前线程运行的指令,除此之外还需要保存线程中的一些变量等信息,可见发生线程的上下文切换 ...
短链接--用户信息脱敏展示
针对用户中的一些敏感信息,不能不展示,因为有时候需要用到这些数据,但又不能完全展示,因为可能会暴露用户的隐私(比如手机号,身份证号等信息),因此需要对这些数据进行脱敏处理,即只展示数据的部分信息,其余部分用特殊字符表示。
本项目中使用的是自定义Json序列化的方式,在SpringMVC中当用户信息返回给浏览器时默认会使用Jackson进行序列化,我们只需要改变此处的序列化方式就能达到给用户信息脱敏的效果。
1234567891011121314151617181920package com.deng.shortlink.admin.util;import cn.hutool.core.util.DesensitizedUtil;import com.fasterxml.jackson.core.JsonGenerator;import com.fasterxml.jackson.databind.JsonSerializer;import com.fasterxml.jackson.databind.SerializerProvider;import java.io.IOExc ...
MIT6.S081操作系统学习-2
Trap机制 今天要讨论的内容是程序是如何完成从用户空间到内核空间的切换的,这种机制称为trap,今天的讨论会更加深入,更加底层也更加困难,希望大家能够尽可能的跟上。
在xv6系统中,我们的设计是当用户程序发生系统调用、异常以及中断时,将会触发trap机制,完成由用户空间到内核空间的转换。
以用户程序调用系统调用触发trap机制为例,研究底层是如何完成这种切换的。在这个过程中硬件的状态非常重要,有很多工作都是将硬件的状态由适合用户空间的状态改为适合内核的状态。而在诸多硬件中,我们最关心的是32个用户寄存器的状态(RISC-V中提供了32个寄存器,包括了堆栈寄存器Stack Register),除了用户寄存器外还有一些对该过程非常重要的寄存器:
STAP寄存器,指向了当前程序的最高级页表的地址;
PC寄存器,也叫程序计数器,存储当前程序将要执行的指令;
STVEC寄存器,指向内核中处理trap指令的起始地址;
SEPC寄存器,在处理trap过程中用于保存PC寄存器中的值;
用于表示系统当前状态的标识位mode
SSRATCH寄存器,xx;
在接下来的讨论 ...
MIT6.S081操作系统学习-1
操作系统 最近也是在学习MIT6.s081,学了一段时间后,知识点很多也很杂,今天在复习的同时也顺带将这些知识点串一下。
首先,我们需要知道什么是操作系统?操作系统有什么用?
那么我认为,操作系统它也是一个程序,只不过它是一个特殊的程序。为什么呢?操作系统能够执行一些特殊的指令,比如创建进程,分配内存等。而普通的程序没有执行这些操作的权限,那有人可能会说,不对啊,我平时使用java等编程语言也能创建进程啊。别急,等下会介绍这个话题。这是因为操作系统能够直接操控最底层的硬件资源,一般的操作系统会将这些硬件资源进行抽象,比如将磁盘等存储器抽象为文件,将CPU抽象为进程。为什么要设计操作系统呢?一方面,是为了方便用户更好的使用计算机,因为直接和硬件打交道太麻烦了,用户必须得输入二进制指令才能和计算机进行交互,有了操作系统后,可以基于操作系统开发出更加高级的语言来和计算机进行交互,比如汇编,C,C++等;但其实最重要的一点是,设计操作系统是为了保护底层硬件,用户可以直接操控底层硬件是一种非常可怕且危险的形式,有些不怀好意的人他们编写的程序可能会使你的计算机崩溃,这不是一种好的设计。因 ...
O(1)时间复杂度求动态数据的中位数
中位数大家都知道,就是一些数经过排序后最中间的数。具体来说如果有n个排好序的数,当n为奇数时,这些数的中位数就是下标为n/2(下取整)的数;当n为奇数时,这些数的中位数就是下标为n/2和下标为n/2+1的数的平均数。
如果给定一个无序数组,并且数组中的数据是动态的,存在删除和新增操作,如何在O(1)的时间复杂度内快速得到该数组的中位数呢?
详细题目可以参考LeetCode295
这里提供的思路为:用一个大根堆维护数组中较小的一半的数,用一个小根堆维护数组中较大的一半的数,当数组的长度为偶数时,该数组的中位数等于两个堆顶元素的平均值;当数组的长度为奇数时,该数组的中位数等于小根堆的堆顶元素(因为优先插入的是小根堆,所以当数组长度为奇数时,小根堆中的元素个数比大根堆多一)
具体的维护方式为:当小根堆和大根堆中的元素个数相等时,此时如果发生数据的添加,需要往小根堆中添加元素,为了使小根堆中的元素始终是数组中较大的一半,先将该元素插入大根堆中,再将大根堆的堆顶元素插入小根堆中;如果不相等,往大根堆中添加元素,为了使大根堆中的元素始终是数组中较小 ...
短链接--全局统一返回实体
全局统一返回实体 为了使在返回给浏览器数据时更加方便,适配各种返回情况,比如只返回成功或失败的信息、返回信息和数据,只返回数据等等。提高开发效率
全局统一返回实体的设计
123456789101112131415161718192021222324252627282930313233343536373839404142434445import lombok.Data;import lombok.experimental.Accessors;import java.io.Serial;import java.io.Serializable;/** * 全局返回对象 */@Data@Accessors(chain = true)public class Result<T> implements Serializable { @Serial private static final long serialVersionUID = 5679018624309023727L; /** * 正确返回码 */ public static ...
