Redis的应用实战场景

这里列举常见的redis实战场景

大概从网上找来了下面这里实战场景:
1、最新20条评论
2、排行榜
3、计数
4、统计在某段特点时间里有多少特定用户访问了某个特定资源(比如我想要知道某些特定的注册用户或IP地址,他们到底有多少访问了某篇文章。)
5、按照用户投票和时间排序
6、实时分析正在发生的情况,用于数据统计与防止垃圾邮件等
7、Reverse cache(反向cache)
8、用户最近访问记录
9、Fast transaction with Lua
Redis 的Lua的功能扩展实际给Redis带来了更多的应用场景,你可以编写若干command组合作为一个小型的非阻塞事务或者更新逻辑,如:在收到 message推送时,同时1.给自己的增加一个未读的对话 2.给自己的私信增加一个未读消息 3.最后给发送人回执一个完成推送消息,这一层逻辑完全可以在Redis Server端实现。
但是,需要注意的是Redis会将lua script的全部内容记录在aof和传送给slave,这也将是对磁盘,网卡一个不小的开销。
10、分布式锁
11、抢号(不一定每人都有),抢红包模式(每人都有)
12、Uniq操作,获取某段时间所有数据去重值
13、打卡
14、session存储
15、bitmap的使用场景
16、文章投票
17、限流工具

18、好友关系

19、热数据查询

具体api说明:https://blog.csdn.net/sinat_22797429/article/details/89196933

1、排行榜场景(利用zset)

需求:

前段时间,做了一个世界杯竞猜积分排行榜。对世界杯64场球赛胜负平进行猜测,猜对+1分,错误+0分,一人一场只能猜一次。
1.展示前一百名列表。
2.展示个人排名(如:张三,您当前的排名106579)。

       一开始打算直接使用mysql数据库来做,遇到一个问题,每个人的分数都会变化,如何能够获取到个人的排名呢?数据库可以通过分数进行row_num排序,但是这个方法需要进行全表扫描,当参与的人数达到10000的时候查询就非常慢了。
redis的排行榜功能就完美锲合了这个需求。来看看我是怎么实现的吧。

实现逻辑:

redis中的zset(sorted sets)数据类型

Sorted Sets数据类型就像是set和hash的混合。与sets一样,Sorted Sets是唯一的,不重复的字符串组成。可以说Sorted Sets也是Sets的一种。

Sorted Sets是通过Skip List(跳跃表)和hash Table(哈希表)的双端口数据结构实现的,因此每次添加元素时,Redis都会执行O(log(N))操作。所以当我们要求排序的时候,Redis根本不需要做任何工作了,早已经全部排好序了。元素的分数可以随时更新。

将数据以zset的数据方式存进redis(根据得分为score属性)。在存进去的时候redis已经帮我们排序好了。、

(存数据时以分数为score属性,在获取时根据score属性值进行逆序获取)

1.展示前一百名列表。

代码实现:

依赖

 <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.4.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.60</version>
        </dependency>
    </dependencies>

创建以下结构:

controller:

@RestController
public class PhbController {

    @Autowired
    PhbServiceImpl phbServiceImpl;

    @GetMapping("/phb")
    public void phb(){

        phbServiceImpl.phbImpl();
    }
}

service:

@Service
public class PhbServiceImpl {

    //注入template对象(也可以直接用RedisTemplate)
//注意StringRedisTemplate表示key、value都是String类型,如果用RedisTemplate则key为String、value为object类型
    @Autowired
    StringRedisTemplate redisTemplate;

    //redis存储的key值
    public static final String SCORE_RANK = "score_rank";

    //实现逻辑
    public void phbImpl(){

        //1、模拟10万条数据
        Set<ZSetOperations.TypedTuple<String>> tuples = new HashSet<>();
        long start = System.currentTimeMillis();
        for (int i = 0; i < 100000; i++) {//向set集合中插入10万条数据
            DefaultTypedTuple<String> tuple = new DefaultTypedTuple<>("张三" + i, 1D + i);
            tuples.add(tuple);
        }
        System.out.println("循环时间:" +( System.currentTimeMillis() - start));
        //2、向redis中添加该set集合
        //此时的redis中数据则是key:SCORE_RANK----value:{("张三" + i,1D + i),("张三" + i,1D + i)}
        //其中以1D + i为score属性进行排序
        Long num = redisTemplate.opsForZSet().add(SCORE_RANK, tuples);
        System.out.println("批量新增时间:" +(System.currentTimeMillis() - start));
        System.out.println("受影响行数:" + num);

        //3、获取排行版前10名列表+获取前10名分数和名称列表
        list(10);

    }

