自定义Redis连接池

一道大厂面试题:考察服务设计和一些接口要求。

接口二解题,要求一 限流; 要求二 接口幂等;要求三 网络编程超时设置;要求四 限流;

要求五 解决HttpClient线程安全问题,思路自定义HttpClient连接池。

错误写法:在并发场景下,来1000次请求,建立1000次连接,连接开销很致命。

我们用socket定义一个httpclient,来演示一下socket线程不安全现象:

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;

/**
 * Description: Socket网络连接到redis,需要使用redis协议进行连接
 * Author: yz
 * Date: Created in 2021/1/6 11:14
 */
public class Connetion {
    private String host;
    private int post;
    private Socket socket;           // 线程不安全
    private InputStream inputStream;
    private OutputStream outputStream;

    public Connetion(String host, int post) {
        this.host = host;
        this.post = post;
    }

    /**
     * 判断连接是否已经建立,判断连接是不是初始化好了,或者连接没有断开
     */
    public boolean isConnection(){
        if(socket !=null && inputStream !=null && socket.isClosed()){
            return true;
        }
        try {
            socket = new Socket(host,post);
            inputStream = socket.getInputStream();
            outputStream = socket.getOutputStream();
        } catch (IOException e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

    /**
     * 发送命令
     */
    public String sendCommand(byte[] command){
        if(isConnection()){
            try {
                // 客户端先写数据
                outputStream.write(command);
                // 读取服务端响应
                byte[] res = new byte[1024];
                int length = 0;
                while ((length=inputStream.read(res))>0){
                    return new String(res,0,length);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return null;
    }

}
/**
 * Description: redis协议工具类
 * Author: yz
 * Date: Created in 2021/1/6 11:41
 */
public class RedisProtocolUtils {
    public static final String DOLIER = "$";
    public static final String ALLERTSTIC = "*";
    public static final String CRLE = "\r\n";

    public static byte[] buildRespByte(Command command,byte[]... bytes){
        StringBuilder sb = new StringBuilder();
        sb.append(ALLERTSTIC).append(bytes.length+1).append(CRLE);
        sb.append(DOLIER).append(command.name().length()).append(CRLE);
        sb.append(command.name()).append(CRLE);
        for (byte[] arg : bytes) {
            sb.append(DOLIER).append(arg.length).append(CRLE);
            sb.append(new String(arg)).append(CRLE);
        }
        return sb.toString().getBytes();
    }

    /**
     * redis set,get命令
     */
    public enum Command{
        SET,GET
    }

}
/**
 * Description:
 * Author: yz
 * Date: Created in 2021/1/6 15:01
 */
public class ClientRunalbe implements Runnable {

    private BatRedisClient client;
    private String value;

    public ClientRunalbe(BatRedisClient client, String value) {
        this.client = client;
        this.value = value;
    }

    @Override
    public void run() {
        client.set("ant",value);
    }
}
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * Description: redis连接客户端含redis协议和socket
 * Author: yz
 * Date: Created in 2021/1/6 11:53
 */
public class BatRedisClient {
    private Connetion connetion;
    public BatRedisClient(String host,int port){
        connetion = new Connetion(host,port);
    }

    public String set(String key,String value){
        String result = connetion.sendCommand(
                RedisProtocolUtils.buildRespByte(RedisProtocolUtils.Command.SET,key.getBytes(),value.getBytes()));
        System.out.println("Thread name:"+Thread.currentThread().getName()
        +"[result]:"+result.replace("\r\n","")+"[value]:"+value);
        return result;
    }

    public String get(String key){
        String result = connetion.sendCommand(
                RedisProtocolUtils.buildRespByte(RedisProtocolUtils.Command.GET,key.getBytes(),key.getBytes()));
        return result;
    }

    public static void main(String[] args) {
        BatRedisClient client = new BatRedisClient("localhost",6379);
        ExecutorService pool = Executors.newCachedThreadPool();
        for (int i = 0; i < 20; i++) {
            // 存在现象: 当前线程读取到redis返回给其他线程的响应数据
            pool.execute(new ClientRunalbe(client,"123"+i));
        }
    }

}

执行mian方法测试,20个线程并发执行,结果如下:1,并未执行20次,2,redis返回的结果有粘在一起的,其实是一个请求获取到另一个请求的结果了

1,并未执行20次:多个线程共用一个socket,当一个线程写一半的时候,cpu调度的时候时间片用完了,第二个线程获取到cup时间片,也会写入数据,这些没有写完的数据也会被redis读取到,但是不能被解析,redis不能返回响应,线程读取不到响应,进入io阻塞。

2,redis返回的结果有粘在一起的:发送的时候数据量比较小,线程发送的数据包是完整的,给到redis之后是可以解析成两个请求的,也是正常返回数据,假如线程1获取到cup使用权,在read读取数据的时候,可能会一次性把缓冲区所有的数据都读过来,解析之后发现是+OK+OK这种形式了

使用自定义线程池方式解决socket多线程不安全问题,把client对象放到pool池里面

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.LinkedBlockingDeque;  // 双向队列
import java.util.concurrent.LinkedBlockingQueue;  // 单向队列

/**
 * Description: redis连接池
 * Author: yz
 * Date: Created in 2021/1/6 15:19
 */
public class RedisClientPool {

    private List<BatRedisClient> allObject;
    private LinkedBlockingQueue<BatRedisClient> linkedBlockingQueue;

    public RedisClientPool(String host,int port,int connectionCount){
        allObject = new ArrayList<>();
        this.linkedBlockingQueue = new LinkedBlockingQueue<>(10);
        for (int i = 0; i < connectionCount; i++) {
            BatRedisClient client = new BatRedisClient(host,port);
            linkedBlockingQueue.add(client);
            allObject.add(client);
        }
    }

    /**
     * 获取client连接
     */
    public BatRedisClient getClient(){
        try {
            return linkedBlockingQueue.take(); // or poll
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 将client归还到连接池
     */
    public void returnClient(BatRedisClient client){
        if(client == null){
            return;
        }
        if(!allObject.contains(client)){
            throw new IllegalStateException(
                    "Returned object not currently part of this pool");
        }
        try {
            linkedBlockingQueue.put(client);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}
/**
 * Description: 使用自定义redis连接池
 * Author: yz
 * Date: Created in 2021/1/6 15:28
 */
public class ClientPoolRunable implements Runnable {
    private RedisClientPool pool;
    private String value;

    public ClientPoolRunable(RedisClientPool pool, String value) {
        this.pool = pool;
        this.value = value;
    }

    @Override
    public void run() {
        BatRedisClient client = pool.getClient();
        client.set("ant",value);
        pool.returnClient(client);
    }
}
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * Description: 测试redis连接池
 * Author: yz
 * Date: Created in 2021/1/6 15:30
 */
public class Main {
    public static void main(String[] args) {
        ExecutorService pool = Executors.newCachedThreadPool();
        RedisClientPool redisClientPool = new RedisClientPool("localhost",6379,10);
        for (int i = 0; i < 20; i++) {
            pool.execute(new ClientPoolRunable(redisClientPool,"123"+i));
        }
    }
}

执行main方法测试,结果如下,正好执行20次,也没有再出现+OK+OK现象了

扩展知识:队列

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
MyBatis-Plus 支持内置的 Redis 作为二级缓存,但是可能会遇到一些不可避免的性能问题,例如大量查询时的 Redis 连接池瓶颈,Redis key 命名空间冲突等。 因此,我们可以采用自定义 Redis 缓存管理器来替代默认的 RedisCacheManager,自定义缓存管理器需要实现 org.apache.ibatis.cache.Cache 接口。 以下是自定义 Redis 缓存管理器的示例代码: ```java public class CustomRedisCache implements Cache { private final String id; // 缓存实例名称 private final RedisTemplate<String, Object> redisTemplate; // RedisTemplate 实例 private static final long EXPIRE_TIME_IN_SECONDS = 3600; // 缓存过期时间,单位:秒 public CustomRedisCache(String id, RedisTemplate<String, Object> redisTemplate) { if (id == null || redisTemplate == null) { throw new IllegalArgumentException("缓存实例名称和 RedisTemplate 实例均不能为空!"); } this.id = id; this.redisTemplate = redisTemplate; } @Override public String getId() { return this.id; } @Override public void putObject(Object key, Object value) { if (key == null) { return; } redisTemplate.opsForValue().set(key.toString(), value, EXPIRE_TIME_IN_SECONDS, TimeUnit.SECONDS); } @Override public Object getObject(Object key) { return key == null ? null : redisTemplate.opsForValue().get(key.toString()); } @Override public Object removeObject(Object key) { if (key == null) { return null; } redisTemplate.delete(key.toString()); return null; } @Override public void clear() { Set<String> keys = redisTemplate.keys("*" + getId() + "*"); if (keys != null && keys.size() > 0) { redisTemplate.delete(keys); } } @Override public int getSize() { return redisTemplate.keys("*" + getId() + "*").size(); } @Override public ReadWriteLock getReadWriteLock() { return null; } } ``` 接下来,我们需要将自定义 Redis 缓存管理器注册到 MyBatis 中,示例代码如下: ```java @Configuration public class MybatisConfig { @Autowired private RedisTemplate<String, Object> redisTemplate; @Bean public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource) throws Exception { MybatisSqlSessionFactoryBean sqlSessionFactory = new MybatisSqlSessionFactoryBean(); sqlSessionFactory.setDataSource(dataSource); sqlSessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/*.xml")); sqlSessionFactory.setPlugins(new Interceptor[]{new PaginationInterceptor()}); sqlSessionFactory.setCache(new CustomRedisCache("mybatis_cache", redisTemplate)); // 注册自定义缓存管理器 return sqlSessionFactory; } } ``` 最后,我们还需要在 MyBatis-Plus 的配置文件 mybatis-plus.yml 中添加一条配置,将 MyBatis 的缓存管理器设置为自定义的缓存管理器: ```yml mybatis-plus: configuration: cache-enabled: true # 允许缓存 # 使用自定义 Redis 缓存管理器 local-cache: # 是否使用一级缓存,默认为 true enabled: true # 默认缓存过期时间,单位:毫秒,默认值为 -1,即永不过期 ttl: -1 # 一级缓存最大数量,默认值为 1024 size: 1024 second-cache: # 是否使用二级缓存,默认为 true enabled: true # 默认缓存过期时间,单位:毫秒,默认值为 -1,即永不过期 ttl: -1 # 内置二级缓存管理器,支持Redis、Ehcache、Caffeine、H2、LevelDB、J2Cache等 cache-manager: com.baomidou.mybatisplus.extension.caches.RedisCacheManager # 自定义二级缓存管理器,必须实现 org.apache.ibatis.cache.Cache 接口 custom-cache: com.example.CustomRedisCache ``` 参考链接: - https://mp.weixin.qq.com/s/GvF8ffYQbeytE0glCNV9Xg

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值