redis高并发抽奖(2)

本篇文章是基于原先写的一篇文章 进行的另一种补充方案,原先的每次抽奖都会进行计算一次概率和奖品,内存消耗比较大,不太建议使用!本次方案借鉴了腾讯红包先计算在分发策略极大的提高系统的效率和减少内存的消耗。下面我给大家介绍一下本次抽奖补充方案内容。


一、思路

1.表结构:

drop table T_LOTTERY_MANAGEMENT cascade constraints;

/*==============================================================*/
/* Table: T_LOTTERY_MANAGEMENT      抽奖活动管理                */
/*==============================================================*/
create table T_LOTTERY_MANAGEMENT 
(
   LOTTERY_ID           VARCHAR2(50)         not null,
   LOTTERY_NAME         VARCHAR2(50),
   SPONSOR_POINT        INT,
   LOTTERY_TYPE         INT,
   LOTTERY_POINT        INT,
   START_DATE           DATE,
   STOP_DATE            DATE,
   LOTTERY_TOTAIL_NUM   INT,
   LOTTERY_SURPLUS_NUM  INT,
   LOTTERY_NUM          INT,
   LOTTERY_OVER         INT,
   CREATE_TIME          DATE,
   constraint PK_T_LOTTERY_MANAGEMENT primary key (LOTTERY_ID)
);

comment on column T_LOTTERY_MANAGEMENT.LOTTERY_ID is
'抽奖活动编号';

comment on column T_LOTTERY_MANAGEMENT.LOTTERY_NAME is
'活动名称';

comment on column T_LOTTERY_MANAGEMENT.SPONSOR_POINT is
'公司赞助积分';

comment on column T_LOTTERY_MANAGEMENT.LOTTERY_TYPE is
'公司赞助(0否1是)';

comment on column T_LOTTERY_MANAGEMENT.LOTTERY_POINT is
'每次抽奖消耗积分';

comment on column T_LOTTERY_MANAGEMENT.START_DATE is
'活动开始时间';

comment on column T_LOTTERY_MANAGEMENT.STOP_DATE is
'活动结束时间';

comment on column T_LOTTERY_MANAGEMENT.LOTTERY_TOTAIL_NUM is
'抽奖总次数';

comment on column T_LOTTERY_MANAGEMENT.LOTTERY_SURPLUS_NUM is
'抽奖剩余次数';

comment on column T_LOTTERY_MANAGEMENT.LOTTERY_NUM is
'已抽奖次数';

comment on column T_LOTTERY_MANAGEMENT.LOTTERY_OVER is
'抽奖次数是否耗尽(0未耗尽1已耗尽)';

comment on column T_LOTTERY_MANAGEMENT.CREATE_TIME is
'创建时间';

drop table T_PRIZE cascade constraints;

/*==============================================================*/
/* Table: T_PRIZE                   奖品表                      */
/*==============================================================*/
create table T_PRIZE 
(
   ID                   INT                  not null,
   LOTTERY_ID           VARCHAR(50),
   PRIZE_TYPE           INT,
   CLASS_ID             INT,
   PRODUCT_ID           VARCHAR(50),
   PRODUCT_ATTRIBUTES_ID VARCHAR(50),
   PRIZE_NAME           VARCHAR(50),
   PRIZE_PRICE          DECIMAL(7,2),
   POINT                INT,
   PRIZE_NUM            INT,
   PRIZE_TOTAIL_POINT   INT,
   PRIZE_LEV            INT,
   PRIZE_PROBABILITY    DECIMAL(6,4),
   REMARKS              CLOB,
   CREATE_TIME          DATE,
   constraint PK_T_PRIZE primary key (ID)
);

comment on column T_PRIZE.ID is
'自增ID';

comment on column T_PRIZE.LOTTERY_ID is
'抽奖活动编号';

comment on column T_PRIZE.PRIZE_TYPE is
'奖品类型';

comment on column T_PRIZE.CLASS_ID is
'虚拟奖品分类ID';

comment on column T_PRIZE.PRODUCT_ID is
'虚拟奖品ID';

comment on column T_PRIZE.PRODUCT_ATTRIBUTES_ID is
'奖品属性ID';

comment on column T_PRIZE.PRIZE_NAME is
'奖品名称';

