Redis实际应用2

全局唯一ID

下订单 这种情况,如果采用数据库的自增,则会规律明显,所以采用手动的情况下生产id
在这里插入图片描述
这是id的生产策略,注意这个32位,后面的代码需要移动32位
在这里插入图片描述

代码:

public class RedisIDWork {
    //当前的时间
    private static final long BEGIN_TIME=1652453880L;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    //递增全局id
    public long nextID(String key){
        //生产时间戳
        //产生一个调用时候的时间戳然后减去begin即可完成
        LocalDateTime now = LocalDateTime.now();
        long end = now.toEpochSecond(ZoneOffset.UTC);
        long time=end-BEGIN_TIME;

        //生产序列号
        //获取当天日期
        String format = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
        //自增长
        Long increment = stringRedisTemplate.opsForValue().increment("icr:" + key + ":" + format);

        //拼接并返回
        return  time<<32 |increment;
    }
}

秒杀下单

背景:

主要是数据库的背景:
是由两张表构成 普通卷 和秒杀卷 秒杀卷有外键链接普通卷
普通卷:
在这里插入图片描述

秒杀卷:
在这里插入图片描述

效果:

在这里插入图片描述

流程:

在这里插入图片描述

代码:

@Transactional
    @Override
    public Result seckillVoucherByvoucherId(Long voucherId) {
        //1:首先查询是否有优惠卷
        SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
        if (seckillVoucher==null){
            //1.1: 没有返回错误
            return  Result.fail("没有该优惠卷");
        }
        //1.2:有
        //2查询是否开始 或者结束
        LocalDateTime beginTime = seckillVoucher.getBeginTime();
        LocalDateTime endTime = seckillVoucher.getEndTime();
        //判断是否开始
        if (beginTime.isAfter(LocalDateTime.now())){
            //2.1 还没有开始
            return Result.fail("活动暂未开始");
        }
        if (endTime.isBefore(LocalDateTime.now())){
            //2.2过期 返回错误
            return Result.fail("活动已经结束");
        }
        //2.3 有效期之内
        //3 进行库存的查询
        Integer stock = seckillVoucher.getStock();
        if (stock<=0){
            //3.1没有库存 返回信息
            return Result.fail("抱歉该商品已经被抢完了");
        }
        //3.2 还有库存
        //4: 库存减一

        boolean update = seckillVoucherService.update()
                .setSql("stock=stock-1")
                .eq("voucher_id", voucherId).update();
        if (!update){
            return Result.fail("库存不足 插入时失败");
        }
        //5:创建订单
        VoucherOrder voucherOrder=new VoucherOrder();
        //给订单id赋值
        voucherOrder.setId(redisIDWork.nextID("skillVouCher"));
        //优惠卷的id 赋值
        voucherOrder.setVoucherId(voucherId);
        //用户的id  利用拦截器获取到
        voucherOrder.setUserId(UserHolder.getUser().getId());
        //保存到数据库
        save(voucherOrder);
        //返回订单 的id
        return Result.ok(voucherOrder.getId());
    }

效果图:

订单表达到效果 有订单的增加:
在这里插入图片描述
秒杀卷表达到效果 有库存的减少:
在这里插入图片描述

秒杀下的超卖问题

测试

采用jemter进行测试:
在这里插入图片描述

测试结果:

在这里插入图片描述
在这里插入图片描述
不出所料 出现了超卖的问题

原因

原因很简单 ,高并发的访问下,有没有设置锁,当然会造成这种情况的产生。

解决方案:

加锁:
在这里插入图片描述
由于并发不一定经常发生,所以建议采用乐观锁

乐观锁 版本号

在这里插入图片描述
在这里插入图片描述
由于 我们的库存和version 都在改变的 所以完全可以用库存当version
于是有了下面这个方法

乐观锁 CAS法

CAS: compare and set
在这里插入图片描述
在这里插入图片描述

CAS 方法:

代码

只需要在原来的基础上 在插入时进行这样的判断即可:在这里插入图片描述

结果

两白个线程 只卖出26件在这里插入图片描述

原因:

加入 有1个线程修改了 但在他修改的时候 已经有99个线程进来
那么用了这个方法 则会导致 100线程里面 只有一个 能够成功下单。

CAS2.0:

代码:

只需要库存 大于0即可
在这里插入图片描述

结果:

刚好抢完 也刚好100条数据
在这里插入图片描述
在这里插入图片描述

超卖问题的总结

在这里插入图片描述

一人一单

流程修改:

其实很简单,就是订单里面 判断 userid和优惠卷id 是否 已经存在一模一样的,有则证明已经下过单了,没有则没下过
在这里插入图片描述

代码:

只需要添加这一步的判断即可
在这里插入图片描述

结果:

在这里插入图片描述

原因

原因依旧是 高并发下的不安全问题,我们的这个设置时候 依旧可能有线程进来了。

解决方案!!!:

只能是悲观锁了

