美文网首页
Redis 如何实现高性能高并发 - 简单说明

Redis 如何实现高性能高并发 - 简单说明

作者: 右耳菌 | 来源:发表于2022-08-21 15:28 被阅读0次

    1. Redis在缓存中的应用

    2. 简单实现Spring/Spring-Boot集成Redis(通过Jedis)

    1. 假设有以下这样的一个数据库
     create table tb_user(
      id int(11) NOT NULL AUTO_INCREMENT,
      name char(20) NOT NULL DEFAULT '',
      age int(11) NOT NULL,
      img varchar(128) DEFAULT NULL,
      PRIMARY KEY (id)
    ) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET = utf8;
    
    INSERT INTO tb_user (id, `name`, age, img) VALUES (1, 'neco', 18, '/photo/user/neco.png');
    
    1. 项目中加入以下依赖(这里只展示集成的部分,对于访问数据库的流程,这里不再描述说明)
           <dependency>
                <groupId>redis.clients</groupId>
                <artifactId>jedis</artifactId>
            </dependency>
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>fastjson</artifactId>
                <version>1.2.28</version>
            </dependency>
    
    1. 创建JedisConfig类,配置Bean
    package cn.lazyfennec.redis.config;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import redis.clients.jedis.JedisPool;
    
    /**
     * @Author: Neco
     * @Description:
     * @Date: create in 2022/8/21 10:45
     */
    @Configuration
    public class JedisConfig {
        // 创建对象,spring托管,即放入spring容器,使其支持注入
        @Bean
        public JedisPool jedisPool() {
            JedisPool jedisPool = new JedisPool("192.168.1.7", 6379);
            return jedisPool;
        }
    }
    
    1. 创建UserController
    package cn.lazyfennec.redis.controller;
    
    import cn.lazyfennec.redis.entity.User;
    import cn.lazyfennec.redis.service.IUserService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    /**
     * @Author: Neco
     * @Description:
     * @Date: create in 2022/8/21 10:38
     */
    @RestController
    public class UserController {
    
        @Autowired
        private IUserService userService;
    
        @RequestMapping("/user/{id}")
        public User findUserById(@PathVariable Integer id) {
            return userService.findUserById(id);
        }
    }
    
    1. 创建UserServiceImpl
    package cn.lazyfennec.redis.service.impl;
    
    import cn.lazyfennec.redis.entity.User;
    import cn.lazyfennec.redis.mapper.UserMapper;
    import cn.lazyfennec.redis.service.IUserService;
    import com.alibaba.fastjson.JSONObject;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import redis.clients.jedis.Jedis;
    import redis.clients.jedis.JedisPool;
    
    import javax.annotation.Resource;
    
    /**
     * @Author: Neco
     * @Description:
     * @Date: create in 2022/8/21 10:40
     */
    @Service
    public class UserServiceImpl implements IUserService {
    
        @Resource
        private UserMapper userMapper;
    
        @Autowired
        private JedisPool jedisPool;
    
        @Override
        public User findUserById(Integer id) {
            User user = null;
            Jedis jedis = null;
            try {
                jedis = jedisPool.getResource();
                String userStr = jedis.get("user_" + id); // 尝试获取数据
                if (userStr != null && !userStr.isEmpty()) { // 如果获取到有效数据,则转换后返回
                    user = JSONObject.parseObject(userStr, User.class);
                } else {// 如果没有获取到数据,则查询数据库返回
                    user = userMapper.findUserById(id);
                    if (user != null) jedis.set("user_" + id, JSONObject.toJSONString(user)); // 设置到redis中
                }
            } finally {
                // 记得关闭Jedis,因为这里使用的是JedisPool,所以这里的关闭并不是直接关闭连接,而是释放,以供其他的业务使用
                if (jedis != null) jedis.close();
            }
            return user;
        }
    }
    
    1. 总结
      对于这个例子,我们可以得知,在读多写少的情况下,利用redis缓存,可以有效的解决高并发的问题,对数据库的访问进行限流。

    3. 通过benchmark进行基准测试

    [root@lazyfennec redis-5.0.14]# redis-benchmark --help
    Usage: redis-benchmark [-h <host>] [-p <port>] [-c <clients>] [-n <requests>] [-k <boolean>]
    
     -h <hostname>      Server hostname (default 127.0.0.1)
     -p <port>          Server port (default 6379)
     -s <socket>        Server socket (overrides host and port)
     -a <password>      Password for Redis Auth
     -c <clients>       Number of parallel connections (default 50)
     -n <requests>      Total number of requests (default 100000)
     -d <size>          Data size of SET/GET value in bytes (default 3)
     --dbnum <db>       SELECT the specified db number (default 0)
     -k <boolean>       1=keep alive 0=reconnect (default 1)
     -r <keyspacelen>   Use random keys for SET/GET/INCR, random values for SADD
      Using this option the benchmark will expand the string __rand_int__
      inside an argument with a 12 digits number in the specified range
      from 0 to keyspacelen-1. The substitution changes every time a command
      is executed. Default tests use this to hit random keys in the
      specified range.
     -P <numreq>        Pipeline <numreq> requests. Default 1 (no pipeline).
     -e                 If server replies with errors, show them on stdout.
                        (no more than 1 error per second is displayed)
     -q                 Quiet. Just show query/sec values
     --csv              Output in CSV format
     -l                 Loop. Run the tests forever
     -t <tests>         Only run the comma separated list of tests. The test
                        names are the same as the ones produced as output.
     -I                 Idle mode. Just open N idle connections and wait.
    
    Examples:
     Run the benchmark with the default configuration against 127.0.0.1:6379:
       $ redis-benchmark
    
     Use 20 parallel clients, for a total of 100k requests, against 192.168.1.1:
       $ redis-benchmark -h 192.168.1.1 -p 6379 -n 100000 -c 20
    
     Fill 127.0.0.1:6379 with about 1 million keys only using the SET test:
       $ redis-benchmark -t set -n 1000000 -r 100000000
    
     Benchmark 127.0.0.1:6379 for a few commands producing CSV output:
       $ redis-benchmark -t ping,set,get -n 100000 --csv
    
     Benchmark a specific command line:
       $ redis-benchmark -r 10000 -n 10000 eval 'return redis.call("ping")' 0
    
     Fill a list with 10000 random elements:
       $ redis-benchmark -r 10000 -n 10000 lpush mylist __rand_int__
    
     On user specified command lines __rand_int__ is replaced with a random integer
     with a range of values selected by the -r option.
    

    测试:

    • 下面是我虚拟机中的执行结果(内存和核心数比较小)
    [root@lazyfennec redis-5.0.14]# redis-benchmark -r 1000000 -n 2000000 -t get,set,lpush,lpop -q
    SET: 33129.59 requests per second
    GET: 33604.97 requests per second
    LPUSH: 33372.27 requests per second
    LPOP: 33003.84 requests per second
    
    • 在4核4G的云服务器中的执行结果如下:
    [root@VM-4-15-centos bin]# redis-benchmark -r 1000000 -n 2000000 -t get,set,lpush,lpop -q
    SET: 88613.20 requests per second
    GET: 88570.04 requests per second
    LPUSH: 88699.66 requests per second
    LPOP: 89182.20 requests per second
    
    • 如果加上Pipeline的配置
    [root@VM-4-15-centos bin]# redis-benchmark -r 1000000 -n 2000000 -P 4 -t get,set,lpush,lpop 
    ====== SET ======
      2000000 requests completed in 6.08 seconds
      50 parallel clients
      3 bytes payload
      keep alive: 1
    
    98.82% <= 1 milliseconds
    99.99% <= 2 milliseconds
    100.00% <= 3 milliseconds
    100.00% <= 3 milliseconds
    329163.91 requests per second
    
    ====== GET ======
      2000000 requests completed in 5.78 seconds
      50 parallel clients
      3 bytes payload
      keep alive: 1
    
    99.68% <= 1 milliseconds
    100.00% <= 2 milliseconds
    100.00% <= 2 milliseconds
    346200.44 requests per second
    
    ====== LPUSH ======
      2000000 requests completed in 5.83 seconds
      50 parallel clients
      3 bytes payload
      keep alive: 1
    
    99.63% <= 1 milliseconds
    100.00% <= 1 milliseconds
    342935.53 requests per second
    
    ====== LPOP ======
      2000000 requests completed in 6.42 seconds
      50 parallel clients
      3 bytes payload
      keep alive: 1
    
    98.49% <= 1 milliseconds
    99.99% <= 2 milliseconds
    100.00% <= 2 milliseconds
    311526.47 requests per second
    

    根据测试结果可以看到,执行效率算是非常高了,其实从这里就可以看出来,redis是完全可以实现支撑10w/s的并发查询的。


    3. 数据结构 - String

    String数据结构是简单的key-value类型,value其实不仅是String,也可以是数字。
    使用场景:微博数,粉丝数(常规计数)等

    常用命令:

    命令 说明
    GET 获取指定key的值
    SET 设置指定key的值
    INCR 将key中存储的数字值增一
    DECR 将key中存储的数字值减一
    MGET 获取所有(一个或多个)给定key的值

    场景1: 粉丝数或者点赞数或浏览数增加的情况

    如某个直播场景中,某个主播同一时间有多个人对该主播进行关注操作,那么将会触发一个粉丝数+1的请求,这个时候如何保证数据的准确性?

    • 答:redis命令的执行是一条一条执行的,虽然可能前端同时发起关注请求,但是到redis的时候,还是会根据前后顺序一条一条执行。
      针对命令处理来说,redis是单线程的!
      这样就保证了其操作的原子性

    4. 数据结构 - Hash

    Hash是一个string类型的field和value的映射表
    使用场景: 存储部分变更数据,如用户信息等等
    常用命令:

    命令 说明
    HGET 获取指定key的获取存储在哈希表中指定字段的值
    HSET 将哈希表key中的字段field的值设置为value
    HGETALL 获取在哈希表中指定key的所有字段和值

    场景1: 某种情况下,我们需要查询某一个用户的某一个/多个信息

    如我们在缓存某个用户的信息的时候,可能碰到一种情况就是,我并不需要整个用户的所有信息,可能我只是想要查询某个字段的值,如name。为了提高效率,我们获取缓存数据的时候,可以只获取name的值(避免过多的数据传输消耗的资源)

    • 实现的大致代码如下
        @Override
        public User findUserById(Integer id) {
            User user = null;
            Jedis jedis = null;
            try {
                jedis = jedisPool.getResource();
                Map<String, String> map = jedis.hgetAll("hash_user_" + id);// 尝试获取数据
    
                if (map != null && map.size() > 0) { // 如果获取到有效数据,则转换后返回
    
                    user = new User();
                    user.setId(Integer.valueOf(map.get("id")));
                    user.setName(map.get("name"));
                    user.setAge(Integer.valueOf(map.get("age")));
                    user.setImg(map.get("img"));
    
                } else {// 如果没有获取到数据,则查询数据库返回
                    user = userMapper.findUserById(id);
    
                    // 第一种方式,缺点,需要多次与redis进行交互
    //                jedis.hset("hash_user_" + id, "id", String.valueOf(user.getId()));
    //                jedis.hset("hash_user_" + id, "name", user.getName());
    //                jedis.hset("hash_user_" + id, "age", String.valueOf(user.getAge()));
    //                jedis.hset("hash_user_" + id, "img", String.valueOf(user.getImg()));
    
                    // 第二种方式,优化的方式
                    HashMap<String, String> hmap = new HashMap<>();
                    hmap.put("id", String.valueOf(user.getId()));
                    hmap.put("name", user.getName());
                    hmap.put("age", String.valueOf(user.getAge()));
                    hmap.put("img", user.getImg());
                    jedis.hmset("hash_user_" + id, hmap);
                }
            } finally {
                // 记得关闭Jedis,因为这里使用的是JedisPool,所以这里的关闭并不是直接关闭连接,而是释放,以供其他的业务使用
                if (jedis != null) jedis.close();
            }
            return user;
        }
    
        @Override
        public String findUserNameById(Integer id) {
            Jedis jedis = null;
            String name = null;
            try {
                jedis = jedisPool.getResource();
                name = jedis.hget("hash_user_" + id, "name"); // 尝试获取数据
                if(name != null && !name.isEmpty()) {
                    name = userMapper.findUserNameById(id);
                }
            } finally {
                // 记得关闭Jedis,因为这里使用的是JedisPool,所以这里的关闭并不是直接关闭连接,而是释放,以供其他的业务使用
                if (jedis != null) jedis.close();
            }
            return name;
        }
    

    如果觉得有收获,欢迎点赞和评论,更多知识,请点击关注查看我的主页信息哦~

    相关文章

      网友评论

          本文标题:Redis 如何实现高性能高并发 - 简单说明

          本文链接:https://www.haomeiwen.com/subject/rdrvgrtx.html