021 redis场景应用-抢红包

场景描述:

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模拟抢红包:

 

 

输出结果:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值