redis(五)

1、高并发下火车买票问题的分析

代码1:

public void produceStock1(){
        //1.在redis中把票数存好
        //2.从redis中获取票数
        int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("trainTickets"));
        //判断票数是否大于0
        if(stock>0){
            //票数减一
            int rStock = stock - 1;
            //将当前票数存进redis
            stringRedisTemplate.opsForValue().set("trainTickets",String.valueOf(rStock));
            logger.info("扣减库存成功。。剩余库存"+rStock);
        }else{  //说明票数不足
            logger.info("库存扣减失败,库存不足");
        }
    }

 分析:这里简单模拟了一下火车买票的场景,当程序执行时,库存会相应减一。但是明显会出现超卖现象,前面的人库存还没来得及减一,后面的人已经开始买了。解决办法如下:

public void produceStock1(){
        //1.在redis中把票数存好
        //2.从redis中获取票数
        int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("trainTickets"));
        //判断票数是否大于0
        if(stock>0){
            //票数减一
            int rStock = stock - 1;
            //将当前票数存进redis
            stringRedisTemplate.opsForValue().set("trainTickets",String.valueOf(rStock));
            logger.info("扣减库存成功。。剩余库存"+rStock);
        }else{  //说明票数不足
            logger.info("库存扣减失败,库存不足");
        }
    }

synchronized在单体应用上或许没问题,但是在分布式上面却锁不住

public synchronized String produceStock3(){
        //设置一个字符串
        String lock = "lock";
        Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(lock, "");
        if(!aBoolean){
            return "排队人数过多,请稍后重试";
        }
        //1.在redis中把票数存好
        //2.从redis中获取票数
        int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("trainTickets"));
        //判断票数是否大于0
        if(stock>0){
            //票数减一
            int rStock = stock - 1;
            //将当前票数存进redis
            stringRedisTemplate.opsForValue().set("trainTickets",String.valueOf(rStock));
            logger.info("扣减库存成功。。剩余库存"+rStock);
        }else{  //说明票数不足
            logger.info("库存扣减失败,库存不足");
        }
        return "买票成功";
    }

这里又有另外一个问题,当第一个人执行完的时候,后面的人都执行不了,所以前面的人执行完之后一定要把lock删除,后面的人才可以执行

public synchronized String produceStock4(){
        //设置一个字符串
        String lock = "lock";

        try {
            Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(lock, "");
            if(!aBoolean){
                return "排队人数过多,请稍后重试";
            }
            //1.在redis中把票数存好
            //2.从redis中获取票数
            int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("trainTickets"));
            //判断票数是否大于0
            if(stock>0){
                //票数减一
                int rStock = stock - 1;
                //将当前票数存进redis
                stringRedisTemplate.opsForValue().set("trainTickets",String.valueOf(rStock));
                logger.info("扣减库存成功。。剩余库存"+rStock);
            }else{  //说明票数不足
                logger.info("库存扣减失败,库存不足");
            }
        } finally {
        	//执行完把key删除
            stringRedisTemplate.delete(lock);
        }
        return "买票成功";
    }

如果第一个人在执行到一半的时候服务器宕机了,lock还没来得及删除,就发生了死锁问题,所以可以给lock设置一个过期时间

public synchronized String produceStock5(){
        //设置一个字符串
        String lock = "lock";

        try {
            Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(lock, "");
            //给key设置过期时间
            stringRedisTemplate.expire(lock,30, TimeUnit.SECONDS);
            if(!aBoolean){
                return "排队人数过多,请稍后重试";
            }
            //1.在redis中把票数存好
            //2.从redis中获取票数
            int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("trainTickets"));
            //判断票数是否大于0
            if(stock>0){
                //票数减一
                int rStock = stock - 1;
                //将当前票数存进redis
                stringRedisTemplate.opsForValue().set("trainTickets",String.valueOf(rStock));
                logger.info("扣减库存成功。。剩余库存"+rStock);
            }else{  //说明票数不足
                logger.info("库存扣减失败,库存不足");
            }
        } finally {
        	//执行完把key删除
            stringRedisTemplate.delete(lock);
        }
        return "买票成功";
    }

我们可以将.setIfAbent和expire方法写出一条语句使其具有原子性

public synchronized String produceStock6(){
        //设置一个字符串
        String lock = "lock";

        try {
            //Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(lock, "");
            //给key设置过期时间
            //stringRedisTemplate.expire(lock,30, TimeUnit.SECONDS);
            Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(lock,"", 30, TimeUnit.SECONDS);
            if(!aBoolean){
                return "排队人数过多,请稍后重试";
            }
            //1.在redis中把票数存好
            //2.从redis中获取票数
            int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("trainTickets"));
            //判断票数是否大于0
            if(stock>0){
                //票数减一
                int rStock = stock - 1;
                //将当前票数存进redis
                stringRedisTemplate.opsForValue().set("trainTickets",String.valueOf(rStock));
                logger.info("扣减库存成功。。剩余库存"+rStock);
            }else{  //说明票数不足
                logger.info("库存扣减失败,库存不足");
            }
        } finally {
            stringRedisTemplate.delete(lock);
        }
        return "买票成功";
    }