由于乐观锁需要加版本 适用于修改的,我们的这个业务是插入的业务,初始为空的,哪里来的什么版本

思路
第一步方法拆分

我们的方法 需要加锁的拆除来即可
以一人一单以上的方法都是在进行查询操作,
所以下面需要加事务 加锁 上面的方法则可以把事务取消
在这里插入图片描述
在这里插入图片描述

第二步加锁

思考:

1.0

**1.0:**首先最快想到的是 加在方法上,如下图。但是由于我们的锁一旦加了,如果锁的对象一样 则会进行锁住一个一个执行。加在方法上面锁的的对象是this,每一次不管怎么样都要锁住 效率过于低下,在本场景有更优解。
在这里插入图片描述

2.0

2.0: 我们要预防的是同一个ID的用户,不进行多抢即可。那我们只需要锁住id即可,如下图

在这里插入图片描述
但是这样相当于没锁 为什么?
因为userid 是Long类型的,每一个线程来虽然值一样但是地址是不一样的,
我们的锁看的是地址不是值。
测试结果:
在这里插入图片描述

3.0

3.0 基于上述,我们只需要 保证userid 值对应的对象唯一即可
所以用常量池,第一时间应该想到string,所以转换成string就行了嘛?
看源码:
在这里插入图片描述
依旧会new
正解是采用string的intern 就是 按照值去常量池找 有则返回 常量池里面的,没有则放进去。
在这里插入图片描述

在这里插入图片描述
上锁:
在这里插入图片描述

结果:
在这里插入图片描述
在这里插入图片描述

一人一单的多机问题

准备:

复制服务,开一个8082端口
ctrl+D
在这里插入图片描述
修改Nginx :
以便于达到负载均衡的效果:
在这里插入图片描述
重启Nginx即可:

测试

代码不变,还是用jemeter 进行测试:
会出现两个订单
在这里插入图片描述

原因

因为是两个服务,常量池都变了,所以会出现锁住了但没完全锁住的情况
在这里插入图片描述

解决方法:

采用分布式锁

分布式锁

原理:

原理的单机锁,是在一个JVM里面进行锁的监视,分布式就是将监视器变成公用的
在这里插入图片描述

分布式锁的几种实现:

在这里插入图片描述

基于Redis的实现1.0:

在这里插入图片描述

代码:

接口类 ILock

public interface ILock {

  /**
   * @description:尝试获取锁
   * @author:Code-zyc
   * @date: 2022/5/15 9:59
   * @param timeout :给锁设置的超时时间
   * @return boolean : 获取成功返回true
   */
    boolean tryLock(Long timeout);

    /**
     * @description: 释放锁
     * @author:Code-zyc
     * @date: 2022/5/15 10:00
     */

    void unlock();
}

实现类 SimpleRedisLock

public class SimpleRedisLock implements ILock{

    private  static final String LOCK_PROFIX="lock:";

    private String key;
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public boolean tryLock(Long timeout) {
        //获取线程名字,作为值存放
        String name = Thread.currentThread().getName();
        // 采用senet 进行存放数据
        Boolean flag = stringRedisTemplate.opsForValue()
                .setIfAbsent(LOCK_PROFIX + key, name, timeout, TimeUnit.SECONDS);

        return Boolean.TRUE.equals(flag);
    }

    @Override
    public void unlock() {
        stringRedisTemplate.delete(LOCK_PROFIX+key);
    }

    public SimpleRedisLock() {
    }

    public SimpleRedisLock(String key, StringRedisTemplate stringRedisTemplate) {
        this.key = key;
        this.stringRedisTemplate = stringRedisTemplate;
    }
}

逻辑处理类
在原来的基础上,把synchronized锁缓存redis锁

@Autowired
    StringRedisTemplate redisTemplate;
    @Transactional
    public  Result createVoucherOvder(Long voucherId){
        //一人一单子
        Long userid = UserHolder.getUser().getId();

        SimpleRedisLock lock=new SimpleRedisLock("order"+userid,redisTemplate);

        if (!lock.tryLock(1200L)){
            //获取失败 直接返回错误或者重写尝试
            return  Result.fail("不能重复下单");
        }
        //下面部分用try包裹起来 最后进行 锁的释放

        try {
            //判断是否下单了
            Integer count = query().eq("user_id", userid)
                    .eq("voucher_id", voucherId)
                    .count();
            if (count>0){
                return  Result.fail("已经购买过了!");
            }

            //4: 库存减一
            boolean update = seckillVoucherService.update()
                    .setSql("stock=stock-1")
                    .eq("voucher_id", voucherId)
                    .gt("stock", 0) //大于0即可
                    .update();
            if (!update){
                return Result.fail("库存不足 插入时失败");
            }
            //5:创建订单
            VoucherOrder voucherOrder=new VoucherOrder();
            //给订单id赋值
            voucherOrder.setId(redisIDWork.nextID("skillVouCher"));
            //优惠卷的id 赋值
            voucherOrder.setVoucherId(voucherId);
            //用户的id  利用拦截器获取到
            voucherOrder.setUserId(userid);
            //保存到数据库
            save(voucherOrder);
            //返回订单 的id
            return Result.ok(voucherOrder.getId());
        } finally {
            lock.unlock();
        }

    }