    private void list(int count){
        //获取排名列表
        Set<String> range = redisTemplate.opsForZSet().range(SCORE_RANK, 0, count);
        System.out.println("获取到的排名列表为:"+range);
        //获取排行和分数列表
        Set<ZSetOperations.TypedTuple<String>> rangeWithScores = redisTemplate.opsForZSet().reverseRangeWithScores(SCORE_RANK, 0, 10);
        System.out.println("获取排行和分数列表为:"+ JSON.toJSONString(rangeWithScores));

    }
}

启动类:

@SpringBootApplication
public class phbApplication {

    public static void main(String[] args) {

        SpringApplication.run(phbApplication.class,args);
    }
}

yml文件:

spring:
  #redis配置信息
  redis:
    #redis 服务器地址
    host: 192.168.211.20
    #redis端口
    port: 6379
    #客户端超时时间单位是毫秒 默认是2000
    timeout: 5000
    #最大空闲数
    maxIdle: 20
    #连接池的最大数据库连接数
    maxActive: -1
    #控制一个pool可分配多少个jedis实例,用来替换上面的maxActive
    maxTotal: 100
    #最大建立连接等待时间。如果超过此时间将接到异常
    maxWaitMillis: 100
    #连接的最小空闲时间
    minEvictableIdleTimeMillis: 864000000
    #每次释放连接的最大数目
    numTestsPerEvictionRun: 10
    #逐出扫描的时间间隔(毫秒) 如果为负数,则不运行逐出线程
    timeBetweenEvictionRunsMillis: 300000
    #是否在从池中取出连接前进行检验,如果检验失败,则从池中去除连接并尝试取出另一个
    testOnBorrow: true
    #在空闲时检查有效性
    testWhileIdle: false
    #数据库
    database: 0
server:
  port: 9090

访问:http://localhost:9090/phb接口查看控制台:

上面是进行所有人员进行排序的,如果此时只想获取某个人的分数和排名,如何实现?

2.展示个人排名(如:张三,您当前的排名106579)。

代码实现:

在phbImpl方法中先模拟一个数据的插入:

 redisTemplate.opsForZSet().add(SCORE_RANK, "李四", 8899);

在list方法中打印该人的排位等:

Long rankNum = redisTemplate.opsForZSet().reverseRank(SCORE_RANK, "李四");
        System.out.println("李四的个人排名:" + rankNum);

Double score = redisTemplate.opsForZSet().score(SCORE_RANK, "李四");
        System.out.println("李四的分数:" + score);

访问http://localhost:9090/phb查看控制台:

3、统计某分数区间有多少人?

代码实现:8001-9000区间人数

Long count = redisTemplate.opsForZSet().count(SCORE_RANK, 8001, 9000);

4、获取整个集合数量

Long aLong = redisTemplate.opsForZSet().zCard(SCORE_RANK);

5、对某个人进行分数修改,使用加法操作分数(将李四的值加1000)

Double score = redisTemplate.opsForZSet().incrementScore(SCORE_RANK, "李四", 1000);

(上面修改时如果有缓存数据则为更新,没有则为新增)

6、总结:

1)、新增or更新

有三种方式,一种是单个,一种是批量,对分数使用加法(如果不存在,则从0开始加)。

//单个新增or更新
Boolean add(K key, V value, double score);
//批量新增or更新
Long add(K key, Set<TypedTuple<V>> tuples);
//使用加法操作分数
Double incrementScore(K key, V value, double delta);

2)、删除:删除提供了三种方式:通过key/values删除,通过排名区间删除,通过分数区间删除。

//通过key/value删除
Long remove(K key, Object... values);

//通过排名区间删除
Long removeRange(K key, long start, long end);

//通过分数区间删除
Long removeRangeByScore(K key, double min, double max);

3)查询:

3.1:列表查询:分为两大类,正序和逆序。以下只列表正序的,逆序的只需在方法前加上reverse即可:

//通过排名区间获取列表值集合

Set<V> range(K key, long start, long end);

//通过排名区间获取列表值和分数集合
Set<TypedTuple<V>> rangeWithScores(K key, long start, long end);

//通过分数区间获取列表值集合
Set<V> rangeByScore(K key, double min, double max);

//通过分数区间获取列表值和分数集合
Set<TypedTuple<V>> rangeByScoreWithScores(K key, double min, double max);

//通过Range对象删选再获取集合排行
Set<V> rangeByLex(K key, Range range);

//通过Range对象删选再获取limit数量的集合排行
Set<V> rangeByLex(K key, Range range, Limit limit);


3.2:单人查询

可获取单人排行,和通过key/value获取分数。以下只列表正序的,逆序的只需在方法前加上reverse即可:

//获取个人排行
Long rank(K key, Object o);

//获取个人分数
Double score(K key, Object o);

4)、统计:

统计分数区间的人数,统计集合基数。


//统计分数区间的人数
Long count(K key, double min, double max);

//统计集合基数
Long zCard(K key);

以上就是redis中使用排行榜功能的一些例子,和对redis的操作方法了。redis不仅仅只是作为缓存,它更是数据库,提供了许多的功能,我们都可以好好的利用。

在这里我使用redis来实现了世界杯积分排行的展示,无论是在批量更新或是获取个人排行等方便,都有着很高效率,也降低了对数据库操作的压力,达到了很好的效果。

2、最新20条评论(利用list)

关注列表,粉丝列表,评论列表等都可以用Redis的list结构来实现。这里使用lpush和ltrim简单实现始终取最新评论的前20条。

//模拟从左边插入两条数据
redisTemplate.opsForList().leftPush("commons:id001","id为001的文章第1条评论");
redisTemplate.opsForList().leftPush("commons:id001","id为001的文章第2条评论");
//获取最新的一条数据(0-0区间)
List<String> range1 = redisTemplate.opsForList().range("commons:id001", 0, 0);
System.out.println(range1);

查看控制台:

list类型还可以用来其他操作:

 

3、计数器(限流)

如果要记录某个ip或用户的请求次数,限制一天请求次数不超过2次,超过则进行限制访问

实现:在redis中存相应用户的缓存,key为用户标示,value为访问次数,每次访问则利用incr自增1,每次访问都查看访问次数是否超过了2次,超过了则进行限流。

这里不做具体代码,就写几个命令代码说明下:

//缓存一个值
redisTemplate.opsForValue().set("bbb","100");
//该值加上100
redisTemplate.opsForValue().increment("bbb",100);
System.out.println(JSON.toJSONString(redisTemplate.opsForValue().get("bbb")));

最后输出为“200”。注意这里的key,value都是String类型,虽然用inc方法时String和int类型相加后为200,但200仍然是String类型。那为什么value也是String类型的呢?因为我们在之前注入的redisTemplate是注入了StringRedisTemplate,所以其key,value都是String类型,如果此时换成RedisTemplate类型,此时key为String,value则为object类型,即可进行正常的incr操作了。或者可以使用下面这种方式:将key设置为限流标识,value为用户标识,访问次数为score,对其修改则是对score值进行修改(上面有说到如何对score值进行修改)

也可以用分布式信号量实现

 

4、获取某段时间所有数据去重值

存进set即可

5、打卡签到

可以用hash、set,但为了节省空间可以用bitmap。当然也可以用mysql实现

6、session存储

    即springsession实现方案,在分布式系统中不同机器有不同session,此时要将session统一放在某个数据库中,此时就能实现分布式统一共享session,session共享可以将session放到redis、tomcat、mysql等中进行统一存储管理。这里将session内容存到redis中进行共享。

    springsession在一定程度上可以解决单点登陆问题,前提是所有系统是父子域名关系,因为利用cookie实现单点登陆的方案中,每个cookie都有一个对应的域,该域对应相应的域名。springsession中可以将cookie中token的访问域设置为父子域都可访问,则解决了单点登陆的问题。

这里就以简单的session获取存储为例进行说明springsession的实现

//官方案例:https://docs.spring.io/spring-session/docs/current/reference/html5/guides/boot-redis.html

