春节将至,又快到了一年一度抢红包的激动时刻。
为此呢,我专门针对想要学习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方法。
关于其他的,各位同学自行学习啊。
- @AllArgsConstructor
-
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 注解,需要引用如下依赖:
org.projectlombok
lombok
1.16.10
这个注解使我们在开发过程中可以极大的增加开发效率,我此处使用可以让我们省去写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 peopleCache = globalDataCache.getPeopleCache();
peopleCache.put(peopleDO.getId(), peopleDO);
globalDataCache.setPeopleCache(peopleCache);
// 获取全局唯一红包id,并将红包存入缓存
redEnvelopeDO.setId(globalDataCache.getId());
Map 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 peopleCache = globalDataCache.getPeopleCache();
PeopleDO peopleDO = peopleCache.get(peopleId);
// 获取红包
Map redEnvelopeCache = globalDataCache.getRedEnvelopeCache();
RedEnvelopeDO redEnvelopeDO = redEnvelopeCache.get(redEnvelopeId);
// 获取红包历史
Map> envelopeLogCache = globalDataCache.getEnvelopeLogCache();
Map 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工具,一时疏忽,但是既然用了就给大家提一嘴,需要引入下面的依赖:
cn.hutool
hutool-all
5.7.18
这是一个大而全的工具类,只要你想的到的,基本在这里面都有对应的工具类。不要错过。
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 peopleCache = new HashMap<>();
/**
* 存储全部红包数据
*/
private Map redEnvelopeCache = new HashMap<>();
/**
* 红包历史数据 Map<红包id, Map<用户id,GrabEnvelopeLogDO>>
*/
private Map> 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 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 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 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内存模型)分析下:
我们共享变量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 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
由上所示发现没有任何问题。
三、微服务常用组件
前面扯了一大堆基础到不能在基础的内容,下面我们自由飞翔一下,看看当今企业中常见的技术栈有哪些,可以利用到我们的抢红包的场景当中。
下面我简单画一幅架构图,列出比较常用的架构设计:
如上图所示,从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入门小知识,不知道对朋友们有没有帮助,码字不易,给点个赞和关注啊。
学完本篇抢红包的代码,保准让你过年每个红包都不落下,每次红包都抢最大的!!
小弟提前给你们拜年了!!!