首页 > 数据库 >用redis项目练习笔记,跟着黑马敲,并有自己的理解在里面

用redis项目练习笔记,跟着黑马敲,并有自己的理解在里面

时间:2023-05-28 18:14:11浏览次数:40  
标签:释放 return redis userId 笔记 id voucherId 线程 黑马

点评中,优惠卷牵扯到的秒杀问题。

超卖现象

如果多线程同时执行会因为高并发,先查询 再插入之间会有空档时间,发生超卖问题。可以使用悲观锁或者乐观锁解决,出于对性能的考虑,用到了乐观锁。

乐观锁的实现,用到了数据库where语句 多加一个条件。 每次判断跟上次相同,(这样会造成大量的失败问题)

于是引出,用库存>0做判断。(但是其实这样用到的也是类似悲观锁的mysql的行锁。

 

乐观锁
  • 假定数据一般情况下不会被其他线程修改,所以不会事先上锁。

  • 访问数据时,判断数据是否被其他线程修改,如果未修改则正常访问,如果被修改则采取其他措施(重试、报错等)。

  • 这种方式可以提高性能,但可能会造成数据的不一致性,需要容错措施。

悲观锁
  • 假定将要访问的数据一定会被其他线程修改,所以在访问数据之前会先加锁,阻止其他线程访问。

  • 加锁后,才访问数据,使用完成后解锁,释放锁。

  • 这种方式可以保证加锁段的原子性,但锁的粒度过大会影响性能。

一人一单

超卖的现象解决之后,又遇到了,每个人会买很多单。为了杜绝黄牛现象,使用一人一单,在每次查询数据库库存的时候单独查询一次下单的订单表有没有相同id的用户

 int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
 if (count>0) return Result.fail("不能重复购买");

使用悲观锁 让一个人只能买一单

这里锁的条件 如果只用toString 是锁不住的 ,因为 直接调用toString()方法,返回该userId对象的字符串表示。返回的字符串对象是新创建的,不是字符串常量池中的对象。每次调用都会创建一个新字符串对象,这些对象在JVM中是不同的对象,有不同的标识。

intern()方法会返回字符串常量池中的字符串对象。如果池中已经存在相同内容的字符串,则返回池中的字符串对象,否则将此字符串对象加入池中,并返回该对象所以,相同内容的字符串,intern()返回的始终是同一对象

     Long userId = UserHolder.getUser().getId();
     synchronized (userId.toString().intern()){
         IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
         return createVoucherOrder(voucherId,userId);
    }
 }
 ​
 @Transactional
 public Result createVoucherOrder(Long voucherId,Long userId) {
 ​
     int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
     if (count>0) return Result.fail("不能重复购买");
 ​
     //扣库存
     seckillVoucherService.update().setSql("stock = stok - 1").eq("voucher_id", voucherId).update();
     //创建订单
     long orderid = redisWorker.nextId("order");
     VoucherOrder voucherOrder = VoucherOrder.builder()
            .id(orderid)
            .userId(userId)
            .voucherId(voucherId)
            .build();
     boolean save = save(voucherOrder);
 ​
     //返回订单ID
     return Result.ok(orderid);
 }

这里把createVoucherOrder 封装成了单独的一个方法,但是 调用它的方法并没有开启事物,这就造成了可能事物不生效。于是要使用代理对象调用。

 IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
 return proxy.createVoucherOrder(voucherId,userId);

这里用到了AspectJ的 AopContext 对象 找到当前的代理对象 强制转换成对应的接口,并调用接口中定义好的方法。相当于调用的动态代理生成后的方法。可以解决事物生效问题。

 

但是由于业务需要 ,又扩展了一台服务器,这就牵扯到服务器集群 不同jvm环境 不同常量池的问题 就引出了 分布式锁

分布式锁

这里实现分布式锁使用Redis的SETNX。 实现分布式锁需要实现两个基本方法

