Java语言】redis在项目中的应用?

2021-03-04 10:05发布

8条回答
Sunny
2楼 · 2021-03-04 18:46

使用场景:常规key-value缓存应用。常规计数: 微博数, 粉丝数。

实现方式:String在redis内部存储默认就是一个字符串,被redisObject所引用,当遇到incr,decr等操作时会转成数值型进行计算,此时redisObject的encoding字段为int。


小小收藏家
3楼 · 2021-03-05 13:42

1. Redis的应用场景

  • 缓存-是JavaEE项目中使用最多的功能
    如: 数据查询、短连接、新闻内容、商品内容等

  • 任务队列
    如:秒杀、抢购、12306等等

  • 数据过期处理(精确单位毫秒级)

  • 分布式集群架构中的session分离(等同于nginx负债均衡的(ip_hash)策略,其目的都是解决用户session一致性问题)

实际项目中:对于nginx的ip绑定策略同一用户的访问打到同一个机器上,也是存在风险的,而且也失去了负债均衡的意义。
实际项目中分布式架构中的session共享问题解决如下:
其一:把session放到redis中,中间需要多次访问redis。但是这种方式可以水平扩展(增加redis服务集群)即使服务器重启session也不会丢失,但是需要注意下session在redis中的刷新/失效机制。这种方式不仅可以跨服务器session共享,甚至可以跨平台共享(如网页端和APP端)
其二:无session状态的场景,(api无状态服务)很多接口类中每次接口访问都不依赖于session,不依赖于前一次的接口访问。

  • 网站访问统计

  • 应用排行榜

  • 聊天室的在线好友列表


瑶仙女呀
4楼 · 2021-03-05 16:50

使用场景:常规key-value缓存应用。常规计数: 微博数, 粉丝数。

实现方式:String在redis内部存储默认就是一个字符串,被redisObject所引用,当遇到incr,decr等操作时会转成数值型进行计算,此时redisObject的encoding字段为int。


猿小猿
5楼 · 2021-03-05 17:36

使用场景:常规key-value缓存应用。常规计数: 微博数, 粉丝数。

实现方式:String在redis内部存储默认就是一个字符串,被redisObject所引用,当遇到incr,decr等操作时会转成数值型进行计算,此时redisObject的encoding字段为int。


我的网名不再改
6楼 · 2021-03-05 22:54

1 概述

Redis作为一款性能优异的内存数据库,在互联网公司有着多种应用场景,本文介绍笔者在项目中使用Redis的场景。主要从以下几个方面介绍:


分布式锁

接口限流器

订单缓存

Redis和DB数据一致性处理

防止缓存穿透和雪崩

分布式session共享

2 分布式锁

Redis实现分布式锁


3 接口限流器

Redis实现限流器


4 订单缓存

整个订单的存储结构如下:




使用Redis的zset数据结构存储每个用户的订单,按照下单时间倒序排列,用户唯一标识作为key,用户的订单集合作为value,使用订单创建时间的时间戳+订单号后三位作为分数

为什么不直接使用下单时间的时间戳作为分数?因为下单时间只精确到秒,同一秒可能出现多个订单情况,这样就会出现相同的分数,而加上订单号后三位就能基本上避免这种情景。

只放用户的前N条订单即可,因为很少有用户会查看很久以前的订单,这样做会节省很多空间。如果有用户需要查看前N条之后的订单,再从数据库中查询即可,当然这种概率就比较小了。

5 Redis和DB数据一致性处理

只要有多份数据,就会涉及到数据一致性的问题。Redis和数据库的数据一致性,也是必然要面对的问题。我们这边的订单数据是先更新数据库,数据库更新成功后,再更新缓存,若数据库操作成功,缓存操作失败了,就出现了数据不一致的情况。保证数据一致性我们前后使用过两种方式:


方式一


循环5次更新缓存操作,直到更新成功退出循环,这一步主要能减小由于网络瞬间抖动导致的更新缓存失败的概率,对于缓存接口长时间不可用,靠循环调用更新接口是不能补救接口调用失败的。

如果循环5次还没有更新成功,就通过worker去定时扫描出数据库的数据,去和缓存中的数据进行比较,对缓存中的状态不正确的数据进行纠正。

方式二


跟方式一的第一步操作一样

若循环更新5次仍不成功,则发一个缓存更新失败的mq,通过消费mq去更新缓存,会比通过定时任务扫描更及时,也不会有扫库的耗时操作。此方式也是我们现在使用的方式,下面是示例代码:


for (int i = 0; i < 5>

    try {

        // 入缓存操作

        addOrderListRedis(key, score, orderListVO);

        break;

    } catch (Exception e) {

        log.error("{}IOrderRedisCache.putOrderList2OrderListRedis--->>jdCacheCloud.zAdd exception:", logSid, e);

        if (i == 4) sendUpOrderCacheMQ(orderListVO, logSid); // 如果循环5次,仍添加缓存失败,发送MQ,通过MQ继续更新缓存

    }

}


6 防止缓存穿透和雪崩

缓存为我们挡住了80-90%甚至更多的流量,然而当缓存中的大量热点数据恰巧在差不多的时间过期时,或者当有人恶意伪造一些缓存中根本没有的数据疯狂刷接口时,就会有大量的请求直接穿透缓存访问到数据库(因为查询数据策略是缓存没有命中,就查数据库),给数据库造成巨大压力,甚至使数据库崩溃,这肯定是我们系统不允许出现的情况。我们需要针对这种情况进行处理。下图是处理流程图:



示例代码:


// 代码段1

// 锁的数量 锁的数量越少 每个用户对锁的竞争就越激烈,直接打到数据库的流量就越少,对数据库的保护就越好,如果太小,又会影响系统吞吐量,可根据实际情况调整锁的个数

public static final String[] LOCKS = new String[128];

// 在静态块中将128个锁先初始化出来

static {

    for (int i = 0; i < 128>

        LOCKS[i] = "lock_" + i;

    }

}


// 代码段2

public List getOrderVOList(String userId) {

    List list = null;

    // 1.先判断缓存中是否有这个用户的数据,有就直接从缓存中查询并返回

    if (orderRedisCache.isOrderListExist(userId)) {

        return  getOrderListFromCache(userId); 

    }

    // 2.缓存中没有,就先上锁,锁的粒度是根据用户Id的hashcode和127取模

    String[] locks = OrderRedisKey.LOCKS;

    int index = userId.hashCode() & (locks.length - 1);

    try {

        // 3.此处加锁很有必要,加锁会保证获取同一个用户数据的所有线程中,只有一个线程可以访问数据库,从而起到减小数据库压力的作用

        orderRedisCache.lock(locks[index]);

        // 4.上锁之后再判断缓存是否存在,为了防止再获得锁之前,已经有别的线程将数据加载到缓存,就不允许再查询数据库了。

        if (orderRedisCache.isOrderListExist(userId)) {

            return getOrderListFromCache(userId); 

        }

        // 查询数据库

        list = getOrderListFromDb(userId);

        // 如果数据库没有查询出来数据,则在缓存中放入NULL,标识这个用户真的没有数据,等有新订单入库时,会删掉此标识,并放入订单数据

        if(list == null || list.size() == 0) {

            jdCacheCloud.zAdd(OrderRedisKey.getListKey(userId), 0, null);

        } else {

            jdCacheCloud.zAdd(OrderRedisKey.getListKey(userId), list);

        }

        return list;

    } finally {

        orderRedisCache.unlock(locks[index]);

    }

}


防止穿透和雪崩的关键地方在于使用分布式锁和锁的粒度控制。首先初始化了128(0-127)个锁,然后让所有缓存没命中的用户去竞争这128个锁,得到锁后并且再一次判断缓存中依然没有数据的,才有权利去查询数据库。没有将锁粒度限制到用户级别,是因为如果粒度太小的话,某一个时间点有太多的用户去请求,同样会有很多的请求打到数据库。比如:在时间点T1有10000个用户的缓存数据失效了,恰恰他们又在时间点T1都请求数据,如果锁粒度是用户级别,那么这10000个用户都会有各自的锁,也就意味着他们都可以去访问数据库,同样会对数据库造成巨大压力。而如果是通过用户id去hashcode和127取模,意味着最多会产生128个锁,最多会有128个并发请求访问到数据库,其他的请求会由于没有竞争到锁而阻塞,待第一批获取到锁的线程释放锁之后,剩下的请求再进行竞争锁,但此次竞争到锁的线程,在执行代码段2中第4步时:orderRedisCache.isOrderListExist(userId),缓存中有可能已经有数据了,就不用再查数据库了,依次类推,从而可以挡住很多数据库请求,起到很好的保护数据库的作用。


小张同学
7楼 · 2021-03-22 13:53

一、 redis 特点

    1.所有数据存储在内存中,高速读写

    2.提供丰富多样的数据类型:string、 hash、 set、 sorted set、bitmap、hyperloglog

    3.提供了 AOF 和 RDB 两种数据的持久化保存方式,保证了 Redis 重启后数据不丢失

    4.Redis 的所有操作都是原子性的,还支持对几个操作合并后的原子性操作,支持事务

 通常我们都把数据存到关系型数据库中,但为了提升应用的性能,我们应该把访频率高且不会经常变动的数据缓存到内存中。Redis 没有像 MySQL 这类关系型数据库那样强大的查询功能,需要考虑如何把关系型数据库中的数据,合理的对应到缓存的 key-value 数据结构中。

