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

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

大吉大利

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

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

一、基础知识

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

  • 单例模式
    单例模式解决了两个问题:

    • 保证一个类只有一个实例
    • 为该实例提供一个全局访问节点。

    单例模式包含多种实现方式:比如“饿汉模式”(本文所使用的方式)、“懒汉模式”(线程安全/DCL)、静态内部类、枚举等等。

    本文使用饿汉模式,原因在于:线程安全,类加载完成就完成了实例化,简单实用,推荐。

    想了解更多单例模式的实现请看:https://www.jianshu.com/p/6b4d83b71826

  • AtomicInteger

    除了这个类之外,java.util.concurrent.atomic包下都是这一类。这是一个小的类工具包,支持在单变量上进行无锁线程安全编程。
    通过CAS(compare and swap)提供数据的原子更新。

  • CAS(compare and swap,比较并替换)
    又叫做自旋锁,是以无锁的方式解决变量原子性问题。是一种乐观锁思想,通过自身的不断重试。

    CAS 的底层是 lock cmpxchg 指令(X86 架构),在单核 CPU 和多核 CPU 下都能够保证【比较-交换】的原子性。

    • 在多核状态下,某个核执行到带 lock 的指令时,CPU 会让总线锁住,当这个核把此指令执行完毕,再开启总线。这个过程中不会被线程的调度机制所打断,保证了多个线程对内存操作的准确性,是原子的。

    • 特点:无锁方式,线程不会阻塞。效率高。如果竞争过于激烈,极大增加重试次数,效率反而降低。

  • lombok.Data (注解@Data)
    lombok能极大的帮助我们提高代码的开发效率,其提供了一系列的注解,使我们的代码更为整洁。

    • @AllArgsConstructor
      生成全参数的构造,在配合到spring/springboot的构造器注入时,可是不写其构造方法:
    @AllArgsConstructor
    public class ItemStorageAppServiceImpl implements IItemStorageAppService {
    
        private ElasticSearchRepository elastic;
    

    有同学文为什么不使用@Autowired?当然可以使用,只是构造方法的注入方式是官方推荐的注入方式。

    • @Data
      作用于类上,是以下注解的集合:@ToString @EqualsAndHashCode @Getter @Setter @RequiredArgsConstructor;也是我比较喜欢使用的注解。

    • @Getter/Setter
      作用于类上,生成成员变量的get和set方法。

    关于其他的,各位同学自行学习啊。

  • BigDecimal的常用方式

    BigDecimal是涉及到金额问题的最常用解决方式,所以熟练使用其方法很重要,包括加(add)减(subtract)操作、比较(compareTo)等等。必会类型,不多说。

  • LinkedList的特点

    应该是早起最常见的面试题了吧,和ArrayList相比有什么不同。同学们后面自己找相关资料仔细学习下。

    简单来说LinkedList是顺序的,链表的结构,使得其添加/删除数据更快,因为不涉及到数据迁移的问题;线程不安全;查询数据相比于ArrayList要慢,需要遍历查找。

  • 三目运算符的常用方式

    这个没有什么好说的,常见的代码编写方式:

      envelopeLogCache.get(redEnvelopeDO.getId()) == null ? new HashMap<>(4) : envelopeLogCache.get(redEnvelopeDO.getId());
    

    如上的例子表示,envelopeLogCache根据edEnvelopeDO.getId()获取一个Object,这个Object获取到了吗?如果是null,就给它一个new 的HashMap,如果不是空,就把这个对象Object返回。

    自行体会。

  • 多线程场景下的数据异常
    在本文的代码示例当中,就出现了严重的多线程场景下无法保证数据的原子性问题。这一类问题在多线程、高并发场景是必然会涉及到的。
    在java当中有对应的JUC(java.util.concurrent)类库去解决这一系列的问题,是java学习者必须会的知识。

    可以参考我的文集:https://www.jianshu.com/nb/51656252,当前持续更新中。

  • java8 lambda表达式
    java8新出现的流式编程方式,极大的缩减代码复杂度,使其更加符合面向对象的语义。

    参考文集:https://www.jianshu.com/nb/51413235

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

二、编码开始

从现在开始,正式进入编码阶段,全部代码有三个实体类,两个实现类,一个全局变量类,一个初始化处理器类。另外有两个是业务实现的接口,但是因为我们用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());
        });
    }
  • 三人顺序抢红包
    指定张三、李四、王五三个人,其中每个人各自一百块;由张三发送【新年快乐】红包,总金额100,共三个人可以抢,代码如下:
    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块多。要是这样发红包的老板要赔死都不知道咋回事啊。

  • 问题分析
    我们的数据都是存储在一个共享变量GlobalDataCache当中的,我们首先结合下图JMM(java内存模型)分析下:
JMM.png

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

  • 解决方案
    优点基础的同学一定会想到使用锁实现,我们在java中常间的锁有Synchronized,LockSupport以及ReetrantLock。

    想了解原理的同学可以看我的这个专题:https://www.jianshu.com/u/e62c72db32f1

    本文使用ReetrantLock来解决问题,所以有修改后的测试方法如下:

    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请求开始,涉及到如下组建,咱们逐一举例:

  • 负载均衡
    磁层主要是对网关做负载均衡,企业通常采用软负载,常用nginx等,或使用F5的负载均衡组件。

  • 网关层
    网关在如今的主流java开发领域,使用较多的是springCloud生态的zuul或者gateway组件,自带负载均衡和代理转发的功能。同时可以作为权限验证的组件,如集成JWT等。也可以做请求拦截,白名单等。

  • 业务服务层
    通常就是咱们写业务代码的一层,目前主流的框架有两个,分别是阿里开源的dubbo生态,随着注册中心nacos的出现,可以取代原本的zookeeper,目前使用量较广。另一个就是springCloud的生态,提供丰富的组件库,Feign,ribbon,eureka,hystrix等等组件,应当是目前使用量最广泛的java微服务框架。

  • 数据持久层
    此层我应该再细分为三个层面:

    • 关系型数据库:主流的是mysql和PostgreSQL,传统行业可能还在使用Oracle;以及目前阿里背书的OceanBase等。
    • 缓存:随着目前服务数量,用户量的增加,缓存对于互联网应用来说越来越重要。主流是redis和MongoDB,使用量都很广泛。
    • 搜索引擎:主流是Elasticsearch、solr等。业务场景对于查询量大的,修改少的场景也可以使用。
  • 限流、熔断、降级
    当并发量很大,系统不足以应付的时候,可以使用这些策略保证系统的可用性。springcloud提供自带的组件hystrix。
    但是我此处指的是作用与网关层,也就是请求的入口处。推荐使用阿里开源的Sentinel。

  • 注册中心/配置中心
    目前主流的注册中心在国内可以说就是阿里巴巴开源的nacos了,集服务发现和动态配置于一身,同时支持dubbo和springcloud等主流的微服务框架。
    另外springcloud自带的eureka暂时不推荐使用了,在易用性上来说完全不如nacos。还需要单独选择一套配置中心搭配,可以使用zookeeper,etcd等组件自行开发;也可以使用携程的Apollo,也是不错的配置中心组件。

  • 消息中间件
    消息中间件是目前互联网中的明星了,在解决高并发量,大吞吐量,异步解耦等方面可以说做到了极致。有其适合大量订单等场景。
    常用的有阿里巴巴的RocketMQ,RabbitMQ以及经久不衰的Kafka。

  • EFK/ELK
    日志收集组件。对于一些大型传统行业,涉及到一些审计的工作,他们对于操作日志的记录非常严格,此时需要一套专门的系统来做。
    主流的有elasticsearch+ fluentd + kibana 和大多数在企业使用的elasticsearch+ Logstash + kibana。日志存储组件都是Elasticsearch,查询组件都是Kibana,主要差别在于日志的收集组件,这将影响到日志记录的整体速度,除了上面两种还有不少的选择如:flume、filebeat等

  • 链路追踪
    这个属于保证系统可用性的组件,可以反映当前系统的运行状态,以及各服务之间额调用关系,甚至于网络请求的吞吐量等。可以集成邮件等组件实现异常告警推送。

    常用的我推荐skywalking,同时还有zipkin,和新兴的jeager。

  • Prometheus + Grafana
    这是一套云原生的监控系统,可以监控服务器,服务,以及各种会用到的组件,使用exporter将数据收集到prometheus进行存储。之后由Grafana提供动态可配置的报表进行定制展示,是目前最好的监控组件。

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

四、总结

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

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

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

大吉大利
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容