当第一个线程还没执行完的时候已经超过过期时间,这是时候lock已经被过期了,等第一个线程执行完删除的却是第二个的锁,所以每次执行的时候给每个线程生成一个唯一值,每次删除的时候再判断一下

public synchronized String produceStock7(){
        //设置一个字符串
        String lock = "lock";
        //生成id
        String value = UUID.randomUUID().toString();
        try {
            //Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(lock, "");
            //给key设置过期时间
            //stringRedisTemplate.expire(lock,30, TimeUnit.SECONDS);
            Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(lock,value, 30, TimeUnit.SECONDS);
            if(!aBoolean){
                return "排队人数过多,请稍后重试";
            }
            //1.在redis中把票数存好
            //2.从redis中获取票数
            int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("trainTickets"));
            //判断票数是否大于0
            if(stock>0){
                //票数减一
                int rStock = stock - 1;
                //将当前票数存进redis
                stringRedisTemplate.opsForValue().set("trainTickets",String.valueOf(rStock));
                logger.info("扣减库存成功。。剩余库存"+rStock);
            }else{  //说明票数不足
                logger.info("库存扣减失败,库存不足");
            }
        } finally {
            //判断一下值是否相等
            if(value.equals(stringRedisTemplate.opsForValue().get(lock))){
                stringRedisTemplate.delete(lock);
            }
        }
        return "买票成功";
    }

当程序还没执行完,已经超过过期时间,这里时间设置有问题,所以我们可以使用守护线程续命

public synchronized String produceStock8(){
        //设置一个字符串
        String lock = "lock";
        //生成id
        String value = UUID.randomUUID().toString();
        try {
            //Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(lock, "");
            //给key设置过期时间
            //stringRedisTemplate.expire(lock,30, TimeUnit.SECONDS);
            Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(lock,value, 30, TimeUnit.SECONDS);
            if(!aBoolean){
                return "排队人数过多,请稍后重试";
            }

            //开一个守护线程,每隔过期时间的三分之一就进行续命
            MyThread myThread = new MyThread(lock);
            myThread.setDaemon(true);
            myThread.start();
            //1.在redis中把票数存好
            //2.从redis中获取票数
            int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("trainTickets"));
            //判断票数是否大于0
            if(stock>0){
                //票数减一
                int rStock = stock - 1;
                //将当前票数存进redis
                stringRedisTemplate.opsForValue().set("trainTickets",String.valueOf(rStock));
                logger.info("扣减库存成功。。剩余库存"+rStock);
            }else{  //说明票数不足
                logger.info("库存扣减失败,库存不足");
            }
        } finally {
            //判断一下值是否相等
            if(value.equals(stringRedisTemplate.opsForValue().get(lock))){
                stringRedisTemplate.delete(lock);
            }
        }
        return "买票成功";
    }


    class MyThread extends Thread{
        String lock;
        public MyThread(String lock){
            this.lock = lock;
        }

        @Override
        public void run() {
            while (true){
                try {
                    Thread.sleep(10000);
                } catch (Exception e){

                }
                //假设线程还活着,那么说明需要续命
                stringRedisTemplate.expire(lock,30,TimeUnit.SECONDS);
            }
        }
    }

2、基于Redis的分布式锁问题Redisson的简单使用

这个框架就解决了分布式锁的问题
1.导包

<!--导入redssion依赖-->
<dependency>
     <groupId>org.redisson</groupId>
     <artifactId>redisson</artifactId>
     <version>3.11.0</version>
 </dependency>

2.编写配置文件

@SpringBootConfiguration
public class RedisConfig {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Bean
    public RedissonClient redissonClient(){
        RedissonClient redissonClient = null;
        //获取config的实例
        Config config = new Config();
        //设置请求的url地址
        String url = "redis://114.55.219.117:6379";
        //设置config
        config.useSingleServer().setAddress(url);
        //通过Redisson创建一个客户端对象
        try {
            redissonClient = Redisson.create(config);
            logger.info("创建RedissonClient成功");
            return redissonClient;
        } catch (Exception e){
            logger.info("创建RedissonClient失败");
            return null;
        }
    }
}

3.编写lock的类

@Component
public class DistributeRedisLock {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private RedissonClient redissonClient;

