java学习快到碗里来JavaJava 核心技术

面向java入门小白的【春节抢红包】案例

2022-01-07  本文已影响0人  我犟不过你

春节将至,又快到了一年一度抢红包的激动时刻。

大吉大利

为此呢,我专门针对想要学习java,或刚开始学习java的小白们,写了一段简单易懂的【春节抢红包】代码,其中涉及到部分的java编程基础知识。也涉及到关于真正抢红包的思考。相信你们一定能有所收货,同时又能有所联想。

请想要学习的同学们仔阅读代码的注释,有部分基础知识的讲解我没有单独抽取,放在注释当中了。虽然本文不会针对每个知识点讲到原理,但是会在下面列举出来提纲,这些是以后工作中常用,且面试涉及几率很高的内容,希望各位看完本篇后,能够针对这些内容加强理解。

一、基础知识

内部涉及到的基础知识提纲如下,我会针对每个知识点简单介绍:

上面的内容很基础,但是都比较重要的,无论是写代码,阅读源码,都会涉及,我只是再次提供一些思路。

二、编码开始

从现在开始,正式进入编码阶段,全部代码有三个实体类,两个实现类,一个全局变量类,一个初始化处理器类。另外有两个是业务实现的接口,但是因为我们用main方法做的此次演示,暂时忽略吧。

2.1 实体类

import lombok.Data;

import java.math.BigDecimal;

/**
 * @description: 人
 * @author:weirx
 * @date:2022/1/6 9:40
 * @version:3.0
 */
@Data
public class PeopleDO {

    /**
     * 抢红包人的id
     */
    private Integer id;

    /**
     * 人名
     */
    private String name;

    /**
     * 金额
     */
    private BigDecimal amount;

    public PeopleDO(Integer id, String name, BigDecimal amount) {
        this.id = id;
        this.name = name;
        this.amount = amount;
    }
}
import lombok.Data;

import java.math.BigDecimal;

/**
 * @description: 红包
 * @author:weirx
 * @date:2022/1/6 9:37
 * @version:3.0
 */
@Data
public class RedEnvelopeDO {

    /**
     * 红包id
     */
    private Integer id;

    /**
     * 红包名称
     */
    private String name;

    /**
     * 金额
     */
    private BigDecimal amount;

    /**
     * 数量
     */
    private Integer quantity;

    /**
     * 发红包人的id
     */
    private Integer peopleId;

    public RedEnvelopeDO(String name, BigDecimal amount, Integer quantity, Integer peopleId) {
        this.name = name;
        this.amount = amount;
        this.quantity = quantity;
        this.peopleId = peopleId;
    }
}
import lombok.Data;

import java.math.BigDecimal;
import java.util.Date;

/**
 * @description: 抢红包记录
 * @author:weirx
 * @date:2022/1/6 9:45
 * @version:3.0
 */
@Data
public class GrabEnvelopeLogDO {

    /**
     * 用户id
     */
    private Integer peopleId;

    /**
     * 红包id
     */
    private Integer redEnvelopeId;

    /**
     * 抢到的金额
     */
    private BigDecimal amount;

    /**
     * 发送时间
     */
    private Date createTime;

    public GrabEnvelopeLogDO(Integer peopleId, Integer redEnvelopeId, BigDecimal amount, Date createTime) {
        this.peopleId = peopleId;
        this.redEnvelopeId = redEnvelopeId;
        this.amount = amount;
        this.createTime = createTime;
    }
}

实体类使用 @Data 注解,需要引用如下依赖:

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.10</version>
        </dependency>

这个注解使我们在开发过程中可以极大的增加开发效率,我此处使用可以让我们省去写get、set方法的繁琐。调用时又不耽误我们正常使用。

另外就是根据不同的实体类业务场景,我们可以创建不同参数列表的构造器,这是java“重载”在构造器的体现。

2.2 实现类

共有两个实现类,分别是发送红包的实现类和抢红包的工具类:

import com.cloud.bssp.chinesenewyear.grabredenvelope.GlobalDataCache;
import com.cloud.bssp.chinesenewyear.grabredenvelope.GlobalInitProcessor;
import com.cloud.bssp.chinesenewyear.grabredenvelope.entity.PeopleDO;
import com.cloud.bssp.chinesenewyear.grabredenvelope.entity.RedEnvelopeDO;
import com.cloud.bssp.chinesenewyear.grabredenvelope.service.ISendRedEnvelopeService;

import java.math.BigDecimal;
import java.util.Map;

/**
 * @description: 发红包接口
 * @author:weirx
 * @date:2022/1/6 9:56
 * @version:3.0
 */
public class SendRedEnvelopeServiceImpl implements ISendRedEnvelopeService {

    @Override
    public RedEnvelopeDO send(RedEnvelopeDO redEnvelopeDO) throws Exception {
        // 获取全局变量
        GlobalDataCache globalDataCache = GlobalInitProcessor.getGlobalDataCache();

        //获取用户信息
        PeopleDO peopleDO = globalDataCache.getPeopleCache().get(redEnvelopeDO.getPeopleId());
        //校验用户金额是否足够发红包
        if (peopleDO.getAmount().compareTo(redEnvelopeDO.getAmount()) < 0) {
            // 直接跑出异常,调用时捕获异常内容,实际应该定制通用返回结果,并且有对象的成功失败标识
            throw new Exception("您当前余额不足,红包发送失败");
        }
        // 扣除用户账户金额
        BigDecimal subtract = peopleDO.getAmount().subtract(redEnvelopeDO.getAmount());
        peopleDO.setAmount(subtract);
        // 将用户存入缓存当中
        Map<Integer, PeopleDO> peopleCache = globalDataCache.getPeopleCache();
        peopleCache.put(peopleDO.getId(), peopleDO);
        globalDataCache.setPeopleCache(peopleCache);
        // 获取全局唯一红包id,并将红包存入缓存
        redEnvelopeDO.setId(globalDataCache.getId());
        Map<Integer, RedEnvelopeDO> redEnvelopeCache = globalDataCache.getRedEnvelopeCache();
        redEnvelopeCache.put(redEnvelopeDO.getId(), redEnvelopeDO);
        globalDataCache.setRedEnvelopeCache(redEnvelopeCache);
        return redEnvelopeDO;
    }
}
import cn.hutool.core.util.ObjectUtil;
import com.cloud.bssp.chinesenewyear.grabredenvelope.GlobalDataCache;
import com.cloud.bssp.chinesenewyear.grabredenvelope.GlobalInitProcessor;
import com.cloud.bssp.chinesenewyear.grabredenvelope.entity.GrabEnvelopeLogDO;
import com.cloud.bssp.chinesenewyear.grabredenvelope.entity.PeopleDO;
import com.cloud.bssp.chinesenewyear.grabredenvelope.entity.RedEnvelopeDO;
import com.cloud.bssp.chinesenewyear.grabredenvelope.service.IGrabRedEnvelopeService;

import java.math.BigDecimal;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * @description: 抢红包
 * @author:weirx
 * @date:2022/1/6 10:58
 * @version:3.0
 */
public class GrabRedEnvelopeServiceImpl implements IGrabRedEnvelopeService {
    @Override
    public void grab(Integer peopleId, Integer redEnvelopeId) throws Exception {
        // 获取全局变量
        GlobalDataCache globalDataCache = GlobalInitProcessor.getGlobalDataCache();
        // 获取用户
        Map<Integer, PeopleDO> peopleCache = globalDataCache.getPeopleCache();
        PeopleDO peopleDO = peopleCache.get(peopleId);
        // 获取红包
        Map<Integer, RedEnvelopeDO> redEnvelopeCache = globalDataCache.getRedEnvelopeCache();
        RedEnvelopeDO redEnvelopeDO = redEnvelopeCache.get(redEnvelopeId);
        // 获取红包历史
        Map<Integer, Map<Integer, GrabEnvelopeLogDO>> envelopeLogCache = globalDataCache.getEnvelopeLogCache();
        Map<Integer, GrabEnvelopeLogDO> integerGrabEnvelopeLogDOMap =
                envelopeLogCache.get(redEnvelopeDO.getId()) == null ? new HashMap<>(4)
                        : envelopeLogCache.get(redEnvelopeDO.getId());
        //判断红包是否还有余量
        if (redEnvelopeDO.getQuantity() > 0) {
            // 计算抢到的红包金额,并减去余额,和可抢数量
            BigDecimal sub = this.sub(redEnvelopeDO);
            // 用户增加余额
            peopleDO.setAmount(peopleDO.getAmount().add(sub));
            // 记录抢红包历史
            // 没有红包历史则新建,有则返回不能抢红包
            if (ObjectUtil.isNotEmpty(integerGrabEnvelopeLogDOMap) &&
                    ObjectUtil.isNotEmpty(integerGrabEnvelopeLogDOMap.get(peopleId))) {
                throw new Exception("很抱歉,您已抢过红包");
            } else {
                GrabEnvelopeLogDO grabEnvelopeLog = new GrabEnvelopeLogDO(peopleId, redEnvelopeId, sub, new Date());
                integerGrabEnvelopeLogDOMap.put(peopleId, grabEnvelopeLog);
                envelopeLogCache.put(redEnvelopeId, integerGrabEnvelopeLogDOMap);
                globalDataCache.setEnvelopeLogCache(envelopeLogCache);
            }
        } else {
            throw new Exception("很抱歉,红包已被抢完!");
        }
    }

    /**
     * description: 计算抢到的红包金额,并减去余额
     *
     * @param redEnvelopeDO
     * @return: BigDecimal
     * @author: weirx
     * @time: 2022/1/6 11:06
     */
    private BigDecimal sub(RedEnvelopeDO redEnvelopeDO) {
        BigDecimal scale;
        if (redEnvelopeDO.getQuantity() > 1) {
            // 计算能获取的最金额,指定最大最小范围
            int max = redEnvelopeDO.getAmount().intValue();
            double min = 0.01;
            // 随机范围,不会超过max,也不会小于min
            BigDecimal db = new BigDecimal(Math.random() * (max - min) + min);
            //保留两位小数,不四舍五入
            scale = db.setScale(2, BigDecimal.ROUND_DOWN);
        } else {
            // 剩一个则获取全部
            scale = redEnvelopeDO.getAmount();
        }
        //设置红包余额
        redEnvelopeDO.setAmount(redEnvelopeDO.getAmount().subtract(scale));
        //设置剩余可抢数量
        redEnvelopeDO.setQuantity(redEnvelopeDO.getQuantity() - 1);
        return scale;
    }
}

上面的实现都对应实现了其各自的接口,我也提供下,暂时没有使用:

import org.springframework.stereotype.Service;

/**
 * @description: 抢红包接口
 * @author:weirx
 * @date:2022/1/6 9:52
 * @version:3.0
 */
@Service
public interface IGrabRedEnvelopeService {

    /**
     * description: 抢红包接口
     *
     * @param peopleId      用户id
     * @param redEnvelopeId 红包id
     * @author: weirx
     * @time: 2022/1/6 9:54
     */
    void grab(Integer peopleId, Integer redEnvelopeId) throws Exception;
}
import com.cloud.bssp.chinesenewyear.grabredenvelope.entity.RedEnvelopeDO;
import org.springframework.stereotype.Service;

/**
 * @description: 发送红包的接口
 * @author:weirx
 * @date:2022/1/6 9:49
 * @version:3.0
 */
@Service
public interface ISendRedEnvelopeService {

    /**
     * description: 发送红包
     *
     * @param redEnvelopeDO 红包
     * @return: RedEnvelopeDO
     * @author: weirx
     * @time: 2022/1/6 9:50
     */
    RedEnvelopeDO send(RedEnvelopeDO redEnvelopeDO) throws Exception;
}

关于代码的详细解释都在注释里面了,我就不在单独解释了。

这里好像使用了hutool工具,一时疏忽,但是既然用了就给大家提一嘴,需要引入下面的依赖:

<!-- https://mvnrepository.com/artifact/cn.hutool/hutool-all -->
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.7.18</version>
</dependency>

这是一个大而全的工具类,只要你想的到的,基本在这里面都有对应的工具类。不要错过。

2.3 全局变量类及初始化处理器