获取锁
 @Override
 public boolean tryLock(Long timeoutSec) {
    String name1 = Thread.currentThread().getName();
    //获取锁
    Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name,name1,timeoutSec, TimeUnit.MINUTES);
    return Boolean.TRUE.equals(success);
 }

这里用到一个亮点 自动拆箱与自动装箱 如果这里的布尔值获取不到,可能会出现空指针异常,所以,直接用封装类的判断方法直接去判断。

释放锁
 @Override
 public void unlock() {
     //释放锁
     stringRedisTemplate.delete(KEY_PREFIX+name);
 }
极端情况下产生的问题:

衍生出另一个问题,如果Thread1获取到锁之后,业务阻塞足够长时间,到锁超时释放也没有变成运行状态,这时候Thread2趁虚而入,又获取到锁,但是这时候Thread1业务完成了,把锁刚好释放了,Thread3又拿到了锁。又导致线程安全问题。

一个思路:在每次释放锁的时候,判断是不是自己的锁,是自己的再释放,不是自己的就不管了。

 @Override
 public void unlock() {
     //获取线程标识
     String threadId = ID_PREFIX+Thread.currentThread().getName();
     String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
     //判断标识是否一致
     if (threadId.equals(id)){
         //释放锁
         stringRedisTemplate.delete(KEY_PREFIX+name);
    }
 }

但是这样的话,判断锁标识和释放锁是两个动作,这两个动作之间产生了阻塞,会造成redis中定义的锁超时释放,Thread2趁虚而入,这时阻塞状态刚好结束,继续执行释放锁的方法,那么会造成之前的问题。如下图

 

解决这个问题,我们使用Redis的lua脚本 ,把几条指令封装成一个批处理程序,原子性的同时执行。

 --比较线程标识与锁中的标识是否一致
 if(redis.call('get',KEY[1]) == ARGV[1]) then
     --释放锁
     return redis.call('del',KEY[1])
 end
 return 0
 ​

然后利用stringRedisTemplate 执行这段代码

 //先把代码加载到类中
     private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
 ​
     static {
         UNLOCK_SCRIPT = new DefaultRedisScript<>();
         UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
         UNLOCK_SCRIPT.setResultType(Long.class);
    }
 ​
 public void unlock() {
     String threadId = ID_PREFIX+Thread.currentThread().getName();
 ​
     stringRedisTemplate.execute(UNLOCK_SCRIPT,
             Collections.singletonList(KEY_PREFIX+name),
             threadId);
 }

但是基于以上做的分布式锁中 有一些缺点, 不可重入,不可重试,超时释放,主从一致性

不可重入:同一个线程无法多次获取同一把锁。

不可重试:获取锁值尝试一次就返回false,没有重试机制。

超时释放:锁超时释放虽然可以避免死锁,但如果是业务执行耗时较长,也会导致锁释放,存在安全隐患。

主从一致性:如果Redis提供了主从几圈,主从同步存在延迟,当主宕机时,如果从并同步主中的锁数据,则会出现多个线程拿到锁。

 

为了解决这些问题,手写会非常麻烦,我们索性直接用框架解决问题Redisson

Redisson

Redisson,它不仅提供了一系列的分布式的java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现、

可重入锁的底层:当再一次获取锁发现锁被占用,会看看占用此锁的线程是不是自己,内部会有计数功能,看自己获取了多少次锁。

底层就是调用到了lua脚本实现。

重试:利用信号量和PubSub功能实现消息发送订阅, 等待、唤醒、获取锁失败的重试机制,其间会一直判断有没有超过已设置好的等待时间,如果超过返回false

超时延续:利用WacthDog 利用定时任务Timeout每隔一段时间 重置超时时间。 并且在释放锁的时候 获取到Timeout对象,clear掉。

标签:释放,return,redis,userId,笔记,id,voucherId,线程,黑马
From: https://www.cnblogs.com/letfly/p/17438595.html