    /**
     * 加锁
     * @return
     */
    public boolean lock(String lockName){
        try {
            if(null==redissonClient){
                logger.info("注入redissonClient对象失败");
                return false;
            }
            //获取这个锁
            RLock lock = redissonClient.getLock(lockName);
            lock.lock(30, TimeUnit.SECONDS);
            logger.info("加锁成功");
            return true;
        } catch (Exception e) {
            logger.info("出现异常加锁失败");
            return false;
        }
    }

    /**
     * 释放锁
     * @return
     */
    public boolean unlock(String lockName){
        try {
            if(null==redissonClient){
                logger.info("释放锁失败"+lockName);
                return false;
            }
            //获取这个锁
            RLock lock = redissonClient.getLock(lockName);
            if(null!=lock){
                lock.unlock();
                logger.info("释放锁成功");
                return true;
            }
            return false;
        } catch (Exception e) {
            logger.info("释放锁失败");
            return false;
        }
    }
}

4.调用

public synchronized String produceStockRedisson(){
        String lock = "lock";
        try {
            boolean lock1 = distributeRedisLock.lock(lock);
            if(lock1){  //说明加锁成功
                int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("trainTickets"));
                //判断票数是否大于0
                if(stock>0){
                    //票数减一
                    int rStock = stock - 1;
                    //将当前票数存进redis
                    stringRedisTemplate.opsForValue().set("trainTickets",String.valueOf(rStock));
                    logger.info("扣减库存成功。。剩余库存"+rStock);
                }else{  //说明票数不足
                    logger.info("库存扣减失败,库存不足");
                }
            }else {
                return "当前排队人数过多";
            }

        } finally {
            distributeRedisLock.unlock(lock);
        }
        return "买票成功";
    }

3、SpringBoot整合下的键值序列化

键值序列化简单来说就是key和valu存储在redis中的形式
自定义序列化器:

public class BoBoSerializer implements RedisSerializer {

    private Class aClass;

    public BoBoSerializer(Class aClass){
        this.aClass = aClass;
    }

    /**
     * 序列化
     * @param o
     * @return
     * @throws SerializationException
     */
    @Override
    public byte[] serialize(Object o) throws SerializationException {
        if(null==o){
            return null;
        }
        //将值转换为json对象
        String s = JSON.toJSONString(o);
        return s.getBytes(Charset.forName("UTF-8"));
    }

    /**
     * 反序列化
     * @param bytes
     * @return
     * @throws SerializationException
     */
    @Override
    public Object deserialize(byte[] bytes) throws SerializationException {
        if(null==bytes){
            return null;
        }
        String result = new String(bytes);
        //将json转换为Java对象
        return JSON.parseObject(result,aClass);
    }
}

2.编写配置文件

@Bean
    public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        //设置连接工厂
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        //设置键的序列化器
        redisTemplate.setStringSerializer(new StringRedisSerializer());
        //设置值的序列化器
        redisTemplate.setValueSerializer(new BoBoSerializer(Object.class));
        return redisTemplate;
    }

测试:

redisTemplate.opsForValue().set("user",new User(1,"xbb","123"));

在这里插入图片描述

4、Redis开发中常见问题

1、Redis的缓存穿透

后台获取数据首先去redis中查询,redis中没有,然后去数据库中查询,发现没有,这样的话,每一个线程进来都会访问数据库,这样数据库就容易崩溃,这种问题就叫缓存穿透
在这里插入图片描述
解决方案:假设第一个线程没找到的时候,将数据库中的值设为"",并且同步到redis中,这样后面的线程访问的时候就不会进入数据库了

2、缓存雪崩

缓存雪崩是指,缓存层出现了错误,不能正常工作了。于是所有的请求都会达到存储层,存储层的调用量会暴增,造成存储层也会挂掉的情况。也就是redis挂了,所有请求都到数据库去了。指在某一个时间段,缓存集中过期失效
解决方案:

  • 在计算出key过期时间之后,动态加上一个范围内的随机数,作为key的最终过期时间,错峰过期
  • 做缓存的高可用
  • 缓存做一个降级处理
  • 做redis的缓存预热
3、redis脑裂

 客户端向主服务器写入了数据 但是主服务器还没有来得及同步的情况下 主服务器死了 那么这个时候就会选举新的主服务器 原来的主服务器在一段时间之后 又好了 那么这个时候 原来的主服务器 只能作为从服务器了 原来主服务器的数据 没有办法进行同步 这种问题 就是redis的脑裂问题
解决方案:

min-slaves-to-write 1:在我们客户端写入数据的时候 至少保证 主服务器上有一个从服务器 处于正常连接才能写入这个数据
min-slaves-max-lag 10 :主从同步的时间 10s
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值