前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >扛不住 1W+ 并发流量请求,SpringCache 缓存注解真的那么弱?

扛不住 1W+ 并发流量请求,SpringCache 缓存注解真的那么弱?

作者头像
猿芯
发布2020-07-06 18:00:15
发布2020-07-06 18:00:15
1.2K00
代码可运行
举报
运行总次数:0
代码可运行

前言

最近做 API 接口压测时,TPS(要求至少 7000/s)始终上不去,究其原因发现很多接口是直接连库查询。

所以想到用 SpringCache +Codis 集群(底层 Redis)做缓存。效果还是很不错的,平均每个接口 tps 能达到 1W/s,但是有些接口时不时的会报类型转换或读取超时异常。

先奉上代码

Redis 配置

代码语言:javascript
代码运行次数:0
运行
复制
global:  redis:    nodes: IP:2181,IP:2181,IP:2181    zkProxyDir: /zk/codis/db_codis-demo/proxy    timeout: 30000    maxtTotal: 80    maxIdle: 20

Rest 接口类

代码语言:javascript
代码运行次数:0
运行
复制
@CacheConfig(cacheNames = "CACHE_USER", keyGenerator = "keyGenerator")
@RestController@RequestMapping("/users/v1")public class UserController extends BaseController {
    @Cacheable    @GetMapping    public R queryUser(@RequestHeader("pid") String pid) {        ...    }
    @CacheEvict(allEntries = true)    @PostMapping    public R save(UserEntity entity) {        ...        return R.ok();    }
    @CacheEvict(allEntries = true)    @DeleteMapping("/{id}")    public R delete(@PathVariable("id") Long id) {        ...        return R.ok();    }
    @CacheEvict(allEntries = true)    @PutMapping    public R update(UserEntity user) {        ...        return R.ok();    }
    @CacheEvict(allEntries = true)    @PutMapping("/sort")    public R sort(@RequestParam("ids") String ids) {        return R.ok();    }}

上面代码缓存逻辑是利用 @CacheConfig 定义 KEY 的名称与值

代码语言:javascript
代码运行次数:0
运行
复制
cacheNames = Const.Cache.XXXkeyGenerator = "keyGenerator"

在查询接口上加缓存 @Cacheable,一旦有增删改操作,利用 @CacheEvict(allEntries = true)注解使缓存失效,重新查库。

压测异常

1、在实际 API 压测时,时不时程序执行上抛出类型转换异常:

代码语言:javascript
代码运行次数:0
运行
复制
java.lang.ClassCastException: java.util.ArrayList cannot be cast to java.lang.Longat redis.clients.jedis.Connection.getIntegerReply(Connection.java:161)at redis.clients.jedis.Jedis.del(Jedis.java:108)

或者

代码语言:javascript
代码运行次数:0
运行
复制
Caused by: redis.clients.jedis.exceptions.JedisDataException: ERR handle request, command 'EXEC' is not allowed

2、当缓存数据比较大时,报读取超时

代码语言:javascript
代码运行次数:0
运行
复制
JedisConnectionException: java.net.SocketTimeoutException: Read timed out

问题定位

Redis 在执行 getdel 命令时,因为 Redis 已超负荷,可能会返回超时异常,del命令未执行,从而导致Codis把这异常连接实例收回到连接池。

依据 jedis 源码发现 Connection 中封装 buffer 对象输出流,每当发生异常时,buffer 里残存着上次异常信息,然后 jedis 把这个异常连接实例收回到连接池,那么重用该连接执行下次命令时,就会将上次没有发送的命令一起发送过去,所以才会抛出类型转换异常。

正确的姿势是,一旦存在命令执行异常,就要立马销毁这个连接!

所以个人觉得这是 SpringCache 的一个坑或者说是 SpringCacheCodis 配合使用的一个 bug

怎么解决了?

修改源码

第一 类型转换异常 在redis.clients.jedis.Transaction类中,exec 方法体添加了如下代码:

代码语言:javascript
代码运行次数:0
运行
复制
public List<Object> exec() {    ...    int retry = 0;    List<Object> unformatted = null;    while (retry > 3) {        try {            unformatted = client.getObjectMultiBulkReply();            break;        } catch (Exception e) {            ++retry;            try {                Thread.sleep(200L);            } catch (InterruptedException e1) {                e1.printStackTrace();            }        }    }    ...    return formatted;}

redis.clients.jedis.BinaryJedis类中,exists 方法体添加了如下代码:

代码语言:javascript
代码运行次数:0
运行
复制
public Long exists(final byte[]... keys) {    int retry = 0;    Long rtValue = 0l;    while (retry > 3) {        try {            rtValue = client.getIntegerReply();            break;        } catch (Exception e) {            ++retry;            try {                Thread.sleep(200L);            } catch (InterruptedException e1) {                e1.printStackTrace();            }        }    }}

org.springframework.data.redis.connection.jedis.JedisConnection 类的 convertJedisAccessException 方法加上如下代码:

代码语言:javascript
代码运行次数:0
运行
复制
protected DataAccessException convertJedisAccessException(Exception ex) {...    String errMsg = exception.getMessage();    // 让Codis把异常连接释放。    if (errMsg.contains("ERR handle request, command 'EXEC' is not allowed")) {    broken = true;    }    ..    return exception;}

根据 DEBUG 情况,有可能第一次调用 client.getObjectMultiBulkReply,就会抛出异常,所以遍历三次,问题解决。但这种改源码的方式不可取,需要针对 Redis 的不同数据类型(如 stringbyte等)进行源码修改。

第二、当缓存数据比较大时,报读取超时

不要用 @CacheConfig 注解方式,直接采用标准 get/set 方式 ,比如:

代码语言:javascript
代码运行次数:0
运行
复制
@GetMapping("/list")public R getList(@RequestParam(defaultValue = "6") int size) {    String key = "users:" + getUserId() + ":" + getPath().replace(",", "")+wid;    String jsonValue = redisClient.get(key);    List<UserEntity> list=new ArrayList<>();    if (StringUtils.isEmpty(jsonValue)) {        Map<String, Object> map = new HashMap<String, Object>();        map.put("user_id", getUserId());        map.put("path", getPath());        map.put("size", size);        list = service.getList(map, wid);        if (!list.isEmpty()) {            jsonValue = (new Gson().toJson(list));            redisClient.setex(key, 10 * 60, jsonValue);        }    }else {        list = new CommonConverter().toListDTOByJson(jsonValue, new TypeToken<List<UserEntity>>() {    }.getType());    }    return R.ok().data(list);}

总结

在并发很高的业务场景,可以使用 Redis 原生的 set\get 写入或读取缓存数据。 使用 SpringCache 的注解时,适合查询的数据尽量小并且数据值变化不大应用场景。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-06-16,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 架构荟萃 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 先奉上代码
  • 压测异常
  • 问题定位
  • 修改源码
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档