全局变量类是我走的一个临时存储数据的类,本文没有使用数据库等存储组件。也正是由于这个原因导致了多线程的问题,后面会介绍。

import com.cloud.bssp.chinesenewyear.grabredenvelope.entity.GrabEnvelopeLogDO;
import com.cloud.bssp.chinesenewyear.grabredenvelope.entity.PeopleDO;
import com.cloud.bssp.chinesenewyear.grabredenvelope.entity.RedEnvelopeDO;
import lombok.Data;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @description: 全局数据存储(为了方便学习,我没有使用任何的数据库等,直接通过内存当时存储)
 * @author:weirx
 * @date:2022/1/6 10:01
 * @version:3.0
 */
@Data
public class GlobalDataCache {

    /**
     * AtomicInteger是原子性的,能够保证在并发环境下的数据原子性
     * 通过CAS(compare and swap,比较并替换)实现的原子性
     *
     * 我使用这个变量作为后面对象的自增id
     */
    private final AtomicInteger atomicInteger = new AtomicInteger(0);

    /**
     * 存储全部用户数据
     */
    private Map<Integer, PeopleDO> peopleCache = new HashMap<>();

    /**
     * 存储全部红包数据
     */
    private Map<Integer, RedEnvelopeDO> redEnvelopeCache = new HashMap<>();

    /**
     * 红包历史数据 Map<红包id, Map<用户id,GrabEnvelopeLogDO>>
     */
    private Map<Integer, Map<Integer, GrabEnvelopeLogDO>> envelopeLogCache = new HashMap<>();

    /**
     * 获取全局唯一id
     *
     * incrementAndGet方法使用CAS自加1,保证原子性
     *
     * @return
     */
    public Integer getId() {
        return atomicInteger.incrementAndGet();
    }
}
/**
 * @description: 全局初始化处理器
 * @author:weirx
 * @date:2022/1/6 10:12
 * @version:3.0
 */
public class GlobalInitProcessor {

    /**
     * 单例模式(使用静态变量实现),个人认为是最好也是最实用的方式。
     *
     * 静态变量在程序运行时就会加载,从而执行getInstance方法,创建对象实例,有且仅有一次创建的过程
     *
     * 调用方使用GlobalInitProcessor.getGlobalDataCache()即可获取GlobalDataCache的全局唯一实例
     */
    private final static GlobalDataCache GLOBAL_DATA_CACHE = getInstance();

    private static GlobalDataCache getInstance() {
        return new GlobalDataCache();
    }

    public static GlobalDataCache getGlobalDataCache() {
        return GLOBAL_DATA_CACHE;
    }
}

2.4 main方法测试

基础的代码都在前面的小节提供了,本小节偶尔们主要做些实验看结果。

    /**
     * description: 计算总金额
     *
     * @param globalDataCache
     * @return: void
     * @author: weirx
     * @time: 2022/1/6 15:02
     */
    public static void sum(GlobalDataCache globalDataCache) {
        //计算被领取红包的总金额
        final BigDecimal[] count = {new BigDecimal(0)};
        globalDataCache.getEnvelopeLogCache().forEach((k, v) -> {
            v.forEach((k1, v1) -> {
                count[0] = count[0].add(v1.getAmount());
            });
        });
        System.out.println("被抢总金额:" + count[0]);

        //计算每个人的钱包总金额
        final BigDecimal[] count1 = {new BigDecimal(0)};
        globalDataCache.getPeopleCache().forEach((k, v) -> {
            count1[0] = count1[0].add(v.getAmount());
        });
        System.out.println("所有人的总金额:" + count1[0]);

        //红包剩余金额
        globalDataCache.getRedEnvelopeCache().forEach((k, v) -> {
            System.out.println("红包剩余金额:" + v.getAmount());
        });
    }
    public static void main(String[] args) {
        GlobalDataCache globalDataCache = GlobalInitProcessor.getGlobalDataCache();

        // 准备用户
        PeopleDO zhangsan = new PeopleDO(globalDataCache.getId(), "张三", new BigDecimal(100));
        PeopleDO lisi = new PeopleDO(globalDataCache.getId(), "李四", new BigDecimal(100));
        PeopleDO wangwu = new PeopleDO(globalDataCache.getId(), "王五", new BigDecimal(100));
        Map<Integer, PeopleDO> map = new HashMap<>(4);
        map.put(zhangsan.getId(), zhangsan);
        map.put(lisi.getId(), lisi);
        map.put(wangwu.getId(), wangwu);
        globalDataCache.setPeopleCache(map);

        //张三发100的红包
        SendRedEnvelopeServiceImpl sendRedEnvelopeService = new SendRedEnvelopeServiceImpl();
        RedEnvelopeDO redEnvelopeDO = new RedEnvelopeDO("新年快乐", new BigDecimal(100), 3, zhangsan.getId());
        try {
            sendRedEnvelopeService.send(redEnvelopeDO);
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }

        //张三、李四、王五按【顺序】抢红包
        LinkedList<Integer> userList = new LinkedList<>();
        userList.add(zhangsan.getId());
        userList.add(lisi.getId());
        userList.add(wangwu.getId());

        GrabRedEnvelopeServiceImpl grabRedEnvelopeService = new GrabRedEnvelopeServiceImpl();
        userList.forEach(i -> {
            try {
                grabRedEnvelopeService.grab(i, redEnvelopeDO.getId());
            } catch (Exception e) {
                System.out.println(e.getMessage());
            }
        });

        System.out.println(globalDataCache);

        sum(globalDataCache);
    }

