Project scenario:
Spring boot project write off card secret recharge in concurrent test, a card secret is successfully recharged repeatedlyProblem Description:
The first time I wrote, I didn't consider so much, so I added several logic to verify the card secret status in the business layer. As long as the card secret status is OK, I can directly modify the user's balance
if(ObjectUtil.isNull(cards)){ return failure("The card does not exist"); } if(cards.getIsEnable() == false){ return failure("The card has been disabled,Please contact the administrator"); } if(cards.getStatus() == 3){ return failure("The card has been used,If the balance is not increased,Please contact the administrator"); }
<update id="updateUserLeftAmount"> UPDATE sys_user SET left_amount = #{add} where id = #{id} </update>
result:
In the case of 20 concurrent requests, 100 fast flushes directly into 900 fast, because the interface is requested by a large number of duplicate data
Solution:
Since the interface is repeatedly requested, I will directly add an aop interception interface to prevent repeated requests
The slice logic verifies the number of requests for this interface from the same ip
@Aspect @Component public class LimitRequestAspect { private static ConcurrentHashMap<String, ExpiringMap<String, Integer>> book = new ConcurrentHashMap<>(); // Define tangent point // Let all methods annotated with @ LimitRequest execute facet methods @Pointcut("@annotation(limitRequest)") public void excudeService(LimitRequest limitRequest) { } @Around("excudeService(limitRequest)") public Object doAround(ProceedingJoinPoint pjp, LimitRequest limitRequest) throws Throwable { // Get request object RequestAttributes ra = RequestContextHolder.getRequestAttributes(); ServletRequestAttributes sra = (ServletRequestAttributes) ra; HttpServletRequest request = sra.getRequest(); // Gets the Map object. If not, the default value is returned // The first parameter is key, and the second parameter is the default value ExpiringMap<String, Integer> uc = book.getOrDefault(request.getRequestURI(), ExpiringMap.builder().variableExpiration().build()); Integer uCount = uc.getOrDefault(request.getRemoteAddr(), 0); if (uCount >= limitRequest.count()) { // If the number of times is exceeded, the target method will not be executed throw new BaseException("Do not request frequently"); // return "the number of interface requests exceeded"; } else if (uCount == 0){ // Set the effective time on the first request // /** Expires entries based on when they were last accessed */ // ACCESSED, // /** Expires entries based on when they were created */ // CREATED; uc.put(request.getRemoteAddr(), uCount + 1, ExpirationPolicy.CREATED, limitRequest.time(), TimeUnit.MILLISECONDS); } else { // If the number of times is not exceeded, add one to the record uc.put(request.getRemoteAddr(), uCount + 1); } book.put(request.getRequestURI(), uc); // The value of result is the return value of the intercepted method Object result = pjp.proceed(); return result; }
Annotation
@Documented @Target(ElementType.METHOD) // Note that the annotation can only be placed on the method @Retention(RetentionPolicy.RUNTIME) public @interface LimitRequest { long time() default 6000; // Limit time unit: milliseconds int count() default 1; // Number of requests allowed }
result:
Although the number of interface requests is intercepted, it does not prevent the problem of repeated recharge of the card secret from being used concurrently. Then the boss told me to add a lock and sent me a picture
Then I added a version field according to the description of the picture and modified my sql
<update id="updateUserLeftAmount"> UPDATE sys_user SET left_amount = #{add},version = version+1 where left_amount = #{leftAmount} and id = #{id} and version=#{version} </update>
Retest:
Or will there be repeated rechargeSolution:
Ask the boss. The boss sent me two more pictures about redis. Let me use the single thread mechanism of redis to verifyThen I integrated jedis and modified the verification logic code as follows
Before modifying the user's balance, use the setnx operation of redis to save the user's unique id, set an expiration time, and then expire to query whether it exists. If it exists, return
JedisUtil jedisUtil = JedisUtil.getInstance(); Long verifyCarmi = jedisUtil.setnxWithTimeOut("verifyCarmi", user.getId(), 10); if(verifyCarmi == 0){ map.put("msg","Do not recharge repeatedly"); map.put("bool",false); return map; }
jedisUtil. Code for setnxwithtimeout
/** * Add a key value pair. If the key exists, it will not be added. If it does not exist, set the validity period of the key after adding * @param key * @param value * @param timeOut */ public Long setnxWithTimeOut(String key,String value,int timeOut){ Jedis jedis = getJedis(); long expire = 2; if(0!=jedis.setnx(key, value)){ expire = jedis.expire(key, timeOut); } returnJedis(jedis); return expire; }
Retest:
No problem. You can only recharge it once under concurrent requests. It's all for redis's verification return
redis is very useful 😊
Thank you for learning