场景描述:
1.发红包:发多少钱,多少人;(红包属性)
2.抢红包:不可重复抢(set数据结构应用,其特性是集合中不可以出现重复的元素),可获取份额(金额),有两种解决方式,第一,在抢的时候对红包进行随机金额分割;第二,事先对红包按一定规则进行划分固定份额,然后在抢的动作开始时,随机分配一个份额给用户(redis中List数据结构,把整个红包分成多少份,金额按照一定的算法实现,发在一个队列中,每次抢的时候都用rpop或lpop做出队列)就可以随机获取金额,并且杜绝一个人获取多个的可能;
3.关联用户与金额的关系(将抢来的金额存入自己的账户):落入数据库(不只是抢红包,参加活动、抽奖都要最终落库);
模拟实现:
1.创建项目导入依赖及编辑配置:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> <version>2.4.2</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.8.0</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> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-beans</artifactId> <version>5.1.8.RELEASE</version> <scope>compile</scope> </dependency> <dependency> <groupId>net.minidev</groupId> <artifactId>json-smart</artifactId> <version>2.3</version> <scope>compile</scope> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>5.1.8.RELEASE</version> </dependency> </dependencies>
--------------------------------------application.properties-----------------------------
server.port=9001 redis.host=192.168.1.11 redis.port=10179
2.应用jedis操作redis建立配置
package com.cc.springbootredisredpacket.config; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPoolConfig; @Configuration public class RedisConfig { @Value("${redis.host}") private String redisHost; @Value("${redis.port}") private int redisPort; @Bean public JedisPool jedisPool() { // jedis 连接池的配置 JedisPoolConfig config = new JedisPoolConfig(); config.setMaxIdle(10); config.setMaxTotal(200); config.setMaxWaitMillis(1000); config.setTestOnBorrow(false); return new JedisPool(config, redisHost, redisPort, 5000); } }
3.创建发红包类
package com.cc.springbootredisredpacket; import lombok.Data; import java.math.BigDecimal; import java.math.MathContext; import java.util.Random; @Data public class RedPacket { /** * 红包总个数 */ private int count; /** * 红包总金额 */ private BigDecimal sumMoney; /** * 红包剩余个数 */ private int surplusCount; /** * 红包剩余金额 */ private BigDecimal surplusMoney; public RedPacket(int count,BigDecimal sumMoney){ this.count = count; this.sumMoney = sumMoney; this.surplusCount = count; //初始化都一直,所以一并初始化; this.surplusMoney = sumMoney; } private final Random random = new Random(); /** * 采用预先按照规则分配金额到固定份数的方法 * 分配红包金额算法 */ public BigDecimal nextRedPacket(){ //预设抢到的红包 BigDecimal money = new BigDecimal(0.01); BigDecimal leftAvgMoney = getAvgMoney(this); //随机当前红包 if(surplusCount != 1){ while(true){ if(leftAvgMoney.floatValue() >0.01f){ //剩下的单个红包平均值大于0.01就进行随机分配 //计算浮动值(剩下的金额/剩下的人数)*几人次 BigDecimal d = BigDecimalUtil.mutiply(BigDecimalUtil.divide(surplusMoney,new BigDecimal(surplusCount)),new BigDecimal(3)); //计算分配金额=随机数 * 浮动范围 money = BigDecimalUtil.mutiply(new BigDecimal(random.nextFloat()),d).setScale(2,BigDecimal.ROUND_HALF_UP); } //else情况就是初始值BigDecimal money = new BigDecimal(0.01);此处不写了 //break if (leftAvgMoney.floatValue() > money.floatValue() && money.floatValue() != 0){ break; } } }else{ //最后一个红包,取两位小数 money = surplusMoney.setScale(2,BigDecimal.ROUND_HALF_UP); } surplusCount--; surplusMoney = BigDecimalUtil.subtract(surplusMoney,money); return money; } private static BigDecimal getAvgMoney(RedPacket redPacket) { //剩余总金额除以剩余的个数(剩余个数是int类型所以要转),指定精度MathContext.DECIMAL128,最后乘以2 return redPacket.getSurplusMoney().divide(new BigDecimal(redPacket.getSurplusCount()),MathContext.DECIMAL128).multiply(new BigDecimal(2)); } }
4.应用BigDecimal类型,重写并封装器基本算法,以实现红包分配算法
package com.cc.springbootredisredpacket; import java.math.BigDecimal; import java.math.MathContext; /** * 简单封装一下BigDecimal四种基础运算 */ public class BigDecimalUtil { //加法 static BigDecimal add(BigDecimal a,BigDecimal b){ return a.add(b,MathContext.DECIMAL128); } //减法 static BigDecimal subtract(BigDecimal a,BigDecimal b){ return a.subtract(b,MathContext.DECIMAL128); } //乘法 static BigDecimal mutiply(BigDecimal a,BigDecimal b){ return a.multiply(b,MathContext.DECIMAL128); } //除法 static BigDecimal divide(BigDecimal a,BigDecimal b){ return a.divide(b,MathContext.DECIMAL128); } } 5.实现抢红包类:
package com.cc.springbootredisredpacket; import redis.clients.jedis.Jedis; //实现抢红包类 public class RedPacketUtil { //存放初始化红包的“桶”,未消费 static final String RED_PACKET_LIST = "RED_PACKET_LIST"; //抢过红包的桶,已消费 static final String CONSUMED_RED_PACKET_LIST = "CONSUMED_RED_PACKET_LIST"; //抢红包和抢红包的人的映射;即将抢到红包的人存入此列表,用来过滤 static final String CONSUMED_RED_PACKET_LIST_MAP = "CONSUMED_RED_PACKET_LIST_MAP"; /** * 抢红包:1.先判断用户是否抢过,如果抢过就返回不能再抢 * 2.从列表中随机拿出一个红包,创建一个对象存入以发放的红包列表中 * 为了实现原子性操作,通过lua脚本完成代码 */ static final String LUA_SCRIPT = "if redis.call('hexists', KEYS[3], KEYS[4]) ~= 0 then\n" + //判断是否存在 "\treturn nil\n" + "else\n" + "\tlocal redPacket = redis.call('rpop', KEYS[1]);\n" + //取出红包 //"\tprint('redPacket:', redPacket);\n" + "\tif redPacket then\n" + "\tlocal x = cjson.decode(redPacket);\n" + "\tx['userId'] = KEYS[4];\n" + //通过userId构造一个数据存起来 "\tlocal re = cjson.encode(x);\n" + "\tredis.call('hset', KEYS[3], KEYS[4], KEYS[4]);\n" + "\tredis.call('lpush', KEYS[2], re);\n" + //将数据存到一个新的集合里(已抢红包集合) "\treturn re;\n" + "\tend\n" + "end\n" + "return nil"; //抢红包的逻辑 public static String getRedPacket(Jedis jedis,String userId){ //载入lua脚本 jedis.scriptLoad(LUA_SCRIPT); //执行脚本,需要传入4个参数 Object object = jedis.eval(LUA_SCRIPT,4,RED_PACKET_LIST,CONSUMED_RED_PACKET_LIST,CONSUMED_RED_PACKET_LIST_MAP,userId); if(null == object){ throw new RuntimeException("sold out"); } //不为空返回 return object.toString(); } }
6.构建业务逻辑类
package com.cc.springbootredisredpacket; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import java.math.BigDecimal; import java.util.HashMap; import java.util.Map; //初始化红包,将红包出入List @RestController @RequestMapping("red_packet") @Slf4j public class RedPacketController { @Resource private JedisPool jedisPool; //初始化 @GetMapping("init/{count}/{sum}") public Map<String,Object> init(@PathVariable int count, @PathVariable BigDecimal sum) throws JsonProcessingException { RedPacket redPacket = new RedPacket(count,sum); //声明传回浏览器显示 Map<String,Object> resultMap = new HashMap<>(count); //定义转换器 ObjectMapper objectMapper = new ObjectMapper(); for (int i = 0; i < redPacket.getCount();i++){ BigDecimal red = redPacket.nextRedPacket(); log.info("第{}个红包的金额:{}",i+1,red.toPlainString()); //存入回显页面map resultMap.put(String.valueOf(i+1),red.toPlainString()); Jedis jedis = jedisPool.getResource(); //定义出入redis中的map,出入用户ID和每份钱数,初始化ID为null Map<String,Object> map = new HashMap<>(2); map.put("userId",null); map.put("monty",red.toPlainString()); //将初始化的红包放入redis的List中 jedis.lpush(RedPacketUtil.RED_PACKET_LIST,objectMapper.writeValueAsString(map)); jedis.close(); } return resultMap; } /** * 实现抢红包 * 把sessionId做为userId * */ @GetMapping("get") public String get(HttpServletRequest request){ //获取sessionId String userId = request.getSession().getId(); log.info("userId:{}",userId); Jedis jedis = jedisPool.getResource(); //抢红包 return RedPacketUtil.getRedPacket(jedis,userId); } }
7.启动运行:
发红包:
使用jMeter模拟抢红包:
输出结果: