1.不用redis?分布式锁,如何防止用户重复点击?
1.乐观锁
乐观锁是一种在数据库层面上避免并发冲突的机制。它通常通过在数据库记录中添加一个版本号(或时间戳)来实现。每次更新记录时,都会检查版本号是否与数据库中的版本号匹配,如果匹配,则更新数据并将版本号加一。这确保了在更新期间没有其他操作更改了记录。
- 应用场景:适用于更新操作并不频繁,且冲突概率较低的场景
代码实现:
//实体类:注意@Version注解
@Entity
@Table(name = "form_submissions")
public class FormSubmission {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "user_id")
private Long userId;
@Column(name = "form_data")
private String formData;
@Column(name = "version")
@Version
private int version;
// 构造函数、Getter和Setter略去
}
//业务层
@Service
public class FormSubmissionService {
@Autowired
private FormSubmissionRepository repository;
@Transactional
public String submitForm(Long userId, String formData) {
Optional<FormSubmission> existingSubmission = repository.findById(userId);
FormSubmission submission;
if (existingSubmission.isPresent()) {
// 更新现有记录
submission = existingSubmission.get();
submission.setFormData(formData);
} else {
// 创建新的记录
submission = new FormSubmission();
submission.setUserId(userId);
submission.setFormData(formData);
submission.setVersion(0); // 或根据需要设置初始版本号
}
try {
repository.save(submission);
return "表单提交成功。";
} catch (org.springframework.orm.ObjectOptimisticLockingFailureException e) {
// 捕获乐观锁异常,处理冲突
return "提交失败,请不要重复提交。";
}
}
}
//没有注解的时候sql层
UPDATE form_submissions
SET form_data = '新的表单数据', version = version + 1
WHERE id = ? AND version = ?;
2.数据库悲观锁
悲观锁通常通过数据库提供的锁机制实现,如 SQL 的 SELECT FOR UPDATE
语句,这会锁定被选中的数据库行,直到事务完成。这种方法适用于高冲突环境,因为它会阻止其他任何尝试修改这些行的操作。
- 应用场景:适用于更新操作频繁,且冲突概率高的场景。
代码实现:
@Service
public class FormSubmissionService {
@Autowired
private FormSubmissionRepository repository;
@Transactional
public boolean submitForm(Long userId, String formData) {
Optional<FormSubmission> existingSubmission = repository.findByUserIdForUpdate(userId);
if (existingSubmission.isPresent()) {
// 存在记录,处理重复提交逻辑
return false;
} else {
// 不存在记录,保存新的表单提交
FormSubmission submission = new FormSubmission();
submission.setUserId(userId);
submission.setFormData(formData);
repository.save(submission);
return true;
}
}
}
//sql层代码
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT fs FROM FormSubmission fs WHERE fs.userId = :userId")
Optional<FormSubmission> findByUserIdForUpdate(@Param("userId") Long userId);
3.基于内存的锁
如果你的应用程序运行在单个实例或能够使用共享内存系统(如 Hazelcast、Apache Ignite),可以使用内存中的数据结构来实现锁逻辑。例如,使用一个全局哈希表存储正在进行的操作的标识符,来防止重复。
- 应用场景:适用于单实例应用或者有共享内存系统的分布式应用。
@Service
public class FormSubmissionService {
private final Map<Long, Lock> userLocks = new HashMap<>();
public String submitForm(Long userId, String formData) {
Lock lock = userLocks.computeIfAbsent(userId, k -> new ReentrantLock());
if (lock.tryLock()) {
try {
// 模拟表单处理逻辑
Thread.sleep(1000); // 假设处理需要一段时间
System.out.println("表单数据处理: " + formData);
return "表单提交成功。";
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return "表单处理中断。";
} finally {
lock.unlock();
}
} else {
return "正在处理中,请不要重复提交。";
}
}
}
4.应用程序级的去重逻辑
在应用程序级别实现去重逻辑,例如,通过在前端禁用提交按钮,直到请求完成,或者在后端设置一个短暂的时间窗口,在这个窗口内忽略来自同一用户的重复请求。
- 应用场景:适用于需要快速实现且冲突概率不高的场景。
5.唯一标识符
要求客户端在请求时生成一个唯一的标识符(如 UUID),并在服务器端检查这个标识符是否已经被处理。这个标识符可以存储在内存或数据库中,以确保每个请求只被处理一次。
- 应用场景:适合于任何需要确保请求唯一性的场景,特别是在分布式系统中。
代码实现:
@Service
public class RequestService {
@Autowired
private RequestIdRepository requestIdRepository;
public boolean processRequest(String requestId) {
// 检查请求ID是否已存在
Optional<RequestId> existingRequestId = requestIdRepository.findById(requestId);
if (existingRequestId.isPresent()) {
// 请求ID已存在,拒绝重复处理
return false;
} else {
// 请求ID不存在,处理请求
RequestId newRequestId = new RequestId();
newRequestId.setId(requestId);
requestIdRepository.save(newRequestId); // 保存请求ID标记为已处理
// 在这里执行其他请求处理逻辑...
return true;
}
}
}
在这个实现中,客户端需要生成一个 UUID 并在每次请求时发送这个 UUID 作为 `requestId` 参数。服务器通过检查这个 `requestId` 是否已经存在于 `request_ids` 表中来防止重复处理相同的请求。这种方法适用于分布式系统中确保请求的唯一性,有效地防止了用户因为多次点击导致的重复请求问题。
2.Cookie、Session、Token、JWT之间的区别
傻傻分不清之 Cookie、Session、Token、JWT - 掘金
3.40亿个QQ号,限制1G内存,如何去重?
1.位图(Bitmap)
图是一种非常高效的数据结构,通过使用1个位来标记某个元素是否存在。假设我们使用40亿位(或者说500MB)的内存空间,就可以表示40亿个不同的QQ号。
-
初始化位图:创建一个大约500MB大小的位图,每个位对应一个可能的QQ号。由于QQ号可能不会完全连续,我们需要根据实际QQ号的范围来调整位图的大小。
-
标记QQ号:遍历所有QQ号,对每个QQ号,计算它在位图中的位置,并将相应的位设置为1。
-
去重:再次遍历QQ号,通过检查位图中对应位的值,可以判断一个QQ号是否已经出现过。
这种方法的缺点是,如果QQ号的范围非常大,位图的大小可能会超出1GB的内存限制。
代码实现:
public class Bitmap {
private byte[] bits;
public Bitmap(int size) {
bits = new byte[(size + 7) / 8];
}
public void set(int k) {
int byteIndex = k / 8;
int bitIndex = k % 8;
bits[byteIndex] |= (1 << bitIndex);
}
public boolean get(int k) {
int byteIndex = k / 8;
int bitIndex = k % 8;
return (bits[byteIndex] & (1 << bitIndex)) != 0;
}
}
// 假设QQ号范围在一定区间内,这里简化处理
Bitmap bitmap = new Bitmap(4000000000); // 大约需要500MB内存
// 设置QQ号
bitmap.set(qqNumber);
// 检查QQ号是否已存在
boolean exists = bitmap.get(qqNumber);
2.布隆过滤器(Bloom Filter)
布隆过滤器是一种空间效率极高的概率型数据结构,用于判断一个元素是否在集合中:
-
初始化布隆过滤器:根据数据量和可接受的误判率初始化布隆过滤器。
-
添加元素:遍历QQ号,将每个QQ号添加到布隆过滤器中。
-
预过滤:再次遍历QQ号,首先使用布隆过滤器检查是否可能已经存在。由于布隆过滤器存在一定的误判率,对于判断存在的元素,需要进一步确认。
布隆过滤器适用于快速预过滤,减少需要进一步处理的数据量,但需要额外的机制来处理误判。
public class BloomFilter {
private BitSet hashes;
private int size;
public BloomFilter(int size) {
this.size = size;
this.hashes = new BitSet(size);
}
private int hash(Object obj, int k) {
return Math.abs(obj.hashCode() * k) % size;
}
public void add(Object obj) {
for (int k = 1; k <= 3; k++) { // 使用3个不同的哈希函数
int hash = hash(obj, k);
hashes.set(hash);
}
}
public boolean mightContain(Object obj) {
for (int k = 1; k <= 3; k++) {
int hash = hash(obj, k);
if (!hashes.get(hash)) {
return false; // 如果有一个位不是1,那么对象肯定没有添加
}
}
return true; // 可能包含
}
}
// 使用
BloomFilter filter = new BloomFilter(100000000); // 需要调整大小以适应内存限制
filter.add(qqNumber);
boolean mightExist = filter.mightContain(qqNumber);
4.假如博主有上百万个粉丝关注,那么怎么保证正确而又完整地通知到用户呢?
在面对上百万粉丝关注的场景下,确保每个用户都能正确而完整地接收到通知是一个挑战。这个问题通常出现在社交网络、内容分发平台、即时通讯服务等系统中。为了应对这种挑战,通常会采用"推拉结合"(Push-Pull Hybrid)的模式来优化信息分发机制。下面是对"推拉结合"策略及其在feed流系统中应用的解释:
推拉结合(Push-Pull Hybrid)模式
推送(Push)机制
- 实时性:系统主动将新内容推送给用户,适用于需要高实时性的场景,如即时通讯。
- 资源消耗:对于有大量粉丝的博主,一次性推送会消耗大量的服务器资源和网络带宽。
- 适用场景:适合对实时性要求较高的通知,比如好友消息、直播开始通知等。
拉取(Pull)机制
- 按需获取:用户主动请求(拉取)新内容,适用于不需要即时了解最新信息的场景。
- 资源节约:服务器只在用户请求时才发送数据,减少了资源的浪费。
- 适用场景:适合用户浏览feed流、查看更新列表等场景,用户可以在自己方便的时候拉取最新内容。
推拉结合策略在Feed流中的应用
-
初始内容加载使用拉取:用户打开应用时,通过拉取机制加载feed流的初始内容。这样做可以减少服务器压力,因为内容加载是基于用户的主动请求。
-
实时更新使用推送:对于新内容的更新,特别是用户关注的博主有新动态时,可以采用推送机制通知用户。为了减少资源消耗,可以采取一些优化措施:
- 分批推送:将粉丝分成不同的批次,逐批推送更新,避免一次性对服务器造成过大压力。
- 仅推送通知:而不是推送全部新内容,只通知用户有新内容,用户点击通知后再通过拉取获取具体内容。
- 使用消息队列:利用消息队列管理推送任务,可以平衡负载,提高系统的扩展性和可靠性。
-
智能化推拉结合:系统可以根据用户的行为、偏好以及网络状况智能选择推送或拉取。例如,对于经常互动的粉丝使用推送,对于不活跃的用户则在其主动打开应用时通过拉取更新。
-
后台拉取:在某些应用场景中,可以在应用处于后台时做轻量级的数据拉取,保证用户回到应用时能看到较新的内容,同时不过度消耗用户的数据流量和电量。
推拉结合模式通过平衡系统资源消耗和用户体验的实时性,为处理大规模用户通知提供了一种可行且有效的解决方案。在实际应用中,根据具体的业务需求和用户行为,灵活调整推送和拉取的策略至关重要。