代码简单,直接看结果了:

GlobalDataCache(atomicInteger=4, peopleCache={1=PeopleDO(id=1, name=张三, amount=32.64), 2=PeopleDO(id=2, name=李四, amount=125.06), 3=PeopleDO(id=3, name=王五, amount=142.30)}, redEnvelopeCache={4=RedEnvelopeDO(id=4, name=新年快乐, amount=0.00, quantity=0, peopleId=1)}, envelopeLogCache={4={1=GrabEnvelopeLogDO(peopleId=1, redEnvelopeId=4, amount=32.64, createTime=Thu Jan 06 17:03:21 CST 2022), 2=GrabEnvelopeLogDO(peopleId=2, redEnvelopeId=4, amount=25.06, createTime=Thu Jan 06 17:03:21 CST 2022), 3=GrabEnvelopeLogDO(peopleId=3, redEnvelopeId=4, amount=42.30, createTime=Thu Jan 06 17:03:21 CST 2022)}})
被抢总金额:100.00
所有人的总金额:300.00
红包剩余金额:0.00
RedEnvelopeDO redEnvelopeDO = new RedEnvelopeDO("新年快乐", new BigDecimal(100), 2, zhangsan.getId());

结果当中有一个人,王五是抢不到的

很抱歉,红包已被抢完!
GlobalDataCache(atomicInteger=4, peopleCache={1=PeopleDO(id=1, name=张三, amount=34.74), 2=PeopleDO(id=2, name=李四, amount=165.26), 3=PeopleDO(id=3, name=王五, amount=100)}, redEnvelopeCache={4=RedEnvelopeDO(id=4, name=新年快乐, amount=0.00, quantity=0, peopleId=1)}, envelopeLogCache={4={1=GrabEnvelopeLogDO(peopleId=1, redEnvelopeId=4, amount=34.74, createTime=Thu Jan 06 17:06:20 CST 2022), 2=GrabEnvelopeLogDO(peopleId=2, redEnvelopeId=4, amount=65.26, createTime=Thu Jan 06 17:06:20 CST 2022)}})
被抢总金额:100.00
所有人的总金额:300.00
红包剩余金额:0.00
RedEnvelopeDO redEnvelopeDO = new RedEnvelopeDO("新年快乐", new BigDecimal(100), 4, zhangsan.getId());

结果:

GlobalDataCache(atomicInteger=4, peopleCache={1=PeopleDO(id=1, name=张三, amount=20.24), 2=PeopleDO(id=2, name=李四, amount=173.39), 3=PeopleDO(id=3, name=王五, amount=100.62)}, redEnvelopeCache={4=RedEnvelopeDO(id=4, name=新年快乐, amount=5.75, quantity=1, peopleId=1)}, envelopeLogCache={4={1=GrabEnvelopeLogDO(peopleId=1, redEnvelopeId=4, amount=20.24, createTime=Thu Jan 06 17:08:47 CST 2022), 2=GrabEnvelopeLogDO(peopleId=2, redEnvelopeId=4, amount=73.39, createTime=Thu Jan 06 17:08:47 CST 2022), 3=GrabEnvelopeLogDO(peopleId=3, redEnvelopeId=4, amount=0.62, createTime=Thu Jan 06 17:08:47 CST 2022)}})
被抢总金额:94.25
所有人的总金额:294.25
红包剩余金额:5.75

通过上面的测试,我们发现,三种情况下总金额都是300,没有发生数据原子性的问题,那是因为我们的抢红包是通过一个线程串行去抢的,然而实际情况是不可能的。

大家在年会抢红包的时候,都是一直盯着手机,所以人基本同一时刻点击抢红包,先让不满足上述的理想环境。

2.5 并发场景下的抢红包

在本章节我们使用多线程去模拟多人在公司年会抢红包,仍然是100的红包,模拟10个人抢,总共10个红包,通过10个线程模拟10个人,修改测试方法如下:

public static void main(String[] args) throws InterruptedException {
        // 定义一个常量,创建的用户数,也是抢红包的人数,同样是红包设定的个数(此场景就设置正好的数量吧)
        int num = 10;

        //获取全局变量
        GlobalDataCache globalDataCache = GlobalInitProcessor.getGlobalDataCache();

        // 准备用户, Map初始化记得赋予初始大小,设置值 * 负载因子(0.75) = 实际容量
        Map<Integer, PeopleDO> peopleDOMap = new HashMap<>(128);
        for (int i = 0; i < num; i++) {
            //循环初始化用户,添加到peopleDOMap中
            PeopleDO peopleDO = new PeopleDO(globalDataCache.getId(), "用户:" + i, new BigDecimal(100));
            peopleDOMap.put(peopleDO.getId(), peopleDO);
        }
        // 设置用户到全局变量
        globalDataCache.setPeopleCache(peopleDOMap);

        //实例化发红包的业务实现
        SendRedEnvelopeServiceImpl sendRedEnvelopeService = new SendRedEnvelopeServiceImpl();
        //构造一个红包
        RedEnvelopeDO redEnvelopeDO = new RedEnvelopeDO(
                "新年快乐", new BigDecimal(100), num, peopleDOMap.keySet().iterator().next());
        try {
            // 发送红包
            sendRedEnvelopeService.send(redEnvelopeDO);
        } catch (Exception e) {
            //此处会捕获手动抛出的“用户余额不足异常”
            System.out.println(e.getMessage());
        }

        // 获取钱红包实现
        GrabRedEnvelopeServiceImpl grabRedEnvelopeService = new GrabRedEnvelopeServiceImpl();

        // 使用屏障或者叫同步器,指定一个数字,当线程调用一个await方法,数字加1,知道等于设置的数值,所有线程才会开始执行,否则一直处于阻塞状态。
        CyclicBarrier cyclicBarrier = new CyclicBarrier(num);
        // 并发的抢红包
        peopleDOMap.forEach((k, v) -> {
            // 创建和人数一样多的线程
            new Thread(() -> {
                try {
                    // 等到所有线程到达
                    cyclicBarrier.await();
                    // 执行抢红包方法
                    grabRedEnvelopeService.grab(k, redEnvelopeDO.getId());
                } catch (Exception e) {
                    System.out.println(e.getMessage());
                }
            }).start();
        });
        // 主线程休眠1秒,否则业务线程还没执行完,主线程就结束了,看不到结果
        TimeUnit.SECONDS.sleep(1);
        System.out.println(globalDataCache);

        // 结果统计
        sum(globalDataCache);
    }

结果:

GlobalDataCache(atomicInteger=11, peopleCache={1=PeopleDO(id=1, name=用户:0, amount=60.07), 2=PeopleDO(id=2, name=用户:1, amount=106.39), 3=PeopleDO(id=3, name=用户:2, amount=125.67), 4=PeopleDO(id=4, name=用户:3, amount=186.65), 5=PeopleDO(id=5, name=用户:4, amount=178.62), 6=PeopleDO(id=6, name=用户:5, amount=180.41), 7=PeopleDO(id=7, name=用户:6, amount=174.70), 8=PeopleDO(id=8, name=用户:7, amount=170.05), 9=PeopleDO(id=9, name=用户:8, amount=170.19), 10=PeopleDO(id=10, name=用户:9, amount=126.10)}, redEnvelopeCache={11=RedEnvelopeDO(id=11, name=新年快乐, amount=-317.50, quantity=2, peopleId=1)}, envelopeLogCache={11={3=GrabEnvelopeLogDO(peopleId=3, redEnvelopeId=11, amount=25.67, createTime=Thu Jan 06 17:17:44 CST 2022)}})
被抢总金额:25.67
所有人的总金额:1478.85
红包剩余金额:-317.50

上述结果大家看到了吧,这才是10个人,看到数额完全不对了,红包剩余都变成了负数的,总金额页远超了1000块,二红包被抢的总共在25块多。要是这样发红包的老板要赔死都不知道咋回事啊。

JMM.png

我们共享变量GlobalDataCache在实例化后存储在堆中,堆是共享的;当线程被创建,并且使用到这个GlobalDataCache时,会从堆中获取,然后将其存储到自己的虚拟机栈当中;虚拟机栈中有栈帧,每个方法就是一个栈帧,栈帧中又包含本地变量表,此时的GlobalDataCache就存在这里面。所以当线程咋方法中修改GlobalDataCache时,修改的只是本地变量表的数据,没有修改队中的数据,只有当当方法全部完成后,才会同步到队中的GlobalDataCache。此时必然产生数据不同步的问题了。

    public static void main(String[] args) throws InterruptedException {
        // 定义一个常量,创建的用户数,也是抢红包的人数,同样是红包设定的个数(为了测试红包不足,实际会减少)
        int num = 10;

        //获取全局变量
        GlobalDataCache globalDataCache = GlobalInitProcessor.getGlobalDataCache();

        // 准备用户, Map初始化记得赋予初始大小,设置值 * 负载因子(0.75) = 实际容量
        Map<Integer, PeopleDO> peopleDOMap = new HashMap<>(128);
        for (int i = 0; i < num; i++) {
            //循环初始化用户,添加到peopleDOMap中
            PeopleDO peopleDO = new PeopleDO(globalDataCache.getId(), "用户:" + i, new BigDecimal(100));
            peopleDOMap.put(peopleDO.getId(), peopleDO);
        }
        // 设置用户到全局变量
        globalDataCache.setPeopleCache(peopleDOMap);

        //实例化发红包的业务实现
        SendRedEnvelopeServiceImpl sendRedEnvelopeService = new SendRedEnvelopeServiceImpl();
        //构造一个红包
        RedEnvelopeDO redEnvelopeDO = new RedEnvelopeDO(
                "新年快乐", new BigDecimal(100), num - 2, peopleDOMap.keySet().iterator().next());
        try {
            // 发送红包
            sendRedEnvelopeService.send(redEnvelopeDO);
        } catch (Exception e) {
            //此处会捕获手动抛出的“用户余额不足异常”
            System.out.println(e.getMessage());
        }

        // 获取钱红包实现
        GrabRedEnvelopeServiceImpl grabRedEnvelopeService = new GrabRedEnvelopeServiceImpl();

        // 使用屏障或者叫同步器,指定一个数字,当线程调用一个await方法,数字加1,知道等于设置的数值,所有线程才会开始执行,否则一直处于阻塞状态。
        CyclicBarrier cyclicBarrier = new CyclicBarrier(num);
        // 保证数据原子性,线程同步,等待线程处于阻塞状态,lock/unlock
        ReentrantLock lock = new ReentrantLock();
        peopleDOMap.forEach((k, v) -> {
            // 创建和人数一样多的线程
            new Thread(() -> {
                try {
                    // 等到所有线程到达
                    cyclicBarrier.await();
                    // 锁住抢红包方法grap,此时是互斥的,只有当前线程能进来,其余县城在阻塞队列等待
                    lock.lock();
                    // 执行抢红包方法
                    grabRedEnvelopeService.grab(k, redEnvelopeDO.getId());
                } catch (Exception e) {
                    System.out.println(e.getMessage());
                } finally {
                    // 释放互斥锁,要保证在finally当中执行释放锁,防止死锁发生
                    lock.unlock();
                }
            }).start();
        });
        // 主线程休眠1秒,否则业务线程还没执行完,主线程就结束了,看不到结果
        TimeUnit.SECONDS.sleep(1);
        System.out.println(globalDataCache);

        // 结果统计
        sum(globalDataCache);
    }