comment on column T_PRIZE.PRIZE_PRICE is
'剩余价值';

comment on column T_PRIZE.POINT is
'单个奖品价值(能力豆)';

comment on column T_PRIZE.PRIZE_NUM is
'奖品数量';

comment on column T_PRIZE.PRIZE_TOTAIL_POINT is
'奖品总价值(能力豆)';

comment on column T_PRIZE.PRIZE_LEV is
'奖品等级';

comment on column T_PRIZE.PRIZE_PROBABILITY is
'中奖概率';

comment on column T_PRIZE.REMARKS is
'备注';

comment on column T_PRIZE.CREATE_TIME is
'创建时间';


drop table T_WINNING_RECORD cascade constraints;

/*==============================================================*/
/* Table: T_WINNING_RECORD                 中奖记录表           */
/*==============================================================*/
create table T_WINNING_RECORD 
(
   ID                   INT                  not null,
   PRIZE_ID             VARCHAR(50),
   PRIZE_NAME           VARCHAR(50),
   USER_ID              VARCHAR(50),
   USER_NAME            VARCHAR(50),
   PRIZE_NUM            INT,
   CREATE_TIME          DATE,
   FIELD1               VARCHAR2(255),
   FIELD2               VARCHAR2(255),
   FIELD3               VARCHAR2(255),
   constraint PK_T_WINNING_RECORD primary key (ID)
);

comment on column T_WINNING_RECORD.ID is
'自增ID';

comment on column T_WINNING_RECORD.PRIZE_ID is
'奖品ID';

comment on column T_WINNING_RECORD.PRIZE_NAME is
'奖品名称';

comment on column T_WINNING_RECORD.USER_ID is
'中奖用户ID';

comment on column T_WINNING_RECORD.USER_NAME is
'中奖用户名称';

comment on column T_WINNING_RECORD.PRIZE_NUM is
'奖品数量';

comment on column T_WINNING_RECORD.CREATE_TIME is
'中奖时间';

comment on column T_WINNING_RECORD.FIELD1 is
'备用字段1';

comment on column T_WINNING_RECORD.FIELD2 is
'备用字段2';

comment on column T_WINNING_RECORD.FIELD3 is
'备用字段3';

2.概率规则:

                  单个产品数量/总产品数量=单个产品概率。

                  每个产品概率相加=1,如果没有等于1数据有问题。

                  抽奖用户分不同lev等级,可以根据用户不同的lev来限制用户每天或每轮活动的抽奖次数。

3.设计思路:

                 本次补充方案和上一次的方案变化非常大(基本推翻重干。。。)
                   首先我们从一个抽奖活动,变成了多个抽奖活动,每个活动分为两种模式(赞助和非赞助)非赞助:抽奖次数 X 每次抽奖的积分 = 奖品的数量X奖品的价值;赞助:奖品的数量 X 奖品的价值 - 抽奖次数 X 每次抽奖的积分 = 公司赞助的积分;活动与活动之间的时间段是不能相交叉的,这就保证了我们在一个时间点最多只有一个抽奖活动在推广生效。每个活动有自己专属的活动奖品和自己的抽奖限制次数规则。基于上述规则,我从最小数 Min = 1,最大数 Max = 抽奖总次数,这个范围生成了有效的奖品总数量的随机数做奖品唯一兑换序列码。因为兑换码就在MIN ~ MAX 范围里面,用户每次抽奖我都会用一个活动特定标识(我这里用的是活动的ID,和redis单线程的特性)去自增1,然后拿自增后的数数去匹配兑换序列码,判断用户是否中奖,同时能判断本轮活动是否已经结果,同时技术要求和逻辑也没上一篇那么复杂,一举多得
页面效果如下:

抽奖活动列表:


抽奖活动奖品列表:

redis高并发抽奖(2)_第1张图片

4.技术难点:
                    一、奖品唯一性

                     二、 奖品的随机性

                    针对上述两点:我采用了
hashSet数据结构去做每个奖品的兑换序列码,保证每个奖品的唯一性。但是因为hashSet针对存储的纯数字的伪随机(例如:我存一个1-1000的范围800个随机兑换序列码,大多数都是从1 2 3 4 5 7  8...这种从小到大的排序去存储,偶尔夹杂着几个不规律的数字),我特意用一个对象把随机码和奖品标识ID封装成一个model 重写了它的hashCode算法 和 equals 方法 在保证随机兑换序列码唯一性的同事又保证了奖品的排列随机性。


5.注意事项:

                 关于DataObject:这是我们这边封装的一个对象,跟泛型差不多,直接替换自己的model就可以了

二、参考代码

package com.rfg.shop;

public class Prize extends Object {

	
	//奖品ID
	private String id;

	
	//唯一兑换序列码
	private String val;

	public String getId() {
		return id;
	}

	public void setId(String id) {
		this.id = id;
	}

	public String getVal() {
		return val;
	}

	public void setVal(String val) {
		this.val = val;
	}

	Prize(String id, String val) {
		super();
		this.id = id;
		this.val = val;
	}

	Prize() {
		super();
	}

	@Override
	public int hashCode() {
//		System.err.println(this + "..........hashCode=》" + val.hashCode());
		return val.hashCode();
	}

	@Override
	public boolean equals(Object obj) {
//		System.err.println(this + "===========" + obj);
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Prize other = (Prize) obj;
		if (val == null) {
			if (other.val != null)
				return false;
		} else if (!val.equals(other.val))
			return false;
		return true;
	}
	
	@Override
	public String toString() {
		return id + ":" + val;
	}

}
package com.rfg.shop;

import java.math.BigDecimal;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Set;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.Pipeline;

import com.eos.system.annotation.Bizlet;
import commonj.sdo.DataObject;

/**
 * 
 * @author XiongYC
 *
 */
public class Common {

	public static final String PRIZE_RULE = "prizeRule";
	
	public static Jedis jedis = RedisPoolUtils.getJedis();
	
	/**
	 * 计算总价值和中奖概率
	 * @param arr  奖品数组
	 * @param data  抽奖活动obj
	 * @return
	 */
	@Bizlet
	public static HashMap calculation(DataObject[] arr ,DataObject data) {
		
		HashMap map =  new HashMap();
		
		//本次抽奖活动每次抽奖消耗能量豆、
		int lotteryPoint = data.getInt("lotteryPoint");             
		
		//本轮抽奖活动建构总价值能量豆
		int tailNum = 0;  
		
		//本轮抽奖公司赞助能量豆
		int sponsorPoint = 0;
		
		//非赞助本轮抽奖总次数
		int lotteryTotailNum_F = 0;
		
		//非赞助本轮抽奖总次数
		int lotteryTotailNum_T = 0; 
		
		//纪律本轮循环谢谢惠顾的下标
		int index = 0;
		
		//非赞助本轮抽奖总次数
		int NotZeroPointPrizeNum = 0; 
		
		for (int i = 0; i < arr.length; i++) {
			
			//单个奖品的数量
			int prizeNum = arr[i].getInt("prizeNum");
			
			lotteryTotailNum_T +=prizeNum;
			
			//计算单个奖品总价值能量豆
			int a = prizeNum * arr[i].getInt("point");
			
			//本轮次抽奖活动建构总价值能量豆
			tailNum += a;  
			
			if (a>0) {
				//纪律有效间奖品数量
				NotZeroPointPrizeNum+=prizeNum;
			} else {
				index = i;
			}
			
			arr[i].set("prizeTotailPoint",a ); 
		}
		
		lotteryTotailNum_F = tailNum/lotteryPoint;
		
		//0非赞助1赞助
		if (0 == data.getInt("lotteryType")) {
			//
			data.setLong("lotteryTotailNum", lotteryTotailNum_F);
			
			arr[index].setInt("prizeNum", lotteryTotailNum_F - NotZeroPointPrizeNum); 
		} else {
			data.setLong("lotteryTotailNum", lotteryTotailNum_T);
			
			sponsorPoint = (lotteryTotailNum_F - lotteryTotailNum_T)*lotteryPoint;
			
		}
		
		//计算每个奖品的概率
		for (int i = 0; i < arr.length; i++) {
			
//			//单个奖品的数量
			int prizeNum = arr[i].getInt("prizeNum");
			
			//精确到小数点后4位
			BigDecimal d = new BigDecimal(prizeNum).divide(new BigDecimal(lotteryTotailNum_T), 4, BigDecimal.ROUND_HALF_UP);
			
//			//本轮次抽奖活动建构总价值能量豆
			arr[i].setBigDecimal("prizeProbability",d); 
		}
		
		data.setLong("sponsorPoint", sponsorPoint);
		
		
		//初始化
		data.setLong("lotteryNum", 0);
		data.setLong("lotteryOver", 0);
		data.setLong("lotterySurplusNum", data.getInt("lotteryTotailNum"));
		
		//奖品数组
		map.put("arr", arr);
		
		//奖品活动
		map.put("data", data);
		
		//缓存建构数据
		CacheLottery.cachePrizeListByLotteryId(data.getString("lotteryId"), data.getInt("lotteryTotailNum"), arr);
		
		return map;
	}
	
	/**
	 * 缓存抽奖规则
	 * @param arr
	 * @return 
	 */
	@Bizlet
	public static int ruleList(Rule[] arr ,String lotteryId ) {
		if(jedis == null){
			jedis = RedisPoolUtils.getJedis();
		}
		try {
			
			Pipeline p = jedis.pipelined();
			p.del(PRIZE_RULE+lotteryId);
			for (int i = 0; i < arr.length; i++) {
				Rule rule = arr[i];
				p.sadd(PRIZE_RULE+lotteryId, PRIZE_RULE+lotteryId+rule.getEngineerLevel());
				p.set(PRIZE_RULE+lotteryId+rule.getEngineerLevel(), rule.toString());
			}
			p.sync();
			
		} catch (Exception e) {
			System.err.println("PRIZE_RULE_ERROR_MSG=======================>" + e.getMessage());
			return 0;
		} finally {  
//        	RedisPoolUtils.returnResourceObject(jedis);
        }
		return 1;
	}
	
	
	/**
	 * 查询抽奖规则
	 * @param arr
	 * @return 
	 */
	@Bizlet
	public static Rule[] queryRuleList(String lotteryId) {
		
		Rule[] ruleList = null;
		try {
			if(jedis == null){
				jedis = RedisPoolUtils.getJedis();
			}
			Set sets = jedis.smembers(PRIZE_RULE + lotteryId);
			Pipeline p = jedis.pipelined();
			for (String str: sets) {
				p.get(str);
			}
			List list= p.syncAndReturnAll();
			Rule rule = null;
			if(list.size()>0){
				ruleList = new Rule[list.size()];
				for (int i = 0; i < list.size(); i++) {
					String str = list.get(i).toString();
					String[] arr = str.substring(str.indexOf('[')+1, str.indexOf(']')).split(",");
					rule = new Rule();
					rule.setEngineerLevel(Integer.valueOf(arr[0].split("=")[1]));
					rule.setRestrictionType(Integer.valueOf(arr[1].split("=")[1]));
					rule.setNum(Integer.valueOf(arr[2].split("=")[1]));
					ruleList[i]= rule;
				}
			}
		} catch (Exception e) {
			System.err.println("QUERY_PRIZE_RULE_ERROR_MSG=======================>" + e.getMessage());
			return null;
		} finally {  
//        	RedisPoolUtils.returnResourceObject(jedis);
        }
		return ruleList;
	}
	
	
	/**
	 * 返回当前有效时间内的抽奖活动Id
	 * @param arr
	 * @return
	 */
	@Bizlet
	public static HashMap getLotteryId(DataObject[] arr) {
		HashMap map = new HashMap();
		Date date = new Date();
		for (DataObject dataObject : arr) {
			if(date.after(dataObject.getDate("startDate")) && date.before(dataObject.getDate("stopDate"))){
				map.put("lotteryId", dataObject.getString("lotteryId"));
				map.put("lotteryName", dataObject.getString("lotteryName"));
				map.put("lotteryPoint", dataObject.getString("lotteryPoint"));
			}
			
		}
		return map;
	}
	
	
	
