java防止重复提交

公司有个抽奖活动每人一天只能抽取一次,有用户恶意短时间内重复提交多次导致抽奖多发情况

解决思路

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-8
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));
                }
            }
    }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值