此次我们还减少两个红包数量,即8个:

 RedEnvelopeDO redEnvelopeDO = new RedEnvelopeDO("新年快乐", new BigDecimal(100), num - 2, peopleDOMap.keySet().iterator().next());

结果:

很抱歉,红包已被抢完!
很抱歉,红包已被抢完!
GlobalDataCache(atomicInteger=11, peopleCache={1=PeopleDO(id=1, name=用户:0, amount=4.29), 2=PeopleDO(id=2, name=用户:1, amount=100.69), 3=PeopleDO(id=3, name=用户:2, amount=102.21), 4=PeopleDO(id=4, name=用户:3, amount=101.59), 5=PeopleDO(id=5, name=用户:4, amount=100.00), 6=PeopleDO(id=6, name=用户:5, amount=100), 7=PeopleDO(id=7, name=用户:6, amount=100.00), 8=PeopleDO(id=8, name=用户:7, amount=100.85), 9=PeopleDO(id=9, name=用户:8, amount=100), 10=PeopleDO(id=10, name=用户:9, amount=190.37)}, redEnvelopeCache={11=RedEnvelopeDO(id=11, name=新年快乐, amount=0.00, quantity=0, peopleId=1)}, envelopeLogCache={11={1=GrabEnvelopeLogDO(peopleId=1, redEnvelopeId=11, amount=4.29, createTime=Thu Jan 06 17:41:25 CST 2022), 2=GrabEnvelopeLogDO(peopleId=2, redEnvelopeId=11, amount=0.69, createTime=Thu Jan 06 17:41:25 CST 2022), 3=GrabEnvelopeLogDO(peopleId=3, redEnvelopeId=11, amount=2.21, createTime=Thu Jan 06 17:41:25 CST 2022), 4=GrabEnvelopeLogDO(peopleId=4, redEnvelopeId=11, amount=1.59, createTime=Thu Jan 06 17:41:25 CST 2022), 5=GrabEnvelopeLogDO(peopleId=5, redEnvelopeId=11, amount=0.00, createTime=Thu Jan 06 17:41:25 CST 2022), 7=GrabEnvelopeLogDO(peopleId=7, redEnvelopeId=11, amount=0.00, createTime=Thu Jan 06 17:41:25 CST 2022), 8=GrabEnvelopeLogDO(peopleId=8, redEnvelopeId=11, amount=0.85, createTime=Thu Jan 06 17:41:25 CST 2022), 10=GrabEnvelopeLogDO(peopleId=10, redEnvelopeId=11, amount=90.37, createTime=Thu Jan 06 17:41:25 CST 2022)}})
被抢总金额:100.00
所有人的总金额:1000.00
红包剩余金额:0.00

由上所示发现没有任何问题。

三、微服务常用组件

前面扯了一大堆基础到不能在基础的内容,下面我们自由飞翔一下,看看当今企业中常见的技术栈有哪些,可以利用到我们的抢红包的场景当中。

下面我简单画一幅架构图,列出比较常用的架构设计:

架构.png

如上图所示,从web请求开始,涉及到如下组建,咱们逐一举例:

关于常用的组件就介绍到这吧,当然还有很多很多没有提到,也还有很多我没有用过,希望在工作中不断地学习吧。

四、总结

一篇java入门小知识,不知道对朋友们有没有帮助,码字不易,给点个赞和关注啊。

学完本篇抢红包的代码,保准让你过年每个红包都不落下,每次红包都抢最大的!!

小弟提前给你们拜年了!!!

大吉大利
上一篇下一篇

猜你喜欢

热点阅读