1)String数据类型的应用场景

    (1) 存储 MySQL 中某个字段的值

    把 key 设计为 表名:主键名:主键值:字段名 eg.

    set user:id:1:email 10000@qq.com

    (2) 存储对象

    string 类型支持任何格式的字符串,应用最多的就是存储 json 或其他对象格式化的字符串。(这种场景下推荐使用 hash 数据类型)

    set user:id:1 '[{"id":1,"name":"zj","email":"10000@qq.com"},{"id":1,"name":"zj","email":"10000@qq.com"}]'

    (3) 生成自增 id

    当 redis 的 string 类型的值为整数形式时,redis 可以把它当做是整数一样进行自增(incr)自减(decr)操作。由于 redis 所有的操作都是原子性的,所以不必担心多客户端连接时可能出现的事务问题。

    incr 对值进行加1操作,如果不是整数,返回错误,如果不存在按照从0开始 decr 同incr,但是是减1操作 incrby,decrby ,增加减去指定的数

    (4) 生成自增 id

    比如视频播放次数,点赞次数。

    (5)共享session

    数据共享的功能,redis作为单独的应用软件用来存储一些共享数据供多个实例访问。

    (6)自动定时过期

    set key value [ex seconds] [px millseconds] [nx|xx]

    ex seconds: 键过期时间

    px milliseconds: 为键设置毫秒级过期时间

    nx: 键必须不存在才可以设置成功,用于添加

    xx: 键必须存在,才可以设置成功,用于更新

    (7)批量操作

    mset,mget

    批量设置和获取命令,在操作多个key的时候可以节省网络传输时间

    mset key value [key value...]

    mget key [key ...]

2)hash 数据类型的应用场景

    hash 类型十分适合存储对象类数据,相对于在 string 中介绍的把对象转化为 json 字符串存储,hash 的结构可以任意添加或删除‘字段名’,更加高效灵活。 一些关系型数据库中不是特别复杂的表,也无需复杂的关系查询,可以使用Redis的Hash来存储,也可以用Hash做表数据缓存。

3)list 数据类型的应用场景

    (1) 消息队列

    redis 的 list 数据类型对于大部分使用者来说,是实现队列服务的最经济,最简单的方式。我司使用redis做消息队列,lpush + brpop或rpop命令,实现先进先出,如果消费失败客户端把key再放回去,消费成功就remove掉。

    (2) “最新内容”

    因为 list 结构的数据查询两端附近的数据性能非常好,所以适合一些需要获取最新数据的场景,比如新闻类应用的 “最近新闻”。

4)set 数据类型的应用场景

    set 类型的特点是——不重复且无序的一组数据,并且具有丰富的计算功能,在一些特定的场景中可以高效的解决一般关系型数据库不方便做的工作。

    (1)“共同好友列表”

    社交类应用中,获取两个人或多个人的共同好友,两个人或多个人共同关注的微博这样类似的功能,用 MySQL 的话操作很复杂,可以把每个人的好友 id 存到集合中,获取共同好友的操作就可以简单到一个取交集的命令就搞定。

   (2)唯一ip

    跟踪一些具有唯一性的一些数据,比如访问某一博客的唯一ip地址的信息,我们仅需要在每次访问的时候,将ip存入redis中。利用服务器端聚合操作方便高效的特性,维护数据对象之间的关联关系。

5)sorted set 数据类型的应用场景

排行榜

    实效性

    从排行榜的实效性上划分,主要分为:    

        实时榜:基于当前一段时间内数据的实时更新,进行排行。例如:当前一小时内游戏热度实时榜,当前一小时内明星送花实时榜等

        历史榜:基于历史一段周期内的数据,进行排行。例如:日榜(今天看昨天的),周榜(上一周的),月榜(上个月的),年榜(上一年的)

    业务数据类型

    从需要排行的数据类型上划分,主要分为:

        单类型数据排行榜:是指需要排行的主体不需要区分类型,例如,所有用户积分排行,所有公贡献值排行,所有游戏热度排行等

        多类型(复合类型)数据排行榜:是指需要排行的主体在排行中要求有类型上的区分,例如:竞技类游戏热度排行、体育类游戏热度排行、MOBA类游戏操作性排行、角色/回合/卡牌三类游戏热度排行等

    展示唯度

    从榜单的最终展示唯度上划分,主要分为:

        单唯度:是指选择展示的排行榜就是基于一个唯度下的排行,例如前面提到的MOBA类游戏操作性排行榜,就仅展示所有MOBA类游戏按操作性的评分排行

        多唯度:是指选择展示的排行榜还有多种唯度供用户选择,仍然以前面的MOBA类游戏为例,唯度除了操作性,还有音效评分排行,难易度评分排行,画面评分排行等。

    展示数据量

     从需要展示的数据量上划分,主要分为:

        topN数据:只要求展示topN条排行纪录,例如:最火MOBA游戏top20

        全量数据:要求展示所有数据的排行,例如:所有用户的积分排行


