蜂信物联FastBee平台https://gitee.com/beecue/fastbee
阿里资料开源项目https://gitee.com/vip204888
百度低代码前端框架https://gitee.com/baidu/amis
OpenHarmony开源项目https://gitcode.com/openharmony
仓颉编程语言开放项目https://gitcode.com/Cangjie
@GetMapping(“/generateSequenceDateValue/{prefixName}/{width}”)
public AjaxResult generateSequenceDateValue(@PathVariable(“prefixName”) String prefixName, @PathVariable(“width”) Integer width) throws Exception {
return AjaxResult.success(“成功”,prefixSequenceService.generateSequenceDateValue(prefixName,width));
}
}
单次请求返回结果大致如下
AAAA-20230721-00001
AAAA-20230721-00002
AAAA-20230721-00003
BBBB-20230721-00001
BBBB-20230721-00002
返回了预期的结果。
下面我们模拟高并发场景下的顺序号生成
@RestController
@RequestMapping(“/sequence”)
public class PrefixSequenceController {
@Autowired
PrefixSequenceService prefixSequenceService;
/**
* 高并发下测试顺序号
* @param prefixName
* @param width
* @return
* @throws Exception
*/
@GetMapping("/test/generateSequenceDateValue/{prefixName}/{width}")
public AjaxResult testGenerateSequenceDateValue(@PathVariable("prefixName") String prefixName, @PathVariable("width") Integer width) throws Exception {
CountDownLatch countDownLatch = new CountDownLatch(100);
class PrefixSequenceRun implements Runnable {
@SneakyThrows
@Override
public void run() {
countDownLatch.countDown();
countDownLatch.await();
String sequence = prefixSequenceService.generateSequenceDateValue(prefixName,width);
System.out.println(sequence);
}
}
for(int i=0;i<100;i++){
new Thread(new PrefixSequenceRun()).start();
}
return AjaxResult.success();
}
}
使用闭锁模拟100次同时请求顺序号服务(前缀为AAAA,宽度为5)。
输出大致结果如下
ERROR:Duplicate entry 'AAAA-5-1' for key 'prefix\_sequence.PRIMARY'
AAAA-20230721-00002
AAAA-20230721-00002
AAAA-20230721-00002
AAAA-20230721-00002
AAAA-20230721-00002
AAAA-20230721-00003
AAAA-20230721-00003
AAAA-20230721-00003
我们发现并没有按照我们预期的产生正确的顺序号,有抛出主键重复的异常,并且生成了重复的顺序号。
这是为什么呢?这是因为我们没有考虑到线程安全的问题,在基础的顺序号生成服务中,如果有
2个线程同时访问了前缀为AAAA宽度为5的服务,第一个线程发现数据库中没有找到信息,所以就生
成一个新的顺序号,在第一个线程还没有保存的时候,第二个线程进来了,它也发现没有找到前缀为
AAAA宽度为5的数据所以也生成了一个新的顺序号,当第一个线程保存了顺序号以后,第二个线程
才执行保存操作,所以就抛出了主键重复的异常。重复的顺序号也是因为线程不安全造成的。
那么我们怎么解决线程安全的问题呢?答案是在服务入口处加一把锁。
/**
-
前缀顺序号服务实现
*/
@Service
public class PrefixSequenceServiceImpl implements PrefixSequenceService {@Autowired
PrefixSequenceMapper prefixSequenceMapper;// 可重入锁
Lock lock = new ReentrantLock();@Override
public String generateSynSequenceDateValue(String prefixName, Integer width) throws Exception {
if(StringUtils.isEmpty(prefixName)||null == width){
throw new Exception(“参数不能为空”);
}
// 在逻辑入口处加锁,粒度粗,服务吞吐低
lock.lock();
try{
// 查询数据库顺序号
PrefixSequence prefixSequence = prefixSequenceMapper.selectByPrimaryKey(prefixName,width,1);
// 如果没有就新建
if(prefixSequence==null){
String newSequence = generateNewSequence(prefixName,width,1);
SimpleDateFormat dateFormat=new SimpleDateFormat(“yyyyMMdd”);
String format = dateFormat.format(new Date());
return prefixName+“-”+format+“-”+newSequence;
}else{
SimpleDateFormat dateFormat=new SimpleDateFormat(“yyyyMMdd”);
// 如果是当天
if(SequenceStringUtil.judgeSameDay(prefixSequence)){
// 将顺序号转换为Long
Long value = Long.valueOf(prefixSequence.getSequenceValue());
String updateValue = SequenceStringUtil.formatSequenceValue(width,++value);
prefixSequence.setSequenceValue(updateValue);
prefixSequenceMapper.updateByPrimaryKeySelective(prefixSequence);
// 返回更新后的顺序号
String updateSequence = prefixSequence.getPrefixName()+“-”+
dateFormat.format(prefixSequence.getSequenceDate())+“-”+
prefixSequence.getSequenceValue();
return updateSequence;
}else{
String newSequence = SequenceStringUtil.formatSequenceValue(width, 1L);
prefixSequence.setSequenceValue(newSequence);
prefixSequence.setSequenceDate(new Date());
prefixSequenceMapper.updateByPrimaryKeySelective(prefixSequence);
// 返回更新后的顺序号
String updateSequence = prefixSequence.getPrefixName()+“-”+
dateFormat.format(prefixSequence.getSequenceDate())+“-”+
prefixSequence.getSequenceValue();
return updateSequence;
}
}
}finally {
lock.unlock();
}
}/**
- 辅助方法
- 创建一个新的顺序号,指定宽度,从1开始
- @param prefixName
- @param width
- @param dateFlag
- @return
*/
private String generateNewSequence(String prefixName, Integer width, Integer dateFlag) {
// 创建一个新的顺序号,指定宽度,从1开始
String newSequence = SequenceStringUtil.formatSequenceValue(width, 1L);
// 保存顺序号
PrefixSequence prefixSequence = new PrefixSequence();
prefixSequence.setPrefixName(prefixName);
prefixSequence.setWidth(width);
prefixSequence.setSequenceValue(newSequence);
prefixSequence.setSequenceDate(new Date());
prefixSequence.setDateFlag(dateFlag);
prefixSequenceMapper.insertSelective(prefixSequence);
// 返回新生成的顺序号
return newSequence;
}
}
我们使用 ReentrantLock 来为服务加一把锁。
再次测试
这次输出的结果正确了,我们解决了线程安全的问题。
但是如果我们同时请求不同前缀的顺序号会是什么样的情况呢?
测试代码
@RestController
@RequestMapping(“/sequence”)
public class PrefixSequenceController {
@Autowired
PrefixSequenceService prefixSequenceService;
/**
* 高并发下测试顺序号,1 在逻辑入口处加锁,粒度粗,服务吞吐低
* @param prefixName
* @param width
* @return
* @throws Exception
*/
@GetMapping("/test2/generateSequenceDateValue/{prefixName}/{width}")
public AjaxResult test2GenerateSequenceDateValue(@PathVariable("prefixName") String prefixName, @PathVariable("width") Integer width) throws Exception {
CountDownLatch countDownLatch = new CountDownLatch(100);
class PrefixSequenceRunA implements Runnable {
@SneakyThrows
@Override
public void run() {
countDownLatch.countDown();
countDownLatch.await();
String sequence = prefixSequenceService.generateSynSequenceDateValue("AAAA",width);
System.out.println(sequence);
}
}
class PrefixSequenceRunB implements Runnable {
@SneakyThrows
@Override
public void run() {
countDownLatch.countDown();
countDownLatch.await();
String sequence = prefixSequenceService.generateSynSequenceDateValue("BBBB",width);
System.out.println(sequence);
}
}
for(int i=0;i<50;i++){
new Thread(new PrefixSequenceRunA()).start();
}
for(int i=0;i<50;i++){
new Thread(new PrefixSequenceRunB()).start();
}
return AjaxResult.success();
}
}
这次我们分别请求50次前缀为AAAA宽度为5和前缀为BBBB宽度为5的顺序号
输出大致结果如下
AAAA-20230721-00001
BBBB-20230721-00001
AAAA-20230721-00002
BBBB-20230721-00002
AAAA-20230721-00003
AAAA-20230721-00004
BBBB-20230721-00003
AAAA-20230721-00005
…
BBBB-20230721-00004
BBBB-20230721-00005
…
如果在顺序号服务中让线程休眠1秒,我们发现在同一时间,也就是1秒内只能生成一个顺
序号。不管是相同的前缀编码还是不同的前缀编码,在同一时刻只能生成一个顺序号,这种吞吐量
在真实的项目环境中是很差的,我们还需要继续优化。
为了保证线程的安全,相同前缀的的顺序号必须要保证串行执行。但是不同前缀的顺序号是
可以并行执行的,这样就大大优化了并发性能。
思想仍然是加锁,但是同一前缀的顺序号生成时加锁,不同前缀的顺序号在同一时刻可以并
行,(其实也加锁了,只不过每个不同前缀的唯一一个访问线程在并发执行)
我们需要自定义一把显示锁(JDK提供的锁或并发工具没有直接实现该功能的),定义类似于
keyLock.lock(prefixName); 的加锁方法,以及 keyLock.unlock(prefixName); 的解锁方法,来替换服
务中的 ReentrantLock。
自定义锁的实现代码
/**
-
前缀锁
-
@param
*/
public class PrefixSequenceKeyLock {// 用于保存锁定的key和信号量
private final ConcurrentMap<K, Semaphore> semaphoreMap = new ConcurrentHashMap<K, Semaphore>();// 用于保存每个线程锁定的key
private final ThreadLocal<Map<K, LockInnerSupport>> threadLocal = new ThreadLocal<Map<K, LockInnerSupport>>() {
@Override
protected Map<K, LockInnerSupport> initialValue() {
return new HashMap<K, LockInnerSupport>();
}
};/**
- 对同一Key加锁,不同Key可以并发执行。
- @param key
*/
public void lock(K key) {
if (key == null){
return;
}
// 获取本线程绑定key的map(local)
LockInnerSupport lockSupport = threadLocal.get().get(key);
// 如果是本线程第一次访问,那么info为空
if (lockSupport == null) {
// 创建1个许可证的信号量
Semaphore current = new Semaphore(1);
// 获取许可
current.acquireUninterruptibly();
// 返回上一个线程保存的该key的信号量
// 如果key不存在,put值并返回null。如果key存在,put更新值并返回旧值。
Semaphore previous = semaphoreMap.put(key, current);
// 当第二个以后的线程访问该key previous 获取的是上一个线程保存的信号量
// 当key不存在时、即第一次semaphoreMap保存key,previous为空
if (previous != null){
// 相同key的顺序号生成线程会阻塞在这里直到上一个线程释放锁(许可证)
// 如果previous不为空,那么它的许可证为0,所以再次获取许可证时会被阻塞在这里,直到上一个访问该key的线程释放许可证才会继续执行
previous.acquireUninterruptibly();
}
// 更新local把该key对应的LockInfo放入ThreadLocal
threadLocal.get().put(key, new LockInnerSupport(current));
} else {
// 支持可重入
lockSupport.lockCount++;
}
}
/**
- 释放key,唤醒其他等待该key的线程
- @param key
*/
public void unlock(K key) {
if (key == null){
return;
}
// 获取当前线程对应key的信号量
LockInnerSupport lockSupport = threadLocal.get().get(key);
// 释放许可证,支持可重入
if (lockSupport != null && --lockSupport.lockCount == 0) {
// 释放许可,唤醒该key中阻塞的其他线程
lockSupport.current.release();
// 删除该key
semaphoreMap.remove(key, lockSupport.current);
// 删除该key,防止OOM
threadLocal.get().remove(key);
}
}
/**
-
封装信号量和计数的内部服务支持类
*/
private static class LockInnerSupport {
private final Semaphore current;
private int lockCount;private LockInnerSupport(Semaphore current) {
this.current = current;
this.lockCount = 1;
}
}
}
使用ConcurrentMap来保存锁定的key和信号量。
使用ThreadLocal来保存每一个线程自己的锁定key。
用Semaphore来控制线程的阻塞和唤醒。
关键在于这里
Semaphore previous = semaphoreMap.put(key, current);
if (previous != null){
previous.acquireUninterruptibly();
}
还是拿前缀编码AAAA 和 宽度5 来举例。
当已经有一个线程在生成AAAA宽度为5的顺序号时,它会把自己的Key和许可证已经减1的信号量注
册到semaphoreMap中, 在这个线程还没有释放锁的时候,下一个访问Key为AAAA宽度为5的顺序号线程
也把自己的Key和许可证已经减1的信号量注册到semaphoreMap中,并且得到上一个线程保存在semaphoreMap
的信号量,然后再次扣减1个许可证,变为了-1,这样就可以阻塞住第二个访问的线程,直到第一个线程释放锁
才会唤醒第二个线程继续执行。
替换为自定义显示锁的业务代码
/**
-
前缀顺序号服务实现
*/
@Service
public class PrefixSequenceServiceImpl implements PrefixSequenceService {@Autowired
PrefixSequenceMapper prefixSequenceMapper;// 自定义锁,相同前缀加锁,不同前缀并发执行
private PrefixSequenceKeyLock keyLock = new PrefixSequenceKeyLock();@Override
public String generateSynKeyLockSequenceDateValue(String prefixName, Integer width) throws Exception {
if(StringUtils.isEmpty(prefixName)||null == width){
throw new Exception(“参数不能为空”);
}
// 相同的前缀名加锁,不同的前缀名可以并行,力度细,服务吞吐高
keyLock.lock(prefixName);
try{
// 查询数据库顺序号
PrefixSequence prefixSequence = prefixSequenceMapper.selectByPrimaryKey(prefixName,width,1);
// 如果没有就新建
if(prefixSequence==null){
String newSequence = generateNewSequence(prefixName,width,1);
SimpleDateFormat dateFormat=new SimpleDateFormat(“yyyyMMdd”);
String format = dateFormat.format(new Date());
return prefixName+“-”+format+“-”+newSequence;
}else{
SimpleDateFormat dateFormat=new SimpleDateFormat(“yyyyMMdd”);
// 如果是当天
if(SequenceStringUtil.judgeSameDay(prefixSequence)){
// 将顺序号转换为Long
Long value = Long.valueOf(prefixSequence.getSequenceValue());
String updateValue = SequenceStringUtil.formatSequenceValue(width,++value);
prefixSequence.setSequenceValue(updateValue);
prefixSequenceMapper.updateByPrimaryKeySelective(prefixSequence);
// 返回更新后的顺序号
String updateSequence = prefixSequence.getPrefixName()+“-”+
dateFormat.format(prefixSequence.getSequenceDate())+“-”+
prefixSequence.getSequenceValue();
return updateSequence;
}else{
String newSequence = SequenceStringUtil.formatSequenceValue(width, 1L);
prefixSequence.setSequenceValue(newSequence);
prefixSequence.setSequenceDate(new Date());
prefixSequenceMapper.updateByPrimaryKeySelective(prefixSequence);
// 返回更新后的顺序号
String updateSequence = prefixSequence.getPrefixName()+“-”+
dateFormat.format(prefixSequence.getSequenceDate())+“-”+
prefixSequence.getSequenceValue();
return updateSequence;
}
}
}finally {
keyLock.unlock(prefixName);
}
}/**
- 辅助方法
- 创建一个新的顺序号,指定宽度,从1开始
- @param prefixName
- @param width
- @param dateFlag
- @return
*/
private String generateNewSequence(String prefixName, Integer width, Integer dateFlag) {
// 创建一个新的顺序号,指定宽度,从1开始
String newSequence = SequenceStringUtil.formatSequenceValue(width, 1L);
// 保存顺序号
PrefixSequence prefixSequence = new PrefixSequence();
prefixSequence.setPrefixName(prefixName);
prefixSequence.setWidth(width);
prefixSequence.setSequenceValue(newSequence);
prefixSequence.setSequenceDate(new Date());
prefixSequence.setDateFlag(dateFlag);
prefixSequenceMapper.insertSelective(prefixSequence);
// 返回新生成的顺序号
return newSequence;
}
}
我们再次测试不同前缀的服务请求。
测试代码
@RestController
@RequestMapping(“/sequence”)
public class PrefixSequenceController {
@Autowired
PrefixSequenceService prefixSequenceService;
/**
* 高并发下测试顺序号,2 相同的前缀名加锁,不同的前缀名可以并行,粒度细,服务吞吐高
* @param prefixName
* @param width
* @return
* @throws Exception
*/
@GetMapping("/test3/generateSequenceDateValue/{prefixName}/{width}")
public AjaxResult test3GenerateSequenceDateValue(@PathVariable("prefixName") String prefixName, @PathVariable("width") Integer width) throws Exception {
CountDownLatch countDownLatch = new CountDownLatch(150);
class PrefixSequenceRunA implements Runnable {
@SneakyThrows
@Override
public void run() {
countDownLatch.countDown();
countDownLatch.await();
String sequence = prefixSequenceService.generateSynKeyLockSequenceDateValue("AAAA",width);
System.out.println(sequence);
}
}
class PrefixSequenceRunB implements Runnable {
@SneakyThrows
@Override
public void run() {
countDownLatch.countDown();
countDownLatch.await();
String sequence = prefixSequenceService.generateSynKeyLockSequenceDateValue("BBBB",width);
System.out.println(sequence);
}
}
class PrefixSequenceRunC implements Runnable {
@SneakyThrows
@Override
public void run() {
countDownLatch.countDown();
countDownLatch.await();
String sequence = prefixSequenceService.generateSynKeyLockSequenceDateValue("CCCC",width);
System.out.println(sequence);
}
}
for(int i=0;i<50;i++){
new Thread(new PrefixSequenceRunA()).start();
}
for(int i=0;i<50;i++){
new Thread(new PrefixSequenceRunB()).start();
}
for(int i=0;i<50;i++){
new Thread(new PrefixSequenceRunC()).start();
}
return AjaxResult.success();
}
}
这次我们分别请求50次前缀为AAAA宽度为5和前缀为BBBB宽度为5和前缀为CCCC宽度为5的顺序号。
输出大致结果如下
AAAA-20230721-00001
BBBB-20230721-00001
CCCC-20230721-00001
AAAA-20230721-00002
BBBB-20230721-00002
CCCC-20230721-00002
AAAA-20230721-00003
BBBB-20230721-00003
CCCC-20230721-00003
AAAA-20230721-00004
BBBB-20230721-00004
CCCC-20230721-00004
…
# 最后
关于面试刷题也是有方法可言的,建议最好是按照专题来进行,然后由基础到高级,由浅入深来,效果会更好。当然,这些内容我也全部整理在一份pdf文档内,分成了以下几大专题:
* Java基础部分
![](https://img-blog.csdnimg.cn/img_convert/38cda247ef865176f1edecbf273512dd.webp?x-oss-process=image/format,png)
* 算法与编程
![](https://img-blog.csdnimg.cn/img_convert/317d31ba072c782ffce08b2241ea6e0d.webp?x-oss-process=image/format,png)
* 数据库部分
![](https://img-blog.csdnimg.cn/img_convert/781d1d46243fc5697265c4ff10d8d641.webp?x-oss-process=image/format,png)
* 流行的框架与新技术(Spring+SpringCloud+SpringCloudAlibaba)
![](https://img-blog.csdnimg.cn/img_convert/6e9c0b4bc037fed733bd256aa8172481.webp?x-oss-process=image/format,png)
这份面试文档当然不止这些内容,实际上像JVM、设计模式、ZK、MQ、数据结构等其他部分的面试内容均有涉及,因为文章篇幅,就不全部在这里阐述了。
**作为一名程序员,阶段性的学习是必不可少的,而且需要保持一定的持续性,这次在这个阶段内,我对一些重点的知识点进行了系统的复习,一方面巩固了自己的基础,另一方面也提升了自己的知识广度和深度。**
003
BBBB-20230721-00003
CCCC-20230721-00003
AAAA-20230721-00004
BBBB-20230721-00004
CCCC-20230721-00004
…
# 最后
关于面试刷题也是有方法可言的,建议最好是按照专题来进行,然后由基础到高级,由浅入深来,效果会更好。当然,这些内容我也全部整理在一份pdf文档内,分成了以下几大专题:
* Java基础部分
[外链图片转存中...(img-2hyI4jJi-1725170164555)]
* 算法与编程
[外链图片转存中...(img-TlbphGP4-1725170164556)]
* 数据库部分
[外链图片转存中...(img-F9UU5ACf-1725170164556)]
* 流行的框架与新技术(Spring+SpringCloud+SpringCloudAlibaba)
[外链图片转存中...(img-d2GZK3Dw-1725170164557)]
这份面试文档当然不止这些内容,实际上像JVM、设计模式、ZK、MQ、数据结构等其他部分的面试内容均有涉及,因为文章篇幅,就不全部在这里阐述了。
**作为一名程序员,阶段性的学习是必不可少的,而且需要保持一定的持续性,这次在这个阶段内,我对一些重点的知识点进行了系统的复习,一方面巩固了自己的基础,另一方面也提升了自己的知识广度和深度。**