jedis连接池在高并发下的连接资源损坏问题 2021-08-12 15:09 jedis连接池在高并发下的连接资源损坏问题 > 包含问题情况与解决,但是不知道为啥。后面补充了使用lua脚本的解决方式。 ### 概述 在SpringBoot项目中使用jedis做秒杀实验,当连接池的MaxTotal和MaxIdle、MinIdle不一致时,会出现redis.clients.jedis.exceptions.JedisException: Could not return the broken resource to the pool的异常。 ### 测试代码 pom: ```xml <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.5.3</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.example</groupId> <artifactId>demo</artifactId> <version>0.0.1-SNAPSHOT</version> <name>demo</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- jedis --> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>3.6.3</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> <version>2.6.0</version> </dependency> <!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.11</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build> </project> ``` JedisPoolUtil: ```java package com.example.demo.redis; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPoolConfig; public class JedisPoolUtil { private static volatile JedisPool jedisPool = null; private JedisPoolUtil() { } public static JedisPool getJedisPoolInstance() { if (null == jedisPool) { synchronized (JedisPoolUtil.class) { if (null == jedisPool) { JedisPoolConfig poolConfig = new JedisPoolConfig(); poolConfig.setMaxTotal(64); poolConfig.setMaxIdle(32); poolConfig.setMinIdle(32); poolConfig.setBlockWhenExhausted(true); poolConfig.setMaxWaitMillis(1*1000); // ping PONG poolConfig.setTestOnBorrow(true); poolConfig.setTestOnReturn(true); jedisPool = new JedisPool(poolConfig, "127.0.0.1", 6379, 60000, "hanxu"); } } } return jedisPool; } // jedis使用后要手动关闭资源,因为jedis底层连接redis的是socket,占用资源,必须手动关闭。 public static void release(JedisPool jedisPool, Jedis jedis) { if (null != jedis) { jedisPool.returnResource(jedis); } } } ``` 测试Controller: ```java package com.example.demo.controller; import com.example.demo.redis.JedisPoolUtil; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import redis.clients.jedis.Transaction; import java.util.List; import java.util.Random; /** * @author: HanXu * on 2021/8/11 * Class description: 秒杀案例 */ @Slf4j @RestController public class RedisDemoController { // 储存剩余商品数量 private static final String spKey = "spKey:"; // 储存已经秒杀到的用户id private static final String userKey = "userKey:"; @PostMapping("doseckill") public boolean test(String spId) { String userId = getRundom(6); log.info("-----入参商品ip:{}, 用户id:{}", spId, userId); //开始秒杀 boolean result = doseckill3(spId, userId); //返回秒杀结果 return result; } public String getRundom(int num) { Random random = new Random(); StringBuilder sb = new StringBuilder(); for (int i = 0; i < num; i++) { int n = random.nextInt(10); sb.append(n); } return sb.toString(); } public boolean doseckill3(String spId, String userId) { JedisPool jedisPool = JedisPoolUtil.getJedisPoolInstance(); Jedis jedis = jedisPool.getResource(); try { // 前置检查 String kcCount = jedis.get(spKey + spId); if (StringUtils.isBlank(kcCount)) { log.error("秒杀还未开始"); return false; } if (Integer.parseInt(kcCount) <= 0) { log.error("秒杀已经结束了"); return false; } Boolean sismember = jedis.sismember(userKey + spId, userId); if (sismember) { log.info("已经秒杀到了,不能在买了"); return false; } // 秒杀业务处理 String watch = jedis.watch(spKey + spId); log.info("watch:{}", watch); Transaction multi = jedis.multi(); multi.decr(spKey + spId); multi.sadd(userKey + spId, userId); List<Object> result = multi.exec(); if (result == null || result.size() == 0) { log.error("秒杀失败,请稍等再来"); return false; } log.info("userId:{} 秒杀成功", userId); } catch (Exception e) { JedisPoolUtil.release(jedisPool, jedis); } finally { JedisPoolUtil.release(jedisPool, jedis); } return true; } } ``` ### 测试及问题 启动本地redis,执行`set spKey:10101 200`初始化商品数量;启动项目。 使用ab工具进行并发测试:ab -n 2000 -c 200 -p 123.txt -T application/x-www-form-urlencoded -k -r http://localhost:8080/doseckill,执行: (在打开的cmd窗口目录新建123.txt文件,内容为spId=10101) ![fErnoD.md.jpg](http://minio.riun.xyz/riun1/2021-08-12_UF8jYHf1ZgZ695sLkF.jpg) ![fErnoD.md.jpg](http://minio.riun.xyz/riun1/2021-08-12_UF8Q9od5yuGMW6XHIK.jpg) 可以看到有部分正常数据: ![fErnoD.md.jpg](http://minio.riun.xyz/riun1/2021-08-12_UFa6DQ38X9rHgJdwuU.jpg) 到执行到最后,就开始批量出现异常了: ![fErnoD.md.jpg](http://minio.riun.xyz/riun1/2021-08-12_UFaTJRc9QwqkVii62w.jpg) 报的异常是:redis.clients.jedis.exceptions.JedisException: Could not return the broken resource to the pool 这个异常大概意思就是连接资源已经损坏了,不能归还到池里。 看一下redis中的数据: ![fErnoD.md.jpg](http://minio.riun.xyz/riun1/2021-08-12_UFeufKetSkUTKjjPsr.jpg) 可以看到是正常的,商品库存已经清0;也存下了有200人秒杀成功。 ### 解决及疑问 那这个错误到底是咋回事呢?我不清楚,百度google也没查到。 最后想到了有的连接池中会建议将什么最大最小都设置为一样的值(比如JVM最大堆内存,最小堆内存),减少超过最小值时,还要扩展的开销。 于是我灵机一动,将jedis连接池的MaxTotal也改成了32,和MaxIdle、MinIdle一样: ```java poolConfig.setMaxTotal(32); poolConfig.setMaxIdle(32); poolConfig.setMinIdle(32); ``` 再重启项目;重设redis的值: ![fErnoD.md.jpg](http://minio.riun.xyz/riun1/2021-08-12_UFigOhfKLPNv4ZWhT0.jpg) ab测试: ![fErnoD.md.jpg](http://minio.riun.xyz/riun1/2021-08-12_UFjnrLYwHniRwLh6UG.jpg) 发现这会没有那个异常了: ![fErnoD.md.jpg](http://minio.riun.xyz/riun1/2021-08-12_UFjWCByCckEbPfDcaK.jpg) ![fErnoD.md.jpg](http://minio.riun.xyz/riun1/2021-08-12_UFklrMNy8Gci47QFcT.jpg) 再看redis数据,也都正常: ![fErnoD.md.jpg](http://minio.riun.xyz/riun1/2021-08-12_UFkZYm3cgic7UrdfgO.jpg) emmmm,真实不知道那些broken的连接是怎么搞出来的,又是为啥把MaxTotal、MaxIdle、MinIdle设置为一样的值就没有broken的连接了。 分割线 --- ### 使用Lua脚本的做法 > 使用lua脚本解决超卖和库存剩余问题,上面的方法测试中发现还是会有超卖剩余库存-1的情况,而且有时候回有许多库存剩余情况(2000多人抢购500个商品,最后还剩很多,很多人抢不到) pom:不变 JedisPoolUtil:改了下关闭连接的方法 ```java package com.example.demo.redis; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPoolConfig; public class JedisPoolUtil { private static volatile JedisPool jedisPool = null; private JedisPoolUtil() { } public static JedisPool getJedisPoolInstance() { if (null == jedisPool) { synchronized (JedisPoolUtil.class) { if (null == jedisPool) { JedisPoolConfig poolConfig = new JedisPoolConfig(); poolConfig.setMaxTotal(32); poolConfig.setMaxIdle(32); poolConfig.setMinIdle(32); poolConfig.setBlockWhenExhausted(true); poolConfig.setMaxWaitMillis(1*1000); // ping PONG poolConfig.setTestOnBorrow(true); poolConfig.setTestOnReturn(true); jedisPool = new JedisPool(poolConfig, "127.0.0.1", 6379, 60000, "hanxu"); } } } return jedisPool; } // jedis使用后要手动关闭资源,因为jedis底层连接redis的是socket,占用资源,必须手动关闭。 public static void release(JedisPool jedisPool, Jedis jedis) { if (null != jedis) { //jedisPool.returnResource(jedis); // 这里面有兼容当连接broken和非broken的关闭 jedis.close(); } } } ``` SecKill_redisByScript:使用Lua脚本一次性执行一个抢购流程内的所有命令,redis单线程,只能一个个执行完,相当于任务队列的模型。 ```java package com.example.demo.script; import com.example.demo.redis.JedisPoolUtil; import org.slf4j.LoggerFactory; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import java.io.IOException; public class SecKill_redisByScript { private static final org.slf4j.Logger logger =LoggerFactory.getLogger(SecKill_redisByScript.class) ; public static void main(String[] args) throws IOException { JedisPool jedispool = JedisPoolUtil.getJedisPoolInstance(); Jedis jedis=jedispool.getResource(); System.out.println(jedis.ping()); doSecKill("23423434","10101"); } public static String secKillScript ="local userid=KEYS[1];\r\n" + "local prodid=KEYS[2];\r\n" + "local qtkey='spKey:'..prodid;\r\n" + "local usersKey='userKey:'..prodid;\r\n" + "local userExists=redis.call(\"sismember\",usersKey,userid);\r\n" + "if tonumber(userExists)==1 then \r\n" + " return 2;\r\n" + "end\r\n" + "local num= redis.call(\"get\" ,qtkey);\r\n" + "if tonumber(num)<=0 then \r\n" + " return 0;\r\n" + "else \r\n" + " redis.call(\"decr\",qtkey);\r\n" + " redis.call(\"sadd\",usersKey,userid);\r\n" + "end\r\n" + "return 1" ; public static boolean doSecKill(String uid,String prodid) throws IOException { JedisPool jedispool = JedisPoolUtil.getJedisPoolInstance(); Jedis jedis=jedispool.getResource(); String sha1= jedis.scriptLoad(secKillScript); Object result= jedis.evalsha(sha1, 2, uid,prodid); String reString=String.valueOf(result); if ("0".equals( reString ) ) { System.err.println("已抢空!!"); }else if("1".equals( reString ) ) { System.out.println("抢购成功!!!!"); }else if("2".equals( reString ) ) { System.err.println("该用户已抢过!!"); }else{ System.err.println("抢购异常!!"); } jedis.close(); return true; } } ``` RedisDemoController:测试controller,新增了doSecKill_lua方法,使用jedis执行Lua脚本 ```java package com.example.demo.controller; import com.example.demo.redis.JedisPoolUtil; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import redis.clients.jedis.Transaction; import java.util.List; import java.util.Random; import static com.example.demo.script.SecKill_redisByScript.secKillScript; /** * @author: HanXu * on 2021/8/11 * Class description: 秒杀案例 */ @Slf4j @RestController public class RedisDemoController { // 储存剩余商品数量 private static final String spKey = "spKey:"; // 储存已经秒杀到的用户id private static final String userKey = "userKey:"; @PostMapping("doseckill") public boolean test(String spId) { String userId = getRundom(6); log.info("-----入参商品ip:{}, 用户id:{}", spId, userId); //开始秒杀 boolean result = doSecKill_lua(spId, userId); //返回秒杀结果 return result; } public String getRundom(int num) { Random random = new Random(); StringBuilder sb = new StringBuilder(); for (int i = 0; i < num; i++) { int n = random.nextInt(10); sb.append(n); } return sb.toString(); } public boolean doseckill3(String spId, String userId) { JedisPool jedisPool = JedisPoolUtil.getJedisPoolInstance(); Jedis jedis = jedisPool.getResource(); try { // 前置检查 String kcCount = jedis.get(spKey + spId); if (StringUtils.isBlank(kcCount)) { log.error("秒杀还未开始"); return false; } if (Integer.parseInt(kcCount) <= 0) { log.error("秒杀已经结束了"); return false; } Boolean sismember = jedis.sismember(userKey + spId, userId); if (sismember) { log.info("已经秒杀到了,不能在买了"); return false; } // 秒杀业务处理 String watch = jedis.watch(spKey + spId); log.info("watch:{}", watch); Transaction multi = jedis.multi(); multi.decr(spKey + spId); multi.sadd(userKey + spId, userId); List<Object> result = multi.exec(); if (result == null || result.size() == 0) { log.error("秒杀失败,请稍等再来"); return false; } log.info("userId:{} 秒杀成功", userId); } catch (Exception e) { log.error("抛出异常:{}", e); } finally { JedisPoolUtil.release(jedisPool, jedis); } return true; } public boolean doSecKill_lua(String spId, String userId) { JedisPool jedispool = JedisPoolUtil.getJedisPoolInstance(); Jedis jedis = jedispool.getResource(); String sha1 = jedis.scriptLoad(secKillScript); Object result = jedis.evalsha(sha1, 2, userId, spId); String reString = String.valueOf(result); if ("0".equals( reString )) { log.error("秒杀已经结束了"); } else if ("1".equals( reString )) { log.info("userId:{} 秒杀成功", userId); } else if ("2".equals( reString )) { log.info("已经秒杀到了,不能在买了"); } else { log.info("秒杀异常"); } JedisPoolUtil.release(jedispool, jedis); return true; } } ``` 经测试(还是同一句ab测试命令),库存初始值为10、99、100、200、300、400、500、1000都能正常抢购(不会超卖,也不会库存剩余)。也不会出现异常信息(因为当连接broken的时候,jedis.close()兼容了,所以不会有异常打印了)。 ![fErnoD.md.jpg](http://minio.riun.xyz/riun1/2021-08-12_UGXcumPrxImk7Ry4nF.jpg) 这样问题看起来解决了,可我还是不明白为啥会出现有连接broken的情况。 --END--
发表评论