	/**
	 * 判断抽奖次数
	 * @param arr
	 * @param lev
	 * @return
       {id:"1", text:"其他"},
       {id:"2", text:"达人"},
       {id:"3", text:"宗师"},
       {id:"4", text:"师尊"},
       {id:"5", text:"大师"},
       {id:"6", text:"师傅"}
       var restrictionType = [
       {id:"0", text:"每天"},
       {id:"1", text:"每轮"},
    ]; 
	 */
	@Bizlet
	public static String verifyNum(Rule[] arr ,int lev , String engineerId ,String lotteryId,int lotteryTotailNum) {
		
//		System.out.println("============>Rule:" + arr.length);
//		System.out.println("============>lev:" + lev);
//		System.out.println("============>engineerId:" + engineerId);
//		System.out.println("============>lotteryId:" + lotteryId);
//		System.out.println("============>lotteryTotailNum:" + lotteryTotailNum);
		//S成功 E1已超过限制抽奖次数!E2 抽奖已结束,已达到抽奖次数。
		String flag = "S";
		
		try {
			if(jedis == null){
				jedis = RedisPoolUtils.getJedis();
			}
			
			//验证抽奖活动是否存在
			if(jedis.get(lotteryId)!=null){
				
				//验证抽奖活次数是否达到结束上限
				if(Integer.valueOf(jedis.get(lotteryId)) >= lotteryTotailNum){
					return flag = "E2" ;
				}
			}
			if(arr != null){
				for (Rule rule : arr) {
					if (rule.getEngineerLevel() == lev) {
						
						//验证用户lev对应的抽奖次数是否消耗完毕  0:每天  , 1:每轮
						if (rule.getRestrictionType() == 0) {
							if (jedis.get(engineerId)!=null) {
								int lunNum =Integer.valueOf(jedis.get(engineerId));
								if(rule.getNum()<= lunNum)flag ="E1";
							} 
						} else {
							if (jedis.get(lotteryId+engineerId)!=null) {
								int lunNum =Integer.valueOf(jedis.get(lotteryId+engineerId));
								if(rule.getNum()<= lunNum)flag ="E1";
							}
						}
					}
				}
			}
		} catch (Exception e) {
			throw new RuntimeException("VERIFY_NUM__RULE_ERROR_MSG=======================>" + e.getMessage());
		} finally {  
//        	RedisPoolUtils.returnResourceObject(jedis);
        }
		return flag;
	}
	
	
	
	/**
	 * 变更符合过期时间的奖品
	 * @param data
	 * @param day
	 * @return
	 */
	@Bizlet
	public static DataObject[] winningBeOverdue(DataObject[] data ,int day ) {
		Calendar ca = Calendar.getInstance();
		ca.add(Calendar.DATE, -day);// num为增加的天数,可以改变的
		Date dateAfter = ca.getTime();
		for (int i = 0; i < data.length; i++) {
			Date date = data[i].getDate("createTime");
			if(dateAfter.after(date)){
				data[i].set("field2", 2);
			}
		}
		return data;
	}
	
}
 
  
package com.rfg.shop;

import java.util.HashSet;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.Pipeline;

import com.eos.system.annotation.Bizlet;
import commonj.sdo.DataObject;
 

/**
 * 
 * @author XiongYC
 *
 */
public class CacheLottery {

	public static Jedis jedis = RedisPoolUtils.getJedis();
	
	public static final String PRIZE_LIST = "prizeList";
	
	public static final String PRIZE_ID = "prizeId";
	
	public static void main(String[] args) {
		
		
		HashSet set = new HashSet();
//		set = randomSet(1, 2258, 2036, set);
		System.err.println(set.size());
		int index=0;
		for (Integer integer : set) {
			System.out.println("索引index="+ index+++"元素"+integer);
		}
	}
	
	
	/**
	 * 缓存奖品信息
	 * @param lotteryId
	 * @param sponsorPoint
	 * @param arr
	 */
	@Bizlet
	public static void cachePrizeListByLotteryId(String lotteryId,int sponsorPoint ,DataObject[] arr) {
		
		try {
			if(jedis == null){
				jedis = RedisPoolUtils.getJedis();
			}
			
			//初始化本活动奖品抽奖次数
			jedis.set(lotteryId, "0");
			
			//奖品容器
			HashSet set = new HashSet();
			
			randomSet(1, sponsorPoint, set, arr);
			
			Pipeline p = jedis.pipelined();
			
			//修改清除本次奖品列表
			p.del(PRIZE_LIST + lotteryId);
			
			for (Prize obj : set) {
				//缓存奖品随机码和奖品ID
				p.zadd(PRIZE_LIST + lotteryId, Double.parseDouble(obj.getId()),obj.getVal());
			}
			p.sync();
			
		} catch (Exception e) {
			throw new RuntimeException("CACHE_PRIZE_LIST_ERROR_MSG");
		} finally {  
//        	RedisPoolUtils.returnResourceObject(jedis);
        }
	}
	