相关文章

  • 软件工程日报——《人间》读书笔记
    总结以下《人件》这本书中涉及到的几个概念和建议1、帕金森定律帕金森定律讲述了如下的定律:如果一个很平庸的人作了管理,那么摆在它面前的只有三条路:退位给有能力的人。使用比自己更优秀的属下。运用比自己还平庸的手下。第一条路和第二条路一般是个有欲望的人,都不会采取,......
  • 【ABAQUS文档笔记】实体单元
    来自ABAQUSDOCUMENT/GETTINGSTARTEDWITHABAQUS/CAE/USINGCONTINUUMELEMENTS单元公式和积分fullintegration“完全积分”是指当单元具有规则形状时,对单元刚度矩阵中的多项式项进行精确积分所需的高斯点数。对于六面体和四边形元素,“规则形状”意味着边缘是直的,并以直......
  • WPF 入门笔记 - 02 - 布局综合应用
    本篇博文对接上篇末尾处WPF常用布局控件的综合应用,为痕迹g布局控件介绍课后作业的一个思路方法。前言首先来谈一谈布局原则:WPF窗口只能包含一个元素(Window元素属于内容控件,内容控件只允许有一个子元素),所以我们得在窗口中放置一个容器,才能使我们的窗口放置更多的内容。所以......
  • 五分钟了解Redis入门安装
    一、Redis是什么二、下载简单一点直接打开GitHub,下载.mis 三、安装 一直下一步,到这里更换你想要的路径,然后勾上自动添加环境变量,就不用手动添加  安装好的目录文件 四、启动点击redis-cli.exe 这个就是默认默认的端口号 五、配置文件cmd打开配置账号密六......
  • Git日常使用技巧 - 笔记
    Git日常使用技巧-笔记Git是目前世界上最先进的分布式版本控制系统学习资料廖雪峰学习视频https://www.bilibili.com/video/BV1pX4y1S7Dq/?spm_id_from=333.337.search-card.all.click&vd_source=2ac127043ccd79c92d5b966fd4a54cd7Git命令在线练习工具https......
  • [CMake] CMake学习笔记
    自己的学习和使用总结,还不完善,不定时更新。一.简介cmake是一款高级编译配置工具;所有操作都是通过编译CMakeLists.txt来完成的;CMake官方全部推荐使用大写指令;学习目的:为将来处理大型的C/C++、Java项目做准备;环境:Ubuntu:20.04cmake:3.16.3简单尝试:用C++写......
  • Rust学习笔记——基础篇3:数据类型
    数据类型整数类型位长度有符号无符号8-biti8u816-biti16u1632-biti32u3264-biti64u64128-biti128u128archisizeusize整数型的表述方式进制例十进制98_222十六进制0xff八进制0o77二进制0b1111_0000字节(只能......
  • CAN笔记
    一、为什么需要总线1、人类需要交换信息的时候可以通过语言、文字,机器、电器设备之间需要交流该如何呢?是的需要一门他们能够读懂的语言,那就是通信协议,这也是在最早的汽车上都是使用了大量的线束,后来慢慢的通过各类的总线进行信息的交换。2、人类的交流手段:文字、语言、动作->......
  • 用CRU给笔记本显示器超频刷新率
    https://www.monitortests.com/forum/Thread-Custom-Resolution-Utility-CRU1、下载下来解压后得到4个文件,双击CRU 2、点击Add3、在RefreshRate后面改超频刷新率,然后点OK 4、回到4个文件那里,双击restart64(你是32位系统就双击restart)5、等电脑闪屏后,重启电脑。(不重启......
  • HCIP学习笔记-云安全服务规划-6
    1.云上安全设计以及华为云安全体系1.1为什么要关注云上安全CSA:CloudSecurityAlliance,云安全联盟1.2云上企业安全诉求1.3五大安全维度应对云上安全诉求1.4华为云安全服务全景2.工作负载安全2.1企业主机安全HSS管理控制台是可视化的管理平台,便于用户集中下发配置信息,查看在同......