How to use redis in java to deal with e-commerce platform, spike, buy and oversold

1. It’s not long since I first came to the company. I saw that a former colleague of the company wrote such a piece of code, which is posted below:

1. This is the part where the following code is called in a method:

if (!this.checkSoldCountByRedisDate(key, limitCount, buyCount, endDate)) {// 标注10:  
                throw new ServiceException("您购买的商品【" + commodityTitle + "】,数量已达到活动限购量");  
            }  

2. The following is the method of judging oversold:


/** 根据缓存数据查询是否卖超 */
	//标注:1;synchronized 
	private synchronized boolean checkSoldCountByRedisDate(String key, int limitCount, int buyCount, Date endDate) {
		boolean flag = false;
		if (redisUtil.exists(key)) {//标注:2;redisUtil.exists(key)
			Integer soldCount = (int) redisUtil.get(key);//标注:3;redisUtil.get(key)
			Integer totalSoldCount = soldCount + buyCount;
			if (limitCount > (totalSoldCount)) {
				flag = false;//标注:4;flag = false
			} else {
				if (redisUtil.tryLock(key, 80)) {//标注:5;rdisUtil.tryLock(key, 80)

					redisUtil.remove(key);// 解锁 //标注:6;redisUtil.remove(key)

					redisUtil.set(key, totalSoldCount);//标注:7;redisUtil.set(key, totalSoldCount)

					flag = true;
				} else {
					throw new ServiceException("活动太火爆啦,请稍后重试");
				}
			}
		} else {
			//标注:8;redisUtil.set(key, new String("buyCount"), DateUtil.diffDateTime(endDate, new Date()))
			redisUtil.set(key, new String("buyCount"), DateUtil.diffDateTime(endDate, new Date()));
			flag = false;
		}
		return flag;
	}

3. The methods in the redisUtil class mentioned above, in which redisTemplate is org.springframework.data.redis.core.RedisTemplate; if you don’t understand this, you can go to the Internet to find the relevant documents of spring-data-redis.jar and post them Related methods used by redisUtil:


/**
	 * 判断缓存中是否有对应的value
	 * 
	 * @param key
	 * @return
	 */
	public boolean exists(final String key) {
		return redisTemplate.hasKey(key);
	}
	/**
	 * 将键值对设定一个指定的时间timeout.
	 * 
	 * @param key
	 * @param timeout
	 *            键值对缓存的时间,单位是毫秒
	 * @return 设置成功返回true,否则返回false
	 */
	public boolean tryLock(String key, long timeout) {
		boolean isSuccess = redisTemplate.opsForValue().setIfAbsent(key, "");
		if (isSuccess) {//标注:9;redisTemplate.expire

			redisTemplate.expire(key, timeout, TimeUnit.MILLISECONDS);
		}
		return isSuccess;
	}
	/**
	 * 读取缓存
	 * 
	 * @param key
	 * @return
	 */
	public Object get(final String key) {
		Object result = null;
		ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
		result = operations.get(key);
		return result;
	}
	/**
	 * 删除对应的value
	 * 
	 * @param key
	 */
	public void remove(final String key) {
		if (exists(key)) {
			redisTemplate.delete(key);
		}
	}
	/**
	 * 写入缓存
	 * 
	 * @param key
	 * @param value
	 * @return
	 */
	public boolean set(final String key, Object value) {
		return set(key, value, null);
	}
	/**
	 * 
	 * @Title: set
	 * @Description: 写入缓存带有效期
	 * @param key
	 * @param value
	 * @param expireTime
	 * @return boolean    返回类型
	 * @throws
	 */
	public boolean set(final String key, Object value, Long expireTime) {
		boolean result = false;
		try {
			ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
			operations.set(key, value);
			if (expireTime != null) {
				redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
			}
			result = true;
		} catch (Exception e) {
			e.printStackTrace();
		}
		return result;
	}

4. The DateUtil class mentioned above, I will send it in the form of a file below!
 

 

2. Now let's interpret this code to see the author's intentions and where the problem lies, so as to help more people understand how to deal with oversold situations that occur during snap purchases and spikes on e-commerce platforms