	/**
	 * 随机指定范围内N个不重复的数 
	 * 利用HashSet的特征,只能存放不同的值 
	 * @param min 指定范围最小值 
	 * @param max 指定范围最大值 
	 * @param HashSet set 随机数结果集 
	 * @param arr 奖品列表
	 * @return
	 */
	public static HashSet randomSet(int min, int max, HashSet set, DataObject[] arr) {

		for (DataObject obj : arr) {

			// 排除无效奖品
			if (Integer.valueOf(String.valueOf(obj.get("point"))) > 0) {

				// 被刺奖品的数量
				int prizeNum = Integer.valueOf(String.valueOf(obj.get("prizeNum")));

				// 记录本个奖品要停止生成的不重复的随机码的个数(hasHset数组大小 + 奖品个数)
				int stopSize = set.size() + prizeNum;

				// 生成指定建构数量的奖品唯一随机码
				while (set.size() < stopSize) {
					int num = (int) (Math.random() * (max - min)) + min;
					set.add(new Prize(String.valueOf(obj.get("id")), num + ""));// 将不同的数存入HashSet中
				}
			}
		}
		return set;
	}
	   
	   /**
	    * 抽奖
	    * @param lotteryId
	    * @return
	    */
	   @Bizlet
		public static String luckDraw(String lotteryId,String engineerId) {
		   String prizeId = null;
			try {
				if (jedis == null) {
					jedis = RedisPoolUtils.getJedis();
				}
				Long index = jedis.incr(lotteryId);
				
				Double id = jedis.zscore(PRIZE_LIST + lotteryId, index.intValue()+"");
				if(id != null){
					prizeId = String.valueOf(id.intValue());
					
					//设置每天
					if(!jedis.exists(engineerId)){
						jedis.incr(engineerId);
						jedis.expire(engineerId, 60*60*24);
//						jedis.expire(engineerId, 30);
					}else {
						jedis.incr(engineerId);
					}
					
					//每个轮询的次数
					jedis.incr(lotteryId + engineerId);
					
					//删除缓存奖品
					jedis.zrem(PRIZE_LIST + lotteryId, index+"");
				}
				
			} catch (Exception e) {
				throw new RuntimeException("CACHE_PRIZE_LIST_ERROR_MSG");
			} finally {
				// RedisPoolUtils.returnResourceObject(jedis);
			}
			return prizeId;
		}
	   
	   
	   
	   /**
	    * 保存抽奖记录
	    * @param lotteryId
	    * @param lotteryManagement
	    * @return
	    */
	   @Bizlet
		public static DataObject saveLotteryManagement(String lotteryId,DataObject lotteryManagement) {
			try {
				if (jedis == null) {
					jedis = RedisPoolUtils.getJedis();
				}
				int lotteryNum = Integer.valueOf(jedis.get(lotteryId));
				lotteryManagement.setLong("lotteryNum", lotteryNum);
				
				int lotterySurplusNum = lotteryManagement.getInt("lotteryTotailNum") - lotteryNum;
				lotteryManagement.setLong("lotterySurplusNum", lotterySurplusNum);
				
				if(lotteryManagement.getInt("lotteryTotailNum") <= lotteryNum){
					lotteryManagement.setLong("lotteryOver", 1);
				}
				
			} catch (Exception e) {
				throw new RuntimeException("SAVE_LOTTERY_MANAGEMENT_ERROR_MSG");
			} finally {
				// RedisPoolUtils.returnResourceObject(jedis);
			}
			return lotteryManagement;
		}
}

三、数据验证

redis高并发抽奖(2)_第2张图片

通过上图我们可以看到一个抽奖活动的一系列关键参数。奖品顺序的唯一性和随机性。通过前面一些我们可以核对一下这些数据都是可以匹配上的。以上就是全部内容了。有什么不足的或者不理解的地方还请在下面评论留言。

你可能感兴趣的:(NoSql,缓存)