结果:

非常成功,在分布式的情况下,没有出现 重复购买的情况。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

Redis实现2.0

1.0的毛病

1.0可能也会产生问题:
在这里插入图片描述
上面这张图片主要就是表达,由于业务阻塞存在,线程1的到期了还没执行完过期了,线程2 来上锁成功了 但是线程1又执行完 来释放锁了出现了恶性循环,导致都能执行了

解决方案

就是 释放锁之前 看看是不是自己的锁,是在进行释放
由于是分布式的,所以不能依靠线程id进行判断,因为每一个JVM是各自维护id的。可以采用UUID+线程ID实现全局唯一性。

流程:

在这里插入图片描述

代码

只需要修改 unlock即可

public class SimpleRedisLock implements ILock{

    private  static final String LOCK_PROFIX="lock:";
    private  static final String VALUE_PROFIX= UUID.randomUUID().toString();

    private String key;
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public boolean tryLock(Long timeout) {
        //创建对应的全局的id
        String value = VALUE_PROFIX+ Thread.currentThread().getId();
        // 采用senet 进行存放数据
        Boolean flag = stringRedisTemplate.opsForValue()
                .setIfAbsent(LOCK_PROFIX + key, value, timeout, TimeUnit.SECONDS);

        return Boolean.TRUE.equals(flag);
    }

    @Override
    public void unlock() {
        //首先获取到 正确的value值
        String value = VALUE_PROFIX+ Thread.currentThread().getId();
        //从缓存中获取到
        String redisvalue = stringRedisTemplate.opsForValue().get(LOCK_PROFIX + key);
        if (value.equals(redisvalue)) {
            //相同表示同一个锁 可以进行释放
            stringRedisTemplate.delete(LOCK_PROFIX+key);
        }
    }

    public SimpleRedisLock() {
    }

    public SimpleRedisLock(String key, StringRedisTemplate stringRedisTemplate) {
        this.key = key;
        this.stringRedisTemplate = stringRedisTemplate;
    }
}
结果

结果成功的略。

Redis实现3.0

2.0的毛病:

依旧是阻塞导致的问题,我们虽然进行判断但是在进行判断的时候,有可能出现判断了 但是要去释放锁的时候阻塞了,这时候别人乘虚而入,导致问题的发生:
在这里插入图片描述

解决方案Lua:

实现检测了上锁的原子性,采用Lua脚本进行实现

代码

Lua脚本代码:
在这里插入图片描述
逻辑代码:
在这里插入图片描述
在这里插入图片描述

Redis锁总结:

在这里插入图片描述

Redission

功能介绍官网地址
Git地址

前期准备:

导入依赖:

<dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.13.6</version>
        </dependency>

写配置类: 或是写在yaml里面

@Component
public class RedissonConfig {
    @Bean
    public RedissonClient redissonClient(){
        Config config=new Config();
        //设值链接地址和密码
        config.useSingleServer().setAddress("redis://192.168.254.138:6379").setPassword("1234");
        return Redisson.create(config);
    }

}

代码

这个相比于前面的版本 只需要修改这几个部分即可:
在这里插入图片描述
tryLock 的构造函数:
在这里插入图片描述
**如果是无参的话:**就是非阻塞的不回去重新获取
在这里插入图片描述
有参的
在这里插入图片描述

自定义的缺陷:

在这里插入图片描述

1:不可重入

就是 同一个锁一个方法调用另一个的时候需要进行获取,但是方法1已经拿到锁了还没释放,所以产生了死锁的情况。
在这里插入图片描述
解决方案 采用记录的方式 先通过唯一标识看看是不是你在调用,是的话获取次数+1 直到为0 才释放锁。
在这里插入图片描述
由于这一些列都必须是 原子性的 所以采用Lua进行编写
获取锁的Lua脚本:
在这里插入图片描述
释放锁的Lua脚本:
在这里插入图片描述
redisson底层也是采用的Lua脚本进行的编辑:
在这里插入图片描述

2,3:

在这里插入图片描述

4:主从一致

进行传输的时候,突然主机挂掉,还未与从机之间进行数据的交流。
在这里插入图片描述
解决方案:
多个主机节点,每一次要设置锁都向这几个进行设置,只有获取到的数量等于了主机的数量,才能算是获取到了锁。
在这里插入图片描述
采用的是redisson自带的: Multilock

补充

关于缓存的过期时间 默认是 30s
在这里插入图片描述
你也可以自己设置:
在这里插入图片描述

Redisson 分布式锁原理

在这里插入图片描述

三种锁的选取总结:

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值