preface

Time flies, two weeks have passed, it’s time to continue to fill in the hole, or you will be sprayed by the Internet.

This article is the third second kill system, through the actual code to explain, to help you understand the key points of the second kill system design, start the actual project.

This article mainly explains the second kill system, about the purchase (order) interface related to the single user brush prevention measures, mainly said two contents:

  • Panic buying interface Hide
  • Single user limiting frequency (limiting access times per unit of time)

Of course, both of these measures are useful in any system, and are not strictly unique to the design of a seckill system, so today’s content will be relatively generic.

In addition, I made a flow chart to describe the order process of seckill interface that we have implemented at present:

Review and article planning

  • Zero base overkill system (1) : prevent oversold
  • Zero base overkill system (ii) : token bucket limit + talk about oversold
  • Zero-base overhand snapkill system (3) : Snap up interface hide + single user limit frequency (this article)
  • Zero-base overkill system: Cache hotspot data using Redis
  • Zero-base overkill system: Message queues asynchronously process orders
  • .

Welcome to pay attention to my personal public account to get the most complete original article: Back-end technology talk (QR code see the bottom of the article)

The project source code is here

Mom no longer need to worry about only reading the article will not achieve:

Github.com/qqxx6661/mi…

The body of the

This section describes the seckill system

Check out the first article in the series, which will not be reviewed here:

Zero base overkill system (1) : prevent oversold

Panic buying interface Hide

In the introduction of the previous two articles, we have completed the traffic limiting to prevent overselling goods and buying interfaces, which has been able to prevent heavy traffic from directly blowing up our server. In this article, we will start to pay attention to some details.

For those of you with a bit of computer savvy and a bit of whimsy, click ON F12 to open your browser’s console, and after clicking on the Snap button, you’ll get a link to our snap interface. (Mobile apps and other clients can grab the bag)

Once the bad guys get the link, they just write a little crawler code to simulate a purchase request, and they can directly request our interface in the code to complete the order without clicking the order button. So there are thousands of wool pulling regiments, writing scripts and snapping up all kinds of instant goods.

They just need to start making a lot of requests in about 000 milliseconds, thinking it’s faster than everyone clicking the button on the APP. After all, human speed is limited, not to mention the APP may have to go through several layers of front-end verification before actually making a request.

Therefore, we need to hide the snapping interface, and the specific method of hiding the snapping interface (adding salt to the interface) is as follows:

  • Every time you click the seckill button, obtain a seckill verification value from the server (determine whether the interface has reached the seckill time).
  • Redis uses the cache user ID and item ID as the Key, and the second kill address as the Value cache validation Value
  • When a user requests a second kill item, the second kill verification value is used for verification.

We first stop to think carefully, through such a way, can prevent through the script brush interface?

Yes and no.

People who request interfaces directly can be prevented, but if the bad guys make the script more complex, ask for a validation value first, and then immediately request the snap, it can be snapped up successfully.

However, the bad guys who request the validating value interface also need to get the validating value after the panic buying time starts, and then they can apply for the panic buying interface. This is theoretically limited in terms of how long it takes to access the interface, and we can even out the ordering time for both the average user and the bad guys by adding more complex logic to the validating interface so that the interface that gets the validating value does not return the validating value quickly. So interface salt is still useful!

Let’s implement a simple salt interface code to throw some light on the problem.

Code logic implementation

The code is the same as the previous project, and we add two interfaces to it:

  • Gets the validation value interface
  • Single interface with verification value

Before, we only had two tables, one stock table for goods in stock and the other stockOrder table for records of successful orders. But this time it involves users, so we add a new user table and add a user named Zhang SAN. And in the order table, not only the item ID is recorded, but also the user ID is written.

The entire SQL structure is as follows, pay attention to a concise, temporarily do not add other redundant fields:

-- ----------------------------
-- Table structure for stock
-- ----------------------------
DROP TABLE IF EXISTS `stock`;
CREATE TABLE `stock` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(50) NOT NULL DEFAULT ' ' COMMENT 'name',
  `count` int(11) NOT NULL COMMENT 'inventory',
  `sale` int(11) NOT NULL COMMENT 'sold',
  `version` int(11) NOT NULL COMMENT 'Optimistic lock, version number',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of stock
-- ----------------------------
INSERT INTO `stock` VALUES ('1'.'iphone'.'50'.'0'.'0');
INSERT INTO `stock` VALUES ('2'.'mac'.'10'.'0'.'0');

-- ----------------------------
-- Table structure for stock_order
-- ----------------------------
DROP TABLE IF EXISTS `stock_order`;
CREATE TABLE `stock_order` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `sid` int(11) NOT NULL COMMENT 'inventory ID',
  `name` varchar(30) NOT NULL DEFAULT ' ' COMMENT 'Trade Name',
  `user_id` int(11) NOT NULL DEFAULT '0',
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Creation time',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of stock_order
-- ----------------------------

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `user_name` varchar(255) NOT NULL DEFAULT ' ',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;

-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES ('1'.'Joe');
Copy the code

SQL files are also placed in the open source code, do not worry.

Gets the validation value interface

This interface requires that the user ID and commodity ID be passed, the validation value returned, and the validation value

Add method to Controller:

/** * get the validation value * @return
 */
@RequestMapping(value = "/getVerifyHash", method = {RequestMethod.GET})
@ResponseBody
public String getVerifyHash(@RequestParam(value = "sid") Integer sid,
                            @RequestParam(value = "userId") Integer userId) {
    String hash;
    try {
        hash = userService.getVerifyHash(sid, userId);
    } catch (Exception e) {
        LOGGER.error("Failed to get validation hash because: [{}]", e.getMessage());
        return "Failed to get validation hash";
    }
    return String.format("Request snap validation hash value: %s".hash);
}
Copy the code

Add method to UserService:

@override public String getVerifyHash(Integer sid, Integer userId) throws Exception {// Verify whether logger.info ("Please verify by yourself whether it is within the buying time."); / / check User legitimacy User User = userMapper. SelectByPrimaryKey (userId. LongValue ());if (user == null) {
        throw new Exception("User does not exist");
    }
    LOGGER.info(User information: [{}], user.toString()); / / check goods legitimacy Stock Stock. = it getStockById (sid);if (stock == null) {
        throw new Exception("Goods don't exist.");
    }
    LOGGER.info("Commodity Information: [{}]", stock.toString()); / / generatedhashString verify = SALT + sid + userId; String verifyHash = DigestUtils.md5DigestAsHex(verify.getBytes()); / / will behashAnd user commodity information is stored in redis StringhashKey = CacheKey.HASH_KEY.getKey() + "_" + sid + "_" + userId;
    stringRedisTemplate.opsForValue().set(hashKey, verifyHash, 3600, TimeUnit.SECONDS);
    LOGGER.info(Redis writes: [{}] [{}]".hashKey, verifyHash);
    return verifyHash;
}
Copy the code

A Cache constant enumeration class CacheKey:

package cn.monitor4all.miaoshadao.utils;

public enum CacheKey {
    HASH_KEY("miaosha_hash"),
    LIMIT_KEY("miaosha_limit");

    private String key;

    private CacheKey(String key) {
        this.key = key;
    }
    public String getKey() {
        returnkey; }}Copy the code

Code explanation:

As you can see in the Service, after we get the user ID and commodity ID, we will check whether the commodity and user information exists in the table and verify the current time (I just write a line of LOGGER for simplicity, you can implement it according to your needs). In this case, the hash value is given. The Hash value is written into Redis and cached for 3600 seconds (1 hour). If the user does not place an order within 1 hour after receiving the Hash value, the user needs to retrieve the Hash value again.

It’s time to think a little bit more carefully. If this hash value is md5 based on the item + user information every time, it’s not too secure. After all, the user ID is not necessarily unknown to the user (for example, I use the self-increasing ID storage, certainly not safe), and the commodity ID, in case also leaked, so if the bad guys know that we are simple MD5, then directly hash out!

In the code, I prefixed the hash value with a salt, which is a handful of salt on the fixed string, and the salt is HASH_KEY(“miaosha_hash”), which is dead in the code. So if you don’t guess the salt, you can’t figure out the hash value.

This is just one example of how, in practice, you can put the salt somewhere else and keep changing it, or combine it with the timestamp, so that even your programmers can’t know what the original string of the hash value is.

Single interface with verification value

After getting the verification value at the front desk, the user clicks the order button, and the front end carries the characteristic value to place the order.

Add method to Controller:

/** * require validation of the purchase interface * @param sid * @return
 */
@RequestMapping(value = "/createOrderWithVerifiedUrl", method = {RequestMethod.GET})
@ResponseBody
public String createOrderWithVerifiedUrl(@RequestParam(value = "sid") Integer sid,
                                         @RequestParam(value = "userId") Integer userId,
                                         @RequestParam(value = "verifyHash") String verifyHash) {
    int stockLeft;
    try {
        stockLeft = orderService.createVerifiedOrder(sid, userId, verifyHash);
        LOGGER.info("Successful purchase, remaining inventory is: [{}]", stockLeft);
    } catch (Exception e) {
        LOGGER.error("Purchase failed: [{}]", e.getMessage());
        return e.getMessage();
    }
    return String.format("Successful purchase, remaining stock: % D", stockLeft);
}
Copy the code

Add methods to OrderService:

@Override public int createVerifiedOrder(Integer sid, Integer userId, String verifyHash) throws Exception {// Verify whether logger.info ("Please verify by yourself whether it is within the time of purchase, assuming successful verification here."); / / verificationhashValue validity StringhashKey = CacheKey.HASH_KEY.getKey() + "_" + sid + "_" + userId;
    String verifyHashInRedis = stringRedisTemplate.opsForValue().get(hashKey);
    if(! verifyHash.equals(verifyHashInRedis)) { throw new Exception("Hash value does not match Redis");
    }
    LOGGER.info("Verifying hash value validity succeeded"); / / check User legitimacy User User = userMapper. SelectByPrimaryKey (userId. LongValue ());if (user == null) {
        throw new Exception("User does not exist");
    }
    LOGGER.info("User information verified successfully: [{}]", user.toString()); / / check goods legitimacy Stock Stock. = it getStockById (sid);if (stock == null) {
        throw new Exception("Goods don't exist.");
    }
    LOGGER.info("Product information verified successfully: [{}]", stock.toString()); // saleStockOptimistic(stock); LOGGER.info("Optimistic lock update inventory success"); CreateOrderWithUserInfo (stock, userId); LOGGER.info("Order created successfully");

    return stock.getCount() - (stock.getSale()+1);
}
Copy the code

Code explanation:

As you can see from the service, we need to verify:

  • Commodity information
  • The user information
  • time
  • inventory

Thus, we have an order interface with validation.

Try out the interface

Let’s start with user 1, an outlaw fanatic named Zhang SAN, making a request:

http://localhost:8080/getVerifyHash?sid=1&userId=1
Copy the code

Results obtained:

Console output:

Don’t rush to place your order, let’s see if redis has the key stored:

Puppet problem, next, Joe can go to request orders!

http://localhost:8080/createOrderWithVerifiedUrl?sid=1&userId=1&verifyHash=d4ff4c458da98f69b880dd79c8a30bcf
Copy the code

Get the output:

The maniac outside the law has snapped it up!

Single user limit frequency

Suppose we’re ready to interface to hide, but like I said above, there is always a boring people will write a complex scripts, request a hash value first, then immediately request, purchase orders if your app button do is very poor, we all want to open grab after 0.5 seconds to request is successful, it could let the script can still snapping up success in front of everyone.

We need to make an additional measure to limit the frequency of buying by a single user.

In fact, it is very simple to think of using Redis to do access statistics for each user, or even with the product ID, to do access statistics for a single product, which is feasible.

We first implement a limit on the user’s access frequency. When the user applies for an order, we check the number of visits of the user. If the number exceeds the number of visits, we will not let him place an order!

Use Redis/Memcached

We solved the problem by using external caching, so that even in a distributed seckill system where requests are randomly diverted, we can accurately control the number of visits per user.

Add method to Controller:

/** * requires a validated snap up interface + single-user limited access frequency * @param sid * @return
 */
@RequestMapping(value = "/createOrderWithVerifiedUrlAndLimit", method = {RequestMethod.GET})
@ResponseBody
public String createOrderWithVerifiedUrlAndLimit(@RequestParam(value = "sid") Integer sid,
                                                 @RequestParam(value = "userId") Integer userId,
                                                 @RequestParam(value = "verifyHash") String verifyHash) {
    int stockLeft;
    try {
        int count = userService.addUserCount(userId);
        LOGGER.info("The number of user visits up to this time is: [{}]", count);
        boolean isBanned = userService.getUserIsBanned(userId);
        if (isBanned) {
            return "Purchase failed, frequency limit exceeded.";
        }
        stockLeft = orderService.createVerifiedOrder(sid, userId, verifyHash);
        LOGGER.info("Successful purchase, remaining inventory is: [{}]", stockLeft);
    } catch (Exception e) {
        LOGGER.error("Purchase failed: [{}]", e.getMessage());
        return e.getMessage();
    }
    return String.format("Successful purchase, remaining stock: % D", stockLeft);
}
Copy the code

Add two methods to UserService:

  • AddUserCount: increases the number of visits each time the order interface is accessed, writing Redis
  • GetUserIsBanned: Reads the number of visits by the user from Redis, and disapproves the purchase after 10! We can’t let Joe act like an outlaw.
@Override
    public int addUserCount(Integer userId) throws Exception {
        String limitKey = CacheKey.LIMIT_KEY.getKey() + "_" + userId;
        String limitNum = stringRedisTemplate.opsForValue().get(limitKey);
        int limit= 1;if (limitNum == null) {
            stringRedisTemplate.opsForValue().set(limitKey, "0", 3600, TimeUnit.SECONDS);
        } else {
            limit = Integer.parseInt(limitNum) + 1;
            stringRedisTemplate.opsForValue().set(limitKey, String.valueOf(limit), 3600, TimeUnit.SECONDS);
        }
        return limit;
    }

    @Override
    public boolean getUserIsBanned(Integer userId) {
        String limitKey = CacheKey.LIMIT_KEY.getKey() + "_" + userId;
        String limitNum = stringRedisTemplate.opsForValue().get(limitKey);
        if (limitNum == null) {
            LOGGER.error("The user did not access the application verification value record, suspected abnormal");
            return true;
        }
        return Integer.parseInt(limitNum) > ALLOW_COUNT;
    }
Copy the code

Try the interface

Using JMeter to access the interface for 30 times, you can see that after placing an order for 10 times, no more purchases are allowed:

And we’re done.

Can the user access frequency statistics be implemented without Redis/Memcached

Wait, if you say you don’t want to use Redis, is there a way to implement access frequency statistics? Yes, if you forgo distributed deployment services, you can store access times in memory, for example:

  • Google Guava memory cache
  • The state pattern

I don’t know how you reviewed the design pattern. If you haven’t reviewed the state pattern, you can first look at the definition of state pattern. The state pattern is well suited for this access limit scenario.

On my blog and public account (Back-end Technology Chatter), I have written a series of Design Patterns Study Rooms, detailing each design pattern, which you can check out if you are interested. Introduction: Why do we have design Patterns?

I’m not going to implement it here, since we’re still focused on distributed seckilling services, but I’ll give you a taste of the state mode in action by quoting an example from a blog:

www.cnblogs.com/java-my-lif…

Considering the application of an online voting system, the same user can only cast one vote. If a user votes repeatedly and votes more than 5 times, it is judged as malicious voting, and the user’s voting qualification should be cancelled, of course, at the same time, his vote should be cancelled. If a user votes for more than eight times, the user is blacklisted and prohibited from logging in to and using the system.

Public class VoteManager {private VoteState state = null; Private Map<String,String> mapVote = new HashMap<String,String>(); Map<String,Integer> corresponds to Map< user name, Number of votes > private Map<String,Integer> mapVoteCount = new HashMap<String,Integer>(); /** * Public Map<String, String>getMapVote() {
        returnmapVote; } public void vote(String user,String voteItem){//1. Integer oldVoteCount = mapVotecount.get (user);if(oldVoteCount == null){ oldVoteCount = 0; } oldVoteCount += 1; mapVoteCount.put(user, oldVoteCount); //2. Determine the voting type of the user, which is equivalent to determine the corresponding status // whether it is normal voting, repeated voting, malicious voting or blacklist statusif(oldVoteCount == 1){
            state = new NormalVoteState();
        }
        else if(oldVoteCount > 1 && oldVoteCount < 5){
            state = new RepeatVoteState();
        }
        else if(oldVoteCount >= 5 && oldVoteCount <8){
            state = new SpiteVoteState();
        }
        else if(oldVoteCount > 8){ state = new BlackVoteState(); State.vote (user, voteItem, this); }}Copy the code
public class Client {

    public static void main(String[] args) {
        
        VoteManager vm = new VoteManager();
        for(int i=0; i<9; i++){ vm.vote("u1"."A"); }}}Copy the code

Results:

conclusion

The code of this project is open source on Github, we can use it freely:

Github.com/qqxx6661/mi…

Finally, thank you all for your love.

I hope you can support my princess: Back-end technology ramble.

reference

  • Cloud.tencent.com/developer/a…
  • Juejin. Cn/post / 684490…
  • Zhenganwen. Top/posts / 30 bb5…
  • www.cnblogs.com/java-my-lif…

Pay attention to my

I’m a back-end development engineer.

Focus on back-end development, data security, Internet of Things, edge computing direction, welcome to exchange.

I can be found on every platform

  • Wechat official account: A ramble on back-end technology
  • Making: @ qqxx6661
  • CSDN: @ Rude3knife
  • Zhihu: @ Ramble on back-end technology
  • Jane: @pretty three knives a knife
  • Nuggets: @ pretty three knife knife

Original blog main content

  • Back-end development techniques
  • Java Interview Knowledge
  • Design patterns/data structures
  • LeetCode/ Sword finger offer algorithm parsing
  • SpringBoot/SpringCloud entry combat series
  • Data analysis/data crawler
  • Anecdotes/good books to share/personal life

Personal public account: Back-end technology ramble

If the article is helpful to you, you might as well bookmark, forward, in the look ~