redis杂谈

使用缓存,可以 提升应用程序性能提高读取吞吐量(IOPS)消除数据库热点可预测的性能减少后端负载降低数据库成本

Redis 相关概念

1、缓存穿透

​ 缓存穿透是指查询一个根本不存在的数据, 缓存层和存储层都不会命中, 通常出于容错的考虑, 如果从存储层查不到数据则不写入缓存层。

问题:缓存穿透将导致不存在的数据每次请求都要到存储层去查询, 失去了缓存保护后端存储的意义。

造成缓存穿透的基本原因有两个:

  • 自身业务代码或者数据出现问题。

  • 一些恶意攻击、 爬虫等造成大量空命中。

    ####      解决方案
    
  • 缓存空对象:当没有命中缓存,从数据库中查询数据为空后则缓存空对象,注意为了避免redis内存缓存空对象的浪费,需要为该空对象设置过期时间(过期时间能一定程度上解决频繁地用不存在的数据的Key来进行请求)。

  • 布隆过滤器:、某个值存在时,这个值可能不存在;当不存在时,那就肯定不存在,布隆过滤器解决的问题是:如何准确快速的判断某个数据是否在大数据量集合中

2、缓存失效(击穿)

​ 由于大批量缓存在同一时间失效可能导致大量请求同时穿透缓存直达数据库,可能会造成数据库瞬间压力过大甚至挂掉。

解决方案

  • 数据设置不同的缓存时间()
  • 根据不同的缓存使用次数延长其过期时间(具体的实现呢?)

3、缓存雪崩

缓存雪崩:就是redis缓存直接挂掉了,请求穿过缓存直接到达数据库,最终导致数据库宕机,服务不可用

解决方案

  • 保证缓存层服务高可用性,比如使用Redis Sentinel或Redis Cluster。

  • 依赖隔离组件为后端限流熔断并降级。比如使用Sentinel或Hystrix限流降级组件。

4、数据一致性

  • [1.方式一:先更新数据库,再更新缓存场景]

    并发访问会出现数据不一致的问题

  • [2.方式二:先更新缓存,再更新数据库场景]

​ 同方式一,并发访问出现数据不一致

  • [3.方式三:先删除缓存,再更新数据库的场景]

    ​ 同方式一,并发访问出现数据不一致

  • [4.方式四:先更新数据库,在删除缓存场景]

    并发访问可能会短暂出现数据不一致情况,但最终都会一致。推荐

  • [5.方式五:最佳实现,数据异步同步]

    canal:基于数据库增量日志解析,提供增量数据订阅和消费

    mysql会将操作记录在Binary log日志中,通过canal去监听数据库日志二进制文件,解析log日志,同步到redis中进行增删改操作。

    canal的工作原理:canal 模拟 MySQL slave 的交互协议,伪装自己为 MySQL slave ,向 MySQL master 发送dump 协议;MySQL master 收到 dump 请求,开始推送 binary log 给 slave (即 canal );canal 解析 binary log 对象(原始为 byte 流)。

5、缓存过期淘汰策略

1. Redis缓存淘汰策略工作流程

  • 首先,客户端会发起需要更多内存的申请;
  • 其次,Redis检查内存使用情况,如果实际使用内存已经超出maxmemory,Redis就会根据用户配置的淘汰策略选出无用的key;
  • 最后,确认选中数据没有问题,成功执行淘汰任务。

2. Redis3.0版本支持淘汰策略有6种

  • no-eviction:当内存不足以容纳新写入数据时,新写入操作会报错。
  • allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。
  • allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。
  • volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。
  • volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。
  • volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。

7、redis应用场景

(1)热点数据的缓存

​ 这个应用场景我们比较常见的使用方式,为了降低对数据库的访问,会将对应数据添加到缓存中,提供并发访问的能力,从而提高系统吞吐量。

(2)限时业务的运用

  • 验证码,二维码生存周期

    手机号(唯一标识) 生成的验证码、二维码信息保存在redis中指定过期时间(如果用户输入后 redis中的验证码过期 需要重新输入),在一定时间内如果redis有信息,用户频繁获取则将该信息直接返回并不调用真实获取验证码,二维码的接口。

  • 接口api防刷,订单重复提交问题

    ip+api接口 作为key存储并设置过期时间,value为请求次数 如果请求次数到达阈值则禁止请求。

    订单重复提交类似

