forked from rbmonster/learning-note
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
20 changed files
with
1,446 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
# 会议室预约系统 | ||
|
||
会议室预约基本元素: | ||
1. 会议室:会议室ID、可容纳人数、是否多媒体等等 | ||
2. 日期 | ||
3. 基本信息:预约人等 | ||
|
||
## 常规思路 | ||
会议室预约是一个时间段,包括开始时间、结束时间。 | ||
|
||
### 数据库设计 | ||
1. 会议室信息主表。 | ||
2. 人员信息表 | ||
3. 预约信息主表、子表 | ||
|
||
|
||
- 以下为预约信息主表`t_reserve`及子表 `t_reserve_detail`: | ||
|
||
| 列 | 类型 | 描述 | | ||
| --- | --- | ---| | ||
|id|int64|自增键| | ||
|meeting_id|int64|索引,会议表的id| | ||
|date_month|date|索引,会议室日期,形如2019-02-18| | ||
|
||
| 列 | 类型 | 描述 | | ||
| --- | --- | ---| | ||
|id|int64|自增键| | ||
|reserve_id|int64|索引,预约主表ID| | ||
|start_time|date|开始时间,形如2019-02-18 12:00| | ||
|end_time|date|开始时间,形如2019-02-18 12:00| | ||
|
||
### 处理逻辑 | ||
|
||
#### 查询逻辑 | ||
查询某天某个时间段空闲会议室: | ||
``` | ||
select t.meeting_id from t_reserve left t join t_reserve_detail td on t.id = t.reserve_id | ||
where t.date_month = xxx and not exists | ||
( | ||
select reserve_id from t_reserve_detail td1 | ||
where start_time between '2019-07-27 00:00:00' and '2019-07-29 00:00:00' and td1.reserve_id = t.id | ||
union | ||
select reserve_id from t_reserve_detail td2 | ||
where end_time between '2019-07-27 00:00:00' and '2019-07-29 00:00:00' | ||
and td2.reserve_id = t.id | ||
) | ||
``` | ||
|
||
#### 会议室预约路基 | ||
1. 查询选定时间段有的会议室 | ||
2. 选定会议室 | ||
3. 选定时间段 | ||
4. 进行主子表的信息插入。 | ||
|
||
#### 并发控制 | ||
1. 单体应用使用java锁进行控制 | ||
2. 集群或者分布式应用使用数据库乐观锁或者redis 分布式锁控制 | ||
|
||
#### 问题点 | ||
1. 查询的性能问题 | ||
2. 并发控制的性能问题: | ||
1. 使用数据库乐观锁,用户体验问题。 | ||
2. redis分布式锁,key:会议ID+日期,一个会议室不同时间段预约不冲突的情况,也需要获取锁。 | ||
|
||
## 改进思路 | ||
|
||
1. 查询效率优化 | ||
2. 并发控制优化 | ||
|
||
实际的会议室预约中,不开始时间及结束时间会精细到具体的分秒。可以与产品沟通将需求确定到以5min、10min、15min的粒度。 | ||
|
||
### 查询优化 | ||
1. 根据5min将一天24h进行划分 | ||
- 24*60/5 = 288 | ||
2. 主表上新增区间字段,代表当前区间是否有会议室预约 | ||
- 假设以2h为刻度,那么新增12个字段区间 T1 ~ T12。 | ||
- 按`位0、1` 代表当前5min刻度是否有人预约 | ||
|
||
|
||
实际查询过程:根据起始时间和结束时间换算对应的区间 | ||
``` | ||
场景一: 同一个区间 | ||
startTime = 12:00 | ||
endTime = 12:40 | ||
区间换算 => 第7个区间(T7) | ||
select meeting_id from demo where SUBSTR(T7 FROM 0 FOR 8) = 0 | ||
场景二:跨区间 | ||
startTime = 11:30 -> T6 | ||
endTime = 12:40 -> T7 | ||
select meeting_id from demo where T6 < xxx and SUBSTR(T7 FROM 0 FOR 8) = 0 | ||
场景三: 跨天 TODO | ||
``` | ||
|
||
### 并发优化 | ||
使用redis分布式锁锁对应的时间区间,减小锁粒度。 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
# 表结构设计 | ||
|
||
## 部门表设计 | ||
单表的设计方案,使用parent_id做关系连接,形成级联结构。例子: | ||
``` | ||
id | parent_id | deparement_name | department_desc| ... | ||
``` | ||
|
||
问题点: | ||
- 如何根据一个部门查询对应的父部门及子部门? | ||
1. 使用java内存操作,一次性读取所有部门信息,进行级联组装。再通过id查询子节点的所有部门。 | ||
2. 添加表字段,添加一个path字段,用于表示父部门的所有路径。 | ||
|
||
## 签到表设计 | ||
用户签到只有两种状态,因此可以使用`位 0 1` 表示签到的状态。使用mask 32位的int 表示每个月签到的次数 | ||
1. 获取用户当月的签到状态: | ||
- 根据服务器时间判断当前的月份month,根据传入的user_id和month去数据库中查找用户签到的数据mask。 | ||
2. 签到: | ||
- 假设当日是本月第i天(这个可以计算得出),更新数据库中mask: mask = mask | (1 << i),更新连续签到天数,若i是1或第i-1天没有签到,本月连续签到天数置为1;其他情况则更新连续签到天数+1。 | ||
3. 判断当日是否签到: | ||
- 如果mask & (1 << i)大于0,说明签到了,如果为0,说明未签到。 | ||
4. 本月补签: | ||
- 补签某个日期,假设是第j天,更新数据库中mask: mask = mask | (1 << j),重新统计本月连续签到天数,从第i位开始逆序遍历到第1天统计连续签到天数。 | ||
|
||
|
||
| 列 | 类型 | 描述 | | ||
| --- | --- | ---| | ||
|id|int64|自增键| | ||
|user_id|int64|索引,用户表的id| | ||
|date_month|date|索引,月份,形如2019-02| | ||
|mask|int32|用户签到的数据| | ||
|continue_sign_month|int32|用户本月连续签到的天数| | ||
|
||
- 相关资料: [签到设计](https://blog.csdn.net/liyunlong41/article/details/86739134) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,160 @@ | ||
package com.four.messageconvert; | ||
|
||
import org.apache.commons.lang3.StringUtils; | ||
import sun.misc.BASE64Decoder; | ||
import sun.misc.BASE64Encoder; | ||
|
||
import javax.crypto.Cipher; | ||
import javax.crypto.spec.SecretKeySpec; | ||
|
||
|
||
/** | ||
* <pre> | ||
* @Description: | ||
* | ||
* </pre> | ||
* | ||
* @version v1.0 | ||
* @ClassName: AESUtils | ||
* @Author: sanwu | ||
* @Date: 2021/1/24 11:16 | ||
*/ | ||
public class AESUtils { | ||
|
||
/** | ||
* 加解密密钥, 外部可以 | ||
*/ | ||
public static final String AES_DATA_SECURITY_KEY = "4%YkW!@g5LGcf9Ut"; | ||
/** | ||
* 算法/加密模式/填充方式 | ||
*/ | ||
private static final String AES_PKCS5P = "AES/ECB/PKCS5Padding"; | ||
|
||
private static final String AES_PERSON_KEY_SECURITY_KEY = "pisnyMyZYXuCNcRd"; | ||
|
||
/** | ||
* 加密 | ||
* | ||
* @param str 需要加密的字符串 | ||
* @param key 密钥 | ||
* @return | ||
* @throws Exception | ||
*/ | ||
public static String encrypt(String str, String key) { | ||
if (StringUtils.isEmpty(key)) { | ||
throw new RuntimeException("key不能为空"); | ||
} | ||
try { | ||
if (str == null) { | ||
return null; | ||
} | ||
// 判断Key是否为16位 | ||
if (key.length() != 16) { | ||
return null; | ||
} | ||
byte[] raw = key.getBytes("UTF-8"); | ||
SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES"); | ||
// "算法/模式/补码方式" | ||
Cipher cipher = Cipher.getInstance(AES_PKCS5P); | ||
cipher.init(Cipher.ENCRYPT_MODE, skeySpec); | ||
byte[] encrypted = cipher.doFinal(str.getBytes("UTF-8")); | ||
// 此处使用BASE64做转码功能,同时能起到2次加密的作用。 | ||
return new BASE64Encoder().encode(encrypted); | ||
} catch (Exception ex) { | ||
return null; | ||
} | ||
|
||
} | ||
|
||
/** | ||
* 解密 | ||
* | ||
* @param str 需要解密的字符串 | ||
* @param key 密钥 | ||
* @return | ||
*/ | ||
public static String decrypt(String str, String key) { | ||
if (StringUtils.isEmpty(key)) { | ||
throw new RuntimeException("key不能为空"); | ||
} | ||
try { | ||
if (str == null) { | ||
return null; | ||
} | ||
// 判断Key是否为16位 | ||
if (key.length() != 16) { | ||
return null; | ||
} | ||
byte[] raw = key.getBytes("UTF-8"); | ||
SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES"); | ||
Cipher cipher = Cipher.getInstance(AES_PKCS5P); | ||
cipher.init(Cipher.DECRYPT_MODE, skeySpec); | ||
// 先用base64解密 | ||
byte[] encrypted = new BASE64Decoder().decodeBuffer(str); | ||
try { | ||
byte[] original = cipher.doFinal(encrypted); | ||
String originalString = new String(original, "UTF-8"); | ||
return originalString; | ||
} catch (Exception e) { | ||
return null; | ||
} | ||
} catch (Exception ex) { | ||
return null; | ||
} | ||
} | ||
|
||
/** | ||
* 加密 | ||
* | ||
* @param str 需要加密的字符串 | ||
* @return | ||
* @throws Exception | ||
*/ | ||
public static String encrypt(String str) { | ||
return encrypt(str, AES_DATA_SECURITY_KEY); | ||
} | ||
|
||
/** | ||
* 解密 | ||
* | ||
* @param str 需要解密的字符串 | ||
* @return | ||
*/ | ||
public static String decrypt(String str) { | ||
return decrypt(str, AES_DATA_SECURITY_KEY); | ||
} | ||
|
||
/** | ||
* 查询的时候对某些字段解密 | ||
* | ||
* @param str | ||
* @return | ||
*/ | ||
public static String aesDecrypt(String str) { | ||
if (StringUtils.isBlank(str)) { | ||
return " "; | ||
} | ||
String sql = " AES_DECRYPT(from_base64(" + str + ")," + "'" + AES_DATA_SECURITY_KEY + "')"; | ||
return sql; | ||
} | ||
|
||
/** | ||
* 对personKey加密 | ||
* | ||
* @param personKey | ||
* @return | ||
*/ | ||
public static String encryptPersonKey(String personKey) { | ||
return AESUtils.encrypt(personKey, AES_PERSON_KEY_SECURITY_KEY); | ||
} | ||
|
||
/** | ||
* 对personKey解密 | ||
* | ||
* @param personKey | ||
* @return | ||
*/ | ||
public static String decryptPersonKey(String personKey) { | ||
return AESUtils.decrypt(personKey, AES_PERSON_KEY_SECURITY_KEY); | ||
} | ||
} |
Oops, something went wrong.