1、依赖:

    <dependency>
		<groupId>org.springframework.session</groupId>
		<artifactId>spring-session-data-redis</artifactId>
	</dependency>

2、配置springsession相关配置:

这里也可以不写,后面用@EnableRedisHttpSession注解代替

#表示将session移交存储到redis
  session:
    store-type: redis

3、代码实现:

3.1、在启动类上添加@EnableRedisHttpSession注解,开启springsession功能。

@EnableRedisHttpSession
@SpringBootApplication
public class phbApplication {

    public static void main(String[] args) {

        SpringApplication.run(phbApplication.class,args);
    }
}

(注意这里如果要向redis存一些对象数据,该对象要实现序列化接口seri,因为向redis存数据是要进行序列化的,java对象默认是jdk序列化,所以要对象实现序列化接口)

3.2、完整代码实现:

@RestController
public class SessionDemo {


    @GetMapping("/session")
    public void sessionDemo(HttpServletRequest request, HttpServletResponse response){
        HttpSession session = request.getSession();
        session.setAttribute("sessionDemo",123123);
        System.out.println(session.getAttribute("sessionDemo"));

    }
}


8、好友关系

求交集

set类型中:

获取两个或者多个集合的并集(otherKeys可以为单个值或者是集合)

redisTemplate.opsForSet().union(key, otherKeys)

获取两个集合的交集(key对应的无序集合与otherKey对应的无序集合求交集)

redisTemplate.opsForSet().intersect(key, otherKey)

 

10、bitmap的使用场景

1、打卡签到

2、统计活跃用户

3、用户在线状态

一般状态只存在两种情况时可用位图(bitmap),如签到只有签到和不签到两种状态。

用bitmap不用其他数据类型的比较:https://www.cnblogs.com/spareyaya/p/12806637.html

11、分布式锁

实现方式:

1、使用jedis/springdata-redis+lua实现

2、使用redisson实现(底层也是用lua脚本实现)

3、zookeeper实现

4、数据库

因为第一种方法要实现比较复杂,而redission为我们封装了一轮代码,直接为我们提供watchedog、分布式锁等实现机制,所以一般情况下redis的分布式锁的实现都是用redission实现比较简单。

redission实现redis的分布式锁:(redisson也实现了juc接口,所以使用基本和juc类似)

这里以redisson官方文档为主https://github.com/redisson/redisson/wiki/2.-%E9%85%8D%E7%BD%AE%E6%96%B9%E6%B3%95

1、依赖:

 <!--redisson分布式锁-->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.5.0</version>
        </dependency>

2、redisson配置(可以用程序配置,或者yml配置然后再加载)(如果依赖是sprinboot-data-redisson的包,则可直接在yml配置)这里使用程序配置

2.1、向容器注入redis的配置类MyRedisConfig

@Configuration
public class MyRedisConfig {
    /**
    * 所有操作都是通过redissonClient对象操作
    *
    * */
    @Bean(destroyMethod = "shutdown")//执行完后的销毁方法shutdown
    public RedissonClient redisson() throws Exception{

        //1、配置信息
        Config config = new Config();
        //集群配置
        config.useClusterServers()
                .addNodeAddress("redis://192.168.211.20:6379",
                                "redis://192.168.211.20:6380",
                                "redis://192.168.211.20:6381");
        //根据配置信息创建出redissonClient
        return Redisson.create(config);
    }

}

3、此时就可以直接来使用redisson