(3)计数器相关问题

​ 文章的点赞数、页面的浏览数、网站的访客数、视频的播放数这些数据增长量很快,一旦数据规模上来后,对 mysql 读写都有很大的压力,这时就要考虑 memcache、redis 进行存储或 cache,同时定时同步到DB层。

(4)排行榜相关问题

​ 对于千万级别的数据、大量并发情况下,基于redis可靠的读写请求以及其zset数据结构 可以考虑使用redis来实现相关排行榜功能。新增数据后并在zset中添加数据(需要考虑排行榜的维度作为score)。

(5)分布式锁

​ redis是一个分布式存储系统,同时其setNx命令是阻塞的(key存在则设置不成功) 可以很好的用来进行分布式的锁操作处理,从而实现 秒杀、模拟抢单、抢红包等关键资源的并发场景下的有序访问。

(6)延时操作

  • 订单超过 30 分钟未支付,则自动取消。
  • 外卖商家超时未接单,则自动取消。
  • 医生抢单电话点诊,超过 30 分钟未打电话,则自动退款

针对如上场景 :我们可以使用 zset(sortedset)这个命令,用设置好的时间戳作为score进行排序,使用 zadd score1 value1 …命令就可以一直往内存中生产消息。再利用 zrangebysocre 查询符合条件的所有待处理的任务,通过循环执行队列任务即可。也可以通过 zrangebyscore key min max withscores limit 0 1 查询最早的一条任务,来进行消费。

(7) 队列

​ redis支持list数据结果 使用LPUSH 和RPUSH、LPOP和RPOP可以很轻松的实现栈、队列等数据结构

(8) 分布式应用session(redis实现)

​ redis是分布式存储且支持读写很快,所有用户登录后的相关用户信息可以保存到redis中便于在后续分布式应用中进行使用。

8、redis高级用法

创建redis连接

@BeforeEach
public  void createMasterSlaveClient(){
    JedisPoolConfig config = new JedisPoolConfig();
    config.setMaxTotal(20);
    config.setMaxIdle(10);
    config.setMinIdle(5);
    //timeout,这里既是连接超时又是读写超时,从Jedis 2.8开始有区分connectionTimeout和soTimeout的构造函数
    jedisPool = new JedisPool(config, "IP", port,3000, "password");
}

Pipeline管道的使用

  • 首先Redis的管道(pipeline)并不是Redis服务端提供的功能,而是Redis客户端为了减少网络交互而提供的一种功能。

  • pipeline主要就是将多个请求合并,进行一次提交给Redis服务器,Redis服务器将所有请求处理完成之后,再一次性返回给客户端。

  • pipeline执行的操作,和mget,mset,hmget这样的操作不同,pipeline的操作是不具备原子性的。还有在集群模式下因为数据是被分散在不同的slot里面的,因此在进行批量操作的时候,不能保证操作的数据都在同一台服务器的slot上,所以集群模式下是禁止执行像mget、mset、pipeline等批量操作的,如果非要使用批量操作,需要自己维护key与slot的关系。

  • pipeline也不能保证批量操作中有命令执行失败了而中断,也不能让下一个指令依赖上一个指令, 如果非要这样的复杂逻辑,建议使用lua脚本来完成操作。

@Test
public void testPipline(){
    Jedis client = jedisPool.getResource();
    Pipeline pipeline = client.pipelined();
    Map<String,Response> responseMap = new HashMap<>();
    //字符串操作
    responseMap.put("name", pipeline.set("name","张三"));
    responseMap.put("age",pipeline.set("age","28"));
    //list列表操作
    pipeline.lpush("worker","张三","李四","王五","赵六");
    //map集合操作
    Map<String,String> bookMap = new HashMap<>();
    bookMap.put("web","web技术书籍");
    bookMap.put("java","java技术数据");
    bookMap.put("h5","h5页面学习");
    responseMap.put("bookInfo",pipeline.hset("bookInfo", bookMap));
    //set操作
    responseMap.put("score",pipeline.sadd("score","1","2","3","4"));
    //sort Set集合操作
    responseMap.put("rankList",pipeline.zadd("rankList",100,"张三"));
    responseMap.put("rankList1",pipeline.zadd("rankList",99,"李四"));
    responseMap.put("rankList2",pipeline.zadd("rankList",98,"王五"));
    //将多个操作放到一起 批量执行 减少网络带宽的使用次数
    pipeline.sync();
    for (Map.Entry<String,Response> entry: responseMap.entrySet()) {
        System.out.println(entry.getKey()+":"+entry.getValue().get());
    }
}