java-小王子
8楼 · 2021-03-23 19:28
  • 分布式锁

  • 接口限流器

  • 订单缓存

  • Redis和DB数据一致性处理

  • 防止缓存穿透和雪崩

  • 分布式session共享


寂静的枫林
9楼 · 2021-07-26 09:54

常规key-value缓存应用。常规计数: 微博数, 粉丝数。String在redis内部存储默认就是一个字符串,被redisObject所引用,当遇到incr,decr等操作时会转成数值型进行计算,此时redisObject的encoding字段为int。


相关问题推荐

  • 回答 7
    已采纳

    里氏代换原则(Liskov Substitution Principle, LSP):所有引用基类(父类)的地方必须能透明地使用其子类的对象里氏代换原则告诉我们,在软件中将一个基类对象替换成它的子类对象,程序将不会产生任何错误和异常,反过来则不成立,如果一个软件实体使用的是一...

  • 回答 8
    已采纳

    心里有个预期,然后看看是以什么目的进这家企业工作,要是赚钱的话,那就多要点,要是学习的话,可以根据情况要一个能养活自己的价格。

  • 回答 4
    已采纳

    Java中有八种数据类型,基础数据类型分别是:byte,short,int,long,float,double,char,boolean,引用数据类型分别是:数组,类和接口。方法传参的时候我们有两种,一种是形式参数(定义方法时写的参数),一种是实际参数(调用方法时给的具体值)。首先...

  • 回答 15
    已采纳

    现在的架构很多,各种各样的,如高并发架构、异地多活架构、容器化架构、微服务架构、高可用架构、弹性化架构等,还有和这些架构相关的管理型的技术方法,如 DevOps、应用监控、自动化运维、SOA 服务治理、去 IOE 等等,还有很多。分布式架构其实就是分布式系...

  • 回答 10

    1、监控GC的状态使用各种JVM工具,查看当前日志,分析JVM参数的设置,分析堆内存快照和GC日志,根据实际的各区域的内存划分和GC的执行时间,判断是否需要进行优化2、分析结果、判断是否需要优化如果各项参数设置合理,系统没有超时的日志出现,GC频率也不高,...

  • 回答 6

    MyBatis 每次创建结果对象的新实例时,它都会使用一个对象工厂(ObjectFactory)实例来完成。 默认的对象工厂需要做的仅仅是实例化目标类,要么通过默认构造方法,要么在参数映射存在的时候通过参数构造方法来实例化。 如果想覆盖对象工厂的默认行为,则可以...

  • 回答 6

    学vue应该要先学习javascript 的基础知识和用法。

  • 回答 8

    1、lambda是jdk8的新特性2、使用lambda的前提,必须是一个接口,接口只能有一个抽象方法3、Lambda 表达式的简单例子:// 1. 不需要参数,返回值为 5  () -> 5    // 2. 接收一个参数(数字类型),返回其2倍的值  x -> 2 * x    // 3. 接受2个参数(数...

  • 回答 20

    100-199 用于指定客户端应相应的某些动作。 200-299 用于表示请求成功。 300-399 用于已经移动的文件并且常被包含在定位头信息中指定新的地址信息。 400-499 用于指出客户端的错误。 400 语义有误,当前请求无法被服务器理解。 401 当前请求需要用户验证...

  • 回答 5
    已采纳

    1、相同点(1)都是表现层框架,都是基于MVC设计模型(2)底层都离不开 Servlet API(3)处理请求的机制都是一个核心控制器2、不同点(1)SpringMVC的入口是Servlet,而Struts2的入口是Filter(2)SpringMVC是基于方法设计的,而Struts2是基于类(3)SpringMV...

  • 回答 22
    已采纳

    类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结...

  • 回答 23
    已采纳

    (1)idea启动时会有两个快捷方式,安装完后默认生成在桌面的是32位的idea的快捷方式,如果我们使用这个快捷方式运行大项目,一般都会很卡。解决方法是找到idea的安装目录,然后进入bin文件夹,找到名称为idea64的应用程序,右键他生成桌面快捷方式。以后每次...

  • 回答 12
    已采纳

    获取Map集合中所有的key可以通过map集合的keySet()方法获取例如:    Map map = new HashMap();    map.put(xx,xx); //存放数据    //.... 省略    Set set = map.keySet();    //可以通过迭代器进行测试    Iterator iter = set.iter...

没有解决我的问题,去提问