java防止重复提交
请把小熊还给我&
已于 2022-07-04 23:20:15 修改
1359
收藏 6
分类专栏: java面试 spring JAVA 文章标签: java 开发语言
版权
java面试
同时被 3 个专栏收录
10 篇文章1 订阅
订阅专栏
spring
11 篇文章1 订阅
订阅专栏
JAVA
3 篇文章0 订阅
订阅专栏
公司有个抽奖活动每人一天只能抽取一次,有用户恶意短时间内重复提交多次导致抽奖多发情况
解决思路
1.创建一个map集合存储每个用户对象作为对象锁,存储用户对象时要采用双重校验锁保证唯一性
2.在控制层加同步代码块,不能在业务层加因为事务会导致同步代码块失效
3.抽奖完成后进行把用户给移除掉释放内存
1.工具类
package com.yujie.utils;
import com.yujie.model.User;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class MapConcurrent {
private static Map<String, User> userMap= new ConcurrentHashMap<>();
public static User getUser(String name){
//双重校验
if (!userMap.containsKey(name)) {
synchronized (MapConcurrent.class){
if (!userMap.containsKey(name)) {
User user = new User();
user.setUserName(name);
userMap.put(name,user);
return user;
}
}
}
return userMap.get(name);
}
public static void removeUser(String name){
userMap.remove(name);
}
}
2.controller层,模拟用户抽奖代码
@RequestMapping("/consume")
public String consume(){
//伪造前端数据
User user1 = new User();
user1.setUserName("王大宝");
user1.setId(1);
user1.setNum(1);
User u = MapConcurrent.getUser(user1.getUserName());
System.out.println("当前对象锁"+u.hashCode());
//加锁并发安全
synchronized (u){
userService.consume(user1);
//抽奖结束销毁用户对象
MapConcurrent.removeUser(user1.getUserName());
}
return "抽奖成功";
}
2.service层代码
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
@Override
@Transactional
public void consume(User user) {
System.out.println(Thread.currentThread().getName());
User user1 = selectUserById(user.getId());
if(user1 != null){
Integer num = user1.getNum();
if(num != null && num>0){
user1.setNum(num-1);
userDao.save(user1);
System.err.println("100元话费卡-卡号:"+new Random().nextInt(99999999)+"密码:"+new Random().nextInt(99999999) +",剩余抽奖次数:"+(num-1));
}
}
}
以下是模拟用户10次并发提交抽奖
3.没加锁前运行效果,导致一次抽奖发了10次奖品的bug
当前对象锁1743829393
当前对象锁1743829393
当前对象锁1743829393
当前对象锁1743829393
当前对象锁1743829393
当前对象锁1743829393
当前对象锁1743829393
当前对象锁1743829393
当前对象锁1743829393
当前对象锁1743829393
http-nio-8080-exec-5
http-nio-8080-exec-10
http-nio-8080-exec-9
http-nio-8080-exec-7
http-nio-8080-exec-4
http-nio-8080-exec-6
http-nio-8080-exec-1
http-nio-8080-exec-3
http-nio-8080-exec-2
100元话费卡-卡号:30879231密码:6585717,剩余抽奖次数:0
100元话费卡-卡号:92273305密码:14406295,剩余抽奖次数:0
100元话费卡-卡号:38463818密码:4953953,剩余抽奖次数:0
100元话费卡-卡号:1038147密码:34559577,剩余抽奖次数:0
100元话费卡-卡号:56007678密码:41413612,剩余抽奖次数:0
100元话费卡-卡号:33740124密码:81815012,剩余抽奖次数:0
100元话费卡-卡号:9332570密码:19397480,剩余抽奖次数:0
100元话费卡-卡号:36659655密码:58524451,剩余抽奖次数:0
100元话费卡-卡号:6957685密码:98985577,剩余抽奖次数:0
100元话费卡-卡号:71413799密码:29059920,剩余抽奖次数:0
4.加锁后运行效果
当前对象锁2137528174
当前对象锁2137528174
当前对象锁2137528174
当前对象锁2137528174
当前对象锁2137528174
当前对象锁2137528174
当前对象锁2137528174
当前对象锁2137528174
当前对象锁2137528174
当前对象锁2137528174
http-nio-8080-exec-3
100元话费卡-卡号:58114881密码:79931529,剩余抽奖次数:0
http-nio-8080-exec-2
http-nio-8080-exec-1
http-nio-8080-exec-5
http-nio-8080-exec-4
http-nio-8080-exec-10
http-nio-8080-exec-7
http-nio-8080-exec-9
http-nio-8080-exec-8
http-nio-8080-exec-6
方式二采用redis setnx解决
package com.yujie.utils;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.Objects;
/**
* @ClassName RedisLockUtil
* @Description 使用redis做锁
* @Author Wangyujie
* @Version V1.1.0
*/
@Component
public class RedisLockUtil {
@Resource
RedisTemplate<String, Object> redisTemplate;
/**
* 获取锁,true 则得到锁,false 已被锁定
* @param lockName 锁名称
* @param lockExoire 锁时间毫秒
* @return
*/
public Boolean getLock(String lockName, Integer lockExoire) {
return (Boolean) redisTemplate.execute((RedisCallback<?>) connection -> {
// 获取时间毫秒值
long expireAt = System.currentTimeMillis() + lockExoire + 1;
// 获取锁
Boolean acquire = connection.setNX(lockName.getBytes(), String.valueOf(expireAt).getBytes());
if (acquire) {
return true;
} else {
byte[] bytes = connection.get(lockName.getBytes());
// 非空判断
if (Objects.nonNull(bytes) && bytes.length > 0) {
long expireTime = Long.parseLong(new String(bytes));
// 如果锁已经过期
if (expireTime < System.currentTimeMillis()) {
// 重新加锁,防止死锁
byte[] set = connection.getSet(lockName.getBytes(),
String.valueOf(System.currentTimeMillis() + lockExoire + 1).getBytes());
return Long.parseLong(new String(set)) < System.currentTimeMillis();
}
}
}
return false;
});
}
/**
* 删除锁
* @param lockName
*/
public void delLock(String lockName) {
redisTemplate.delete(lockName);
}
/**
* 获取锁Key
* @param prefix 前缀
* @param name 名称
* @return
*/
public static String getFullKey(String prefix, String name) {
return prefix + "_" + name;
}
}
在service层使用
@Service
public class UserServiceImpl implements UserService {
@Autowired
private RedisLockUtil redisLockUtil;
@Autowired
private UserDao userDao;
@Override
@Transactional
public void consume(User user) {
Boolean lock = redisLockUtil.getLock(user.getUserName(), 20000);
if (lock){
System.out.println(Thread.currentThread().getName());
User user1 = selectUserById(user.getId());
if(user1 != null){
Integer num = user1.getNum();
if(num != null && num>0){
user1.setNum(num-1);
userDao.save(user1);
System.err.println("100元话费卡-卡号:"+new Random().nextInt(99999999)+"密码:"+new Random().nextInt(99999999) +",剩余抽奖次数:"+(num-1));
}
}
}
}
}
方式三:使用数据库的FOR UPDATE ,当查询这条数据的时候就上行锁,其他线程就查询不了
查询语句加上FOR UPDATE
SELECT * FROM t_user WHERE id=1 FOR UPDATE ;
service层上加上事务注解即可
@Override
@Transactional
public void consume(User user) {
System.out.println(Thread.currentThread().getName());
User user1 = selectUserById(user.getId());
if(user1 != null){
Integer num = user1.getNum();
if(num != null && num>0){
user1.setNum(num-1);
userDao.save(user1);
System.err.println("100元话费卡-卡号:"+new Random().nextInt(99999999)+"密码:"+new Random().nextInt(99999999) +",剩余抽奖次数:"+(num-1));
}
}
————————————————
版权声明:本文为CSDN博主「请把小熊还给我&」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
0条评论