1. Parameter description, the above checkSoldCountByRedisDate method has 4 parameters:

 key: the count of the purchased quantity, the key placed in the redis cache;

 limitCount: find the source code, the original comment is: total purchase limit;

 buyCount: the quantity to be purchased for the current request;

 endDate: the end time of the event;

2. Through the above annotations, let's parse the original author's intention:

Note 1: I want to achieve synchronization through the synchronized keyword, which seems to be no problem

Note 2: Judging whether the key exists through the redisUtil.exists method, it seems that there is no problem

Note 3: redisUtil.get(key) gets the total number of purchases, which seems to be no problem

Note 4: When the user's total purchase quantity < total purchase limit, it returns false, which seems to be a simple judgment

Note 5: I want to lock through redisUtil.tryLock to achieve oversold processing, and the code behind it does not seem to have any problems.
Note 6: Note 5 is locked, then unlocked through redisUtil.remove, it seems logical

Note 7: Record user purchases through redisUtil.set, the original author should mean this

Note 8: If the key judged by Note 2 does not exist, create a key here. It seems that the code is also written like this

Note 9: I think the original author did not want deadlock, and used redisTemplate.expire as lock timeout to release deadlock, which is possible

3. Based on the analysis of the author's intention above, let's take a look. If there seems to be no problem, whether it is really no problem! Ha ha. . , so cheap!

Let's take a look at each annotation and the problems that may arise:

Note 1: synchronized keyword, in the case of distributed high concurrency, synchronization processing cannot be achieved, and you will know it if you don’t believe it;

Then the problems that may arise are:

Now the same user initiates request A, B or different users initiate request A, B, it will enter the checkSoldCountByRedisDate method at the same time and execute

 

Note 2: When the panic buying starts, A and B request to be the first to panic purchase at the same time, and enter the checkSoldCountByRedisDate method.

A and B requests are judged by the redisUtil.exists method that the key does not exist,

Thus, the part marked 8 is executed, and at the same time, an action of creating a key is performed;

It's really a pit! The first one to start snapping up can't get it!

 

Annotation 3: When requests A and B arrive at the same time, suppose: the current purchase buyCount parameter of requests A and B is 40, and soldCount=50 and limitCount=100 obtained from annotation 3,

At this time, the totalSoldCount obtained by request A and B are both 90, and the problem comes again

 

Annotation 4: limitCount > (totalSoldCount): totalSoldCount=90, limitCount=100, when the flag is equal to false,

Return to the location marked 10 and throw exception information (throw new ServiceException("The commodity you purchased [" + commodityTitle + "], the quantity has reached the activity purchase limit"););

Request A, B did not grab the goods. what the hell? A total of 90 purchases, and the total purchase limit is 100. This throws an exception and reaches the event purchase limit. I can't understand it.

 

Annotation 5: When locking here, if the execution reaches annotation 9: isSuccess=true, the client is interrupted, and the code after annotation 9 is not executed.

Damn, deadlock appeared! No one wants to grab it

 

Let's assume that when A request arrives slightly slower than B request, the buyCount parameter of A and B requests is 40, and the soldCount=50 and limitCount=100 obtained by marking 3 are used to execute the code in the else.

That is, in the checkSoldCountByRedisDate method:


else {
				if (redisUtil.tryLock(key, 80)) {

					redisUtil.remove(key);// 解锁

					redisUtil.set(key, totalSoldCount);

					flag = true;
				} else {
					throw new ServiceException("活动太火爆啦,请稍后重试");
				}
			}

Labels 6 and 7: A request arrives first, assuming that the lock is successful and the lock is released successfully. After the set key value is 90, the B request is also successfully locked and the lock is released successfully. Set the key value to 90.

So here comes the question:

A and B each bought 40, the original purchase amount was 50, and the total limit was 100, 40+40+50=130, but it was successfully executed when it was larger than the maximum limit. I went, how did the company explain to the customer!

 

 

It's early in the morning, so I won't say much nonsense. The key depends on how to deal with the problem. Let's go to the code directly! I don’t look at the place where the call is made. In fact, there are only a few lines of code. If there are comments, you can understand it at a glance:

/**
	 * 
	 * 雷------2016年6月17日
	 * 
	 * @Title: checkSoldCountByRedisDate
	 * @Description: 抢购的计数处理(用于处理超卖)
	 * @param @param key 购买计数的key
	 * @param @param limitCount 总的限购数量
	 * @param @param buyCount 当前购买数量
	 * @param @param endDate 抢购结束时间
	 * @param @param lock 锁的名称与unDieLock方法的lock相同
	 * @param @param expire 锁占有的时长(毫秒)
	 * @param @return 设定文件
	 * @return boolean 返回类型
	 * @throws
	 */
	private boolean checkSoldCountByRedisDate(String key, int limitCount, int buyCount, Date endDate, String lock, int expire) {
		boolean check = false;
		if (this.lock(lock, expire)) {
			Integer soldCount = (Integer) redisUtil.get(key);
			Integer totalSoldCount = (soldCount == null ? 0 : soldCount) + buyCount;
			if (totalSoldCount <= limitCount) {
				redisUtil.set(key, totalSoldCount, DateUtil.diffDateTime(endDate, new Date()));
				check = true;
			}
			redisUtil.remove(lock);
		} else {
			if (this.unDieLock(lock)) {
				logger.info("解决了出现的死锁");
			} else {
				throw new ServiceException("活动太火爆啦,请稍后重试");
			}
		}
		return check;
	}

	/**
	 * 
	 * 雷------2016年6月17日
	 * 
	 * @Title: lock
	 * @Description: 加锁机制
	 * @param @param lock 锁的名称
	 * @param @param expire 锁占有的时长(毫秒)
	 * @param @return 设定文件
	 * @return Boolean 返回类型
	 * @throws
	 */
	@SuppressWarnings("unchecked")
	public Boolean lock(final String lock, final int expire) {
		return (Boolean) redisTemplate.execute(new RedisCallback<Boolean>() {
			@Override
			public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
				boolean locked = false;
				byte[] lockValue = redisTemplate.getValueSerializer().serialize(DateUtil.getDateAddMillSecond(null, expire));
				byte[] lockName = redisTemplate.getStringSerializer().serialize(lock);
				locked = connection.setNX(lockName, lockValue);
				if (locked)
					connection.expire(lockName, TimeoutUtils.toSeconds(expire, TimeUnit.MILLISECONDS));
				return locked;
			}
		});
	}

	/**
	 * 
	 * 雷------2016年6月17日
	 * 
	 * @Title: unDieLock
	 * @Description: 处理发生的死锁
	 * @param @param lock 是锁的名称
	 * @param @return 设定文件
	 * @return Boolean 返回类型
	 * @throws
	 */
	@SuppressWarnings("unchecked")
	public Boolean unDieLock(final String lock) {
		boolean unLock = false;
		Date lockValue = (Date) redisTemplate.opsForValue().get(lock);
		if (lockValue != null && lockValue.getTime() <= (new Date().getTime())) {
			redisTemplate.delete(lock);
			unLock = true;
		}
		return unLock;
	}

The methods of the relevant DateUtil classes used in the above methods will be posted below:

/**
     * 日期相减(返回秒值)
     * @param date Date
     * @param date1 Date
     * @return int
     * @author 
     */
    public static Long diffDateTime(Date date, Date date1) {
        return (Long) ((getMillis(date) - getMillis(date1))/1000);
    }
    public static long getMillis(Date date) {
        Calendar c = Calendar.getInstance();
        c.setTime(date);
        return c.getTimeInMillis();
    }
 	/**
	 * 获取 指定日期 后 指定毫秒后的 Date
	 * 
	 * @param date
	 * @param millSecond
	 * @return
	 */
	public static Date getDateAddMillSecond(Date date, int millSecond) {
		Calendar cal = Calendar.getInstance();
		if (null != date) {// 没有 就取当前时间
			cal.setTime(date);
		}
		cal.add(Calendar.MILLISECOND, millSecond);
		return cal.getTime();
	}

It ends here!

New additions:

import java.util.Calendar;
import java.util.Date;
import java.util.concurrent.TimeUnit;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.TimeoutUtils;
import org.springframework.stereotype.Component;

import cn.mindmedia.jeemind.framework.utils.redis.RedisUtils;
import cn.mindmedia.jeemind.utils.DateUtils;