(官方文档:https://github.com/redisson/redisson/wiki/8.-%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%E5%92%8C%E5%90%8C%E6%AD%A5%E5%99%A8

基于Redis的Redisson分布式可重入锁RLock Java对象实现了java.util.concurrent.locks.Lock接口。同时还提供了异步(Async)反射式(Reactive)RxJava2标准的接口。

所有redisson的分布式锁和同步器的使用类似于juc锁的使用

这里基于Reentrant Lock和Semaphore进行举例

3.1、Reentrant Lock

@RestController
public class RedisLock {

    @Autowired
    RedissonClient redissonClient;

    @GetMapping("/lock")
    public void lock(){

        RLock lock = redissonClient.getLock("myLock");
       // 最常见的使用方法,阻塞式等待
        lock.lock();
        try {
            System.out.println("加锁成功,执行业务......."+Thread.currentThread().getId());
            Thread.sleep(30000);//模拟业务30秒
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
            System.out.println("释放锁...."+Thread.currentThread().getId());
        }

    }
}

此时在浏览器开启两个访问端口,都发送请求,此时只有68线程获取到锁,在30秒后释放锁,再被另一个请求获取到锁

此时除了这些方法还存在其方法,例如:

// 加锁以后10秒钟自动解锁
// 无需调用unlock方法手动解锁
lock.lock(10, TimeUnit.SECONDS);

// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);

//异步执行
lock.lockAsync();
lock.lockAsync(10, TimeUnit.SECONDS);
Future<Boolean> res = lock.tryLockAsync(100, 10, TimeUnit.SECONDS);

3.2 Semaphore的使用举例

注意:一个信号量只能指定一次许可数量trySetPermits(再次使用无效),要表可以使用reducePermits/drainPermits/手动去redis修改相应的值

RSemaphore semaphore = redissonClient.getSemaphore("semaphore");
semaphore.trySetPermits(5);//设置有5个坑位(5个许可)
//加锁、获取一个坑位
semaphore.acquire(); 
//解锁
semaphore.release();

另外还有其他方法:

semaphore.acquireAsync();
semaphore.acquire(23);//获取23个坑位
semaphore.tryAcquire();
//或
semaphore.tryAcquireAsync();
semaphore.tryAcquire(23, TimeUnit.SECONDS);//获取一个坑位,等待时间为23秒
//或
semaphore.tryAcquireAsync(23, TimeUnit.SECONDS);
semaphore.release(10);
semaphore.release();
//或
semaphore.releaseAsync();
public interface RSemaphore extends RExpirable, RSemaphoreAsync {
 
    boolean trySetPermits(int var1);                     //设置permits数
    void reducePermits(int var1);                        //减少permit数
 
    void acquire() throws InterruptedException;          //获得一个permit
    void acquire(int var1) throws InterruptedException;  //获得var1个permits
 
    boolean tryAcquire();                                //尝试获得permit
    boolean tryAcquire(int var1);                        //尝试获得var1个permit
    boolean tryAcquire(long var1, TimeUnit var3) throws InterruptedException; //尝试获得permit,等待时间var1
    boolean tryAcquire(int var1, long var2, TimeUnit var4) throws InterruptedException;
                                                         //尝试获得var1个permit,等待时间var2
    void release();                                      //释放permit
    void release(int var1);                              //释放var1个permits
 
    int availablePermits();                              //信号量的permits数
    int drainPermits();                                  //清空permits
 
}

信号量可以用来实现限流操作(同时允许n个线程访问)

12、发布订阅功能

https://blog.csdn.net/wlddhj/article/details/107006035

https://zhuanlan.zhihu.com/p/59065399

由于没有消息持久化与 ACK 的保证,所以,Redis 的发布订阅功能并不可靠。这也就导致了它的应用场景很有限,建议用于实时与可靠性要求不高的场景。

13、抢号(不一定每人都有),抢红包模式

使用redisson实现分布式场景(因为获取到锁+减库存的全过程要求是原子操作的)

抢号:可以使用redisson的ReentrantLock锁,一个人获取到锁就减一个号

抢红包:也可以用分布式锁,指定抢红包人数,随机分成n份数额,一个人获取到锁就红包数减1并且拿走随机的一个金额。

14、延时队列

利用zset中有score的属性,以时间为score属性,这样zset会以时间前后进行排序,然后消费者再利用线程或者定时任务去查看当前时间大于哪个zset中的元素则拿出来消费。(不用遍历,大于就拿来消费即可)

15、布隆过滤器

主要用来解决redis的缓存穿透问题。

其中谷歌的guava则封装了布隆过滤器的实现,但那是把数据放在本地内存中,如果要将数据放在redis中,redis也有实现的一个布隆过滤器插件,需要去安装使用。

16、redis在高并发中的缓存穿透,缓存击穿,缓存雪崩实例解决

穿透:设null值/布隆过滤器

击穿、雪崩:均匀分布过期时间,限流,热数据,预加载等操作

具体实现后面再说

  • 5
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值