美文网首页
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