/**
 * @ClassName: LockRetry
 * @Description: 此功能只用于促销组
 * @author 雷
 * @date 2017年7月29日 上午11:54:54
 * 
 */
@SuppressWarnings("rawtypes")
@Component("lockRetry")
public class LockRetry {
	private Logger logger = LoggerFactory.getLogger(getClass());
	@Autowired
	private RedisTemplate redisTemplate;

	/**
	 * 
	 * @Title: retry
	 * @Description: 重入锁
	 * @author 雷 
	 * @param @param lock 名称
	 * @param @param expire 锁定时长(秒),建议10秒内
	 * @param @param num 取锁重试试数,建议不大于3
	 * @param @param interval 重试时长
	 * @param @param forceLock 强制取锁,不建议;
	 * @param @return
	 * @param @throws Exception    设定文件
	 * @return Boolean    返回类型
	 * @throws
	 */
	@SuppressWarnings("unchecked")
	public Boolean retryLock(final String lock, final int expire, final int num, final long interval, final boolean forceLock) throws Exception {
		Date lockValue = (Date) redisTemplate.opsForValue().get(lock);
		if (forceLock) {
			RedisUtils.remove(lock);
		}
		if (num <= 0) {
			if (null != lockValue && lockValue.getTime() >= (new Date().getTime())) {
				logger.debug(String.valueOf((lockValue.getTime() - new Date().getTime())));
				Thread.sleep(lockValue.getTime() - new Date().getTime());
				RedisUtils.remove(lock);
				return retryLock(lock, expire, 1, interval, forceLock);
			}
			return false;
		} else {
			return (Boolean) redisTemplate.execute(new RedisCallback<Boolean>() {
				@Override
				public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
					boolean locked = false;
					byte[] lockValue = redisTemplate.getValueSerializer().serialize(DateUtils.getDateAdd(null, expire, Calendar.SECOND));
					byte[] lockName = redisTemplate.getStringSerializer().serialize(lock);
					logger.debug(lockValue.toString());
					locked = connection.setNX(lockName, lockValue);
					if (locked)
						return connection.expire(lockName, TimeoutUtils.toSeconds(expire, TimeUnit.SECONDS));
					else {
						try {
							Thread.sleep(interval);
							return retryLock(lock, expire, num - 1, interval, forceLock);
						} catch (Exception e) {
							e.printStackTrace();
							return locked;
						}

					}
				}
			});
		}

	}
}

 

	/**
	 * 
	 * @Title: getDateAddMillSecond
	 * @Description: (TODO)取将来时间
	 * @author 雷 
	 * @param @param date
	 * @param @param millSecond
	 * @param @return    设定文件
	 * @return Date    返回类型
	 * @throws
	 */
	public static Date getDateAdd(Date date, int expire, int idate) {
		Calendar calendar = Calendar.getInstance();
		if (null != date) {// 默认当前时间
			calendar.setTime(date);
		}
		calendar.add(idate, expire);
		return calendar.getTime();
	}

	/**
	 * 删除对应的value
	 * @param key
	 */
	public static void remove(final String key) {
		if (exists(key)) {
			redisTemplate.delete(key);
		}
	}

	/**
	 * 
	 * @Title: getDateAddMillSecond
	 * @Description: (TODO)取将来时间
	 * @author 雷 
	 * @param @param date
	 * @param @param millSecond
	 * @param @return    设定文件
	 * @return Date    返回类型
	 * @throws
	 */
	public static Date getDateAdd(Date date, int expire, int idate) {
		Calendar calendar = Calendar.getInstance();
		if (null != date) {// 默认当前时间
			calendar.setTime(date);
		}
		calendar.add(idate, expire);
		return calendar.getTime();
	}
	/**
	 * 删除对应的value
	 * @param key
	 */
	public static void remove(final String key) {
		if (exists(key)) {
			redisTemplate.delete(key);
		}
	}
	/**
	 * 判断缓存中是否有对应的value
	 * @param key
	 * @return
	 */
	public static boolean exists(final String key) {
		return stringRedisTemplate.hasKey(key);
	}
private static StringRedisTemplate stringRedisTemplate = ((StringRedisTemplate) SpringContextHolder.getBean("stringRedisTemplate"));  

 

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325824842&siteId=291194637