位图bitmap使用

  • redis的bitMap是使用一个bit来表示某个状态,通常用于统计实时用户登录活跃用户数用户签到用户在线状态统计活跃用户各种状态值自定义布隆过滤器点赞功能

    据悉 统计一亿用户实时登录个数 使用内存为11.9M

使用位图主要是考虑其如下优点

  1. 可以的读写性能
  2. 节省内存 一个bit记录一条记录状态,海量数据记录
  3. 同时提供与、或、非等位操作
@Test
public void testRedisBitMap(){
    //模拟1千万用户 登录情况  userId为offset(如果用户没有唯一标识则需要逻辑映射为Integer)
    Jedis client = jedisPool.getResource();
    //设置用户userId登录
    Integer userId = 100;
    client.setbit("userLogin",userId,true);
    //获取用户100的登录状态
    Boolean loginStatus = client.getbit("userLogin", userId);
    System.out.println("用户"+ userId +"登录状态:"+loginStatus);
    //获取用户10的登录状态 未登录
    userId = 10;
    loginStatus = client.getbit("userLogin", userId);
    System.out.println("用户"+ userId +"登录状态:"+loginStatus);
    //统计记录总数
    Long userLoginCount = client.bitcount("userLogin");
    System.out.println("用户登录总数:"+userLoginCount);
}

Redis分布式锁

redis能用的的加锁命令分表是INCRSETNXSET

1、INCR锁

这种加锁的思路是, key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCR 操作进行加一。
然后其它用户在执行 INCR 操作进行加一时,如果返回的数大于 1 ,说明这个锁正在被使用当中。例子如下:

redis实现的IncrLock锁

  class IncrLock {
        //加锁
        public boolean trylock(String key,Long lockTime) {
            Jedis client = jedisPool.getResource();
            Long incrLock = client.incr(key);
            if(incrLock <=1){
                //加锁成功
                System.out.println("incr 加锁成功");
                //设置过期时间
                client.expire("incrLock",lockTime);
                return true;
            }else{
                System.out.println("incr 加锁失败");
                return false;
            }
        }
        //解锁
        public void unLock(String key) {
            Jedis client = jedisPool.getResource();
             if(client.exists(key)){
                 client.del(key);
             }
        }
    }

模拟线程

class IncrTask implements Runnable {
    @Override
    public void run() {
        IncrLock incrLock = new IncrLock();
        String key = "incrLock";
        try{
            //获取锁
            boolean incrLockFlag = incrLock.trylock(key,60L);
            if(incrLockFlag){
                //锁资源获取成功后的相关操作
                System.out.println("模拟锁操作");
                Thread.sleep(50*1000);
            }
        }catch (Exception e){
            System.out.println("异常处理");
        }finally {
            incrLock.unLock(key);
        }
    }
}

测试

@Test
public void testRedisIncrLock(){
    ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(10,10,100L, TimeUnit.SECONDS
    ,new ArrayBlockingQueue<>(20));
    for(int i = 0; i<10;i++){
        poolExecutor.submit(new IncrTask());
    }
    try {
        Thread.sleep(6000L);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}
2、setNx锁

在指定的 key 不存在时,为 key 设置指定的值设置成功,返回 1 。 设置失败,返回 0,key的测试编写和ince一致,这里只提供加解锁代码

class SetNxLock {
    //加锁
    public String trylock(String key,Long lockTime) {
        Jedis client = jedisPool.getResource();
        String value = UUID.randomUUID().toString();
        Long setNxLock = client.setnx(key,value);
        if(setNxLock == 1){
            //加锁成功
            System.out.println("setNx 加锁成功");
            //设置过期时间
            client.expire(key,lockTime);
            return value;
        }else{
            System.out.println("setNx 加锁失败");
            return null;
        }
    }
    //解锁
    public void unLock(String key,String value) {
        Jedis client = jedisPool.getResource();
        if(value.equals(client.get(key))){
            client.del(key);
        }
    }
}

3、set锁

​ 上述的两个锁获取成功后会设置锁过期时间(防止程序异常退出使锁无法释放导致后续的操作无法再获取到锁),但是获取锁和设置过期时间会两者组合会破坏其原子性,需要通过事务来确保原子性,但是还是有些问题,所以SET命令本身已经从版本 2.6.12 开始包含了不存在设置nx和设置过期时间的功能。如下为demo实现:

//redis的set锁操作
class SetLock {
    //加锁
    public String trylock(String key,Long lockTime) {
        Jedis client = jedisPool.getResource();
        String value = UUID.randomUUID().toString();
        SetParams params = new SetParams();
        //设置nx等同于setNx()方法 同时设置ex添加锁过期时间
        params.nx().ex(lockTime);
        String setLock = client.set(key, value, params);
        if(StringUtils.isNotBlank(setLock)){
            //加锁成功
            System.out.println("set 加锁成功");
            //设置过期时间
            client.expire(key,lockTime);
            return value;
        }else{
            System.out.println("set 加锁失败");
            return null;
        }
    }
    //解锁
    public void unLock(String key,String value) {
        Jedis client = jedisPool.getResource();
        if(value.equals(client.get(key))){
            client.del(key);
        }
    }

在上述实现中我们只是简单的对redis锁进行了实现,但是上述代码并非标准的实现方式,上述实现有一些问题,我们来一一解决

  1. incr和setnx实现的获取锁后设置过期实现不是原子操作,需要使用事务或者lua脚本保证原子性
  2. 多线程模拟中只有一个线程锁获取成功,其他线程获取锁失败了要怎么办?中断请求还是循环请求
  3. 循环请求的话,如果有一个获取了锁,其它的在去获取锁的时候,是不是容易发生抢锁的可能?
  4. 锁提前过期后,客户端A还没执行完,然后客户端B获取到了锁怎么办
  5. redis某个节点宕机导致,导致在该节点获取锁的服务A失效,同时服务B获取到了锁

Redis事务

redis事务提供了一种“将多个命令打包, 然后一次性、按顺序地执行”的机制, 并且事务在执行的期间不会主动中断 —— 服务器在执行完事务中的所有命令之后, 才会继续处理其他客户端的其他命令。

若在事务队列中存在语法性错误,则执行EXEC命令时,其他正确命令会被执行,错误命令抛出异常(redis不能保证事务的原子性)。

 //解锁
        public boolean unLock(String key,String value) {
            Jedis client = jedisPool.getResource();
            //监听key 事务执行前key被其他命令修改则事务会被打断
            client.watch(key);
            //开启事务
            Transaction transaction = client.multi();
            try{
                //判断是否为自己的锁才进行释放
                if(value.equals(transaction.get(key))){
                    transaction.del(key);
                }

                //执行事务
                List<Object> results = transaction.exec();
                if (results == null) {
                    return false;
                }

            }catch (Exception e){
                //事务回滚
                transaction.discard();
            }
            client.unwatch();
            return  true;
        }

redis使用lua脚本

Lua 是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。

LUA脚本的融合将使Redis数据库产生更多的使用场景,迸发更多新的优势:

  • **高效性:**减少网络开销及时延,多次redis服务器网络请求的操作,使用LUA脚本可以用一个请求完成
  • **数据可靠性:**Redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。
  • **复用性:**LUA脚本执行后会永久存储在Redis服务器端,其他客户端可以直接复用

所以通常情况下redis+lua用来解决redis事务不能解决保证的原子性操作(redis事务本身是有问题的

    class LuaLock {
        // 加锁脚本
        private static final String SCRIPT_LOCK = "if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then redis.call('expire', KEYS[1], ARGV[2]) return 1 else return 2 end";
        // 解锁脚本
        private static final String SCRIPT_UNLOCK = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        private static final String SCRIPT_TEMP = "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}";


        // 加锁脚本sha1值
//        private static final String SCRIPT_LOCK_SHA1 = Sha1Util.encrypt(SCRIPT_LOCK);
        // 解锁脚本sha1值
//        private static final String SCRIPT_UNLOCK_SHA1 = Sha1Util.encrypt(SCRIPT_UNLOCK);

        //加锁
        public String trylock(String key,Long lockTime) {
            Jedis client = jedisPool.getResource();
            String value = UUID.randomUUID().toString();
            //判断lua脚本是否存在,不存在加载,存在使用
//            if(client.scriptExists(SCRIPT_LOCK_SHA1)){
//                client.evalsha(SCRIPT_LOCK_SHA1);
//            }
//            Long result = (Long) client.eval(SCRIPT_LOCK,2,key,value,key,"60000");
            Long result = (Long) client.eval(LuaLock.SCRIPT_LOCK, Lists.newArrayList("1"),Lists.newArrayList(key,"40000"));

            System.out.println("lua 脚本加锁 "+result);
            if(result == 0){
                return null;
            }
            return value;
        }
        //解锁
        public void unLock(String key,String value) {
            Jedis client = jedisPool.getResource();
            Object result = client.eval(SCRIPT_UNLOCK, Lists.newArrayList(key), Lists.newArrayList(value));
            System.out.println("lua 脚本解锁 "+result);
        }


    }

发布/订阅

序号 命令及描述
1 [PSUBSCRIBE pattern [pattern …]] 订阅一个或多个符合给定模式的频道。
2 [PUBSUB subcommand [argument [argument …]]] 查看订阅与发布系统状态。
3 [PUBLISH channel message] 将信息发送到指定的频道。
4 [PUNSUBSCRIBE [pattern [pattern …]]] 退订所有给定模式的频道。
5 [SUBSCRIBE channel [channel …]] 订阅给定的一个或多个频道的信息。
6 [UNSUBSCRIBE [channel [channel …]]] 指退订给定的频道。

redis杂谈_第1张图片

package com.xiu.redis.study;

import redis.clients.jedis.JedisPubSub;

/**
 * redis发布订阅消息监听器
 * @ClassName: RedisMsgPubSubListener
 * @Description: TODO
 * @author OnlyMate
 * @Date 2018年8月22日 上午10:05:35
 *
 */
public class RedisMsgPubSubListener extends JedisPubSub {

    @Override
    public void unsubscribe() {
        super.unsubscribe();
    }

    @Override
    public void unsubscribe(String... channels) {
        super.unsubscribe(channels);
    }

    @Override
    public void subscribe(String... channels) {
        super.subscribe(channels);
    }

    @Override
    public void psubscribe(String... patterns) {
        super.psubscribe(patterns);
    }

    @Override
    public void punsubscribe() {
        super.punsubscribe();
    }

    @Override
    public void punsubscribe(String... patterns) {
        super.punsubscribe(patterns);
    }

    @Override
    public void onMessage(String channel, String message) {
        System.out.println("onMessage: channel["+channel+"], message["+message+"]");
    }

    @Override
    public void onPMessage(String pattern, String channel, String message) {
        System.out.println("onPMessage: pattern[{"+pattern+"}], channel[{"+channel+"}]," +
                " message[{"+message+"}]");
    }

    @Override
    public void onSubscribe(String channel, int subscribedChannels) {
        System.out.println("Subscribe: channel[{"+channel+"}]," +
                " subscribedChannels[{"+subscribedChannels+"}]");
    }

    @Override
    public void onPUnsubscribe(String pattern, int subscribedChannels) {
        System.out.println("onPUnsubscribe: pattern[{"+pattern+"}], " +
                "subscribedChannels[{"+subscribedChannels+"}]");
    }

    @Override
    public void onPSubscribe(String pattern, int subscribedChannels) {
        System.out.println("onPSubscribe: pattern[{"+pattern+"}]," +
                " subscribedChannels[{"+subscribedChannels+"}]");
    }

    @Override
    public void onUnsubscribe(String channel, int subscribedChannels) {
        System.out.println("channel:{"+channel+"} is been subscribed:{"+channel+"}");
    }
}
@Test
public void testPublishAndSubscrible() throws InterruptedException {
    ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(10,10,100L, TimeUnit.SECONDS
            ,new ArrayBlockingQueue<>(20));

    poolExecutor.execute(new SubscribleTask());

    Jedis client = jedisPool.getResource();
    //发布消息
    client.publish("msg","hello grils");
    Thread.sleep(10000L);

}
class SubscribleTask implements Runnable{
    @Override
    public void run() {

        Jedis client = jedisPool.getResource();

        //订阅消息
        client.subscribe(new RedisMsgPubSubListener(),"msg");

    }
}

在这里插入图片描述

你可能感兴趣的:(